├── .gitignore ├── 0. LP Intro - Blending.ipynb ├── 1. LP Problem - DMC.ipynb ├── 1.2 LP Problem - DMC - 3x3 and 4x4 (Generalized)-Copy1.ipynb ├── 1.2 LP Problem - DMC - 3x3 and 4x4 (Generalized).ipynb ├── 1.2 LP Problem - DMC - 3x3 and 4x4 (Generalized).py ├── 2. DMC Overview.ipynb ├── DMC.ipynb ├── DMC.py ├── README.md ├── WhiskasModel.lp ├── assets └── img │ ├── debut.png │ └── ranade.png ├── main.png ├── requirements.txt └── visualize_lp.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | *.tsbuildinfo 7 | 8 | # Created by https://www.gitignore.io/api/python 9 | # Edit at https://www.gitignore.io/?templates=python 10 | 11 | ### Python ### 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | .spyproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # Mr Developer 93 | .mr.developer.cfg 94 | .project 95 | .pydevproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .dmypy.json 103 | dmypy.json 104 | 105 | # Pyre type checker 106 | .pyre/ 107 | 108 | # OS X stuff 109 | *.DS_Store 110 | 111 | # End of https://www.gitignore.io/api/python 112 | 113 | _temp_extension 114 | junit.xml 115 | [uU]ntitled* 116 | notebook/static/* 117 | !notebook/static/favicons 118 | notebook/labextension 119 | notebook/schemas 120 | docs/source/changelog.md 121 | docs/source/contributing.md 122 | 123 | # playwright 124 | ui-tests/test-results 125 | ui-tests/playwright-report 126 | 127 | # VSCode 128 | .vscode -------------------------------------------------------------------------------- /1.2 LP Problem - DMC - 3x3 and 4x4 (Generalized)-Copy1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "460e40f9", 6 | "metadata": {}, 7 | "source": [ 8 | "# Linear Programming: DMC Formulation (3x3)\n", 9 | "- Author: Siang Lim, Shams Elnawawi\n", 10 | "- Last Updated: June 7th 2022\n", 11 | "- Created March 2nd 2022\n", 12 | "\n", 13 | "## References\n", 14 | "- Morshedi, A. M., Cutler, C. R., & Skrovanek, T. A. (1985). Optimal solution of dynamic matrix control with linear programing techniques (LDMC). In 1985 American Control Conference (pp. 199-208). IEEE.\n", 15 | "- Sorensen, R. C., & Cutler, C. R. (1998). LP integrates economics into dynamic matrix control. Hydrocarbon Processing, 77(9), 57-65.\n", 16 | "- Ranade, S. M., & Torres, E. (2009). From dynamic mysterious control to dynamic manageable control. Hydrocarbon Processing, 88(3), 77-81.\n", 17 | "- Godoy, J. L., Ferramosca, A., & González, A. H. (2017). Economic performance assessment and monitoring in LP-DMC type controller applications. Journal of Process Control, 57, 26-37." 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "id": "c4f71c0d", 23 | "metadata": {}, 24 | "source": [ 25 | "## Before starting\n", 26 | "\n", 27 | "If this is your first time using this notebook, you may need to install a couple of packages to be able to run the code. If you end up with a ```ModuleNotFoundError```, go to the below cell (with the ```pip install``` lines) and uncomment both lines, then run the cell. \n", 28 | "\n", 29 | "You will have to restart the kernel after running installation commands, under the \"Kernel\" tab in the toolbar." 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 1, 35 | "id": "6ecd4b52", 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "# pip install pulp\n", 40 | "# pip install matplotlib-label-lines" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "id": "7e804c41", 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "# Just importing libraries and tweaking the plot settings\n", 51 | "import numpy as np\n", 52 | "import matplotlib.pyplot as plt\n", 53 | "%matplotlib widget\n", 54 | "\n", 55 | "from matplotlib.ticker import StrMethodFormatter\n", 56 | "import matplotlib.gridspec as gridspec\n", 57 | "from labellines import labelLine, labelLines\n", 58 | "\n", 59 | "from ipywidgets.widgets.interaction import interact\n", 60 | "import ipywidgets.widgets as widgets\n", 61 | "from ipywidgets import Layout\n", 62 | "\n", 63 | "# Import PuLP modeler functions\n", 64 | "from pulp import *\n", 65 | "\n", 66 | "fsize = 8\n", 67 | "tsize = 12\n", 68 | "tdir = 'in'\n", 69 | "major = 5.0\n", 70 | "minor = 3.0\n", 71 | "lwidth = 0.8\n", 72 | "lhandle = 2.0\n", 73 | "plt.style.use('default')\n", 74 | "plt.rcParams['font.size'] = fsize\n", 75 | "plt.rcParams['legend.fontsize'] = tsize\n", 76 | "plt.rcParams['xtick.direction'] = tdir\n", 77 | "plt.rcParams['ytick.direction'] = tdir\n", 78 | "plt.rcParams['xtick.major.size'] = major\n", 79 | "plt.rcParams['xtick.minor.size'] = minor\n", 80 | "plt.rcParams['ytick.major.size'] = 5.0\n", 81 | "plt.rcParams['ytick.minor.size'] = 3.0\n", 82 | "plt.rcParams['axes.linewidth'] = lwidth\n", 83 | "plt.rcParams['legend.handlelength'] = lhandle" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "id": "368d8fa3", 89 | "metadata": {}, 90 | "source": [ 91 | "## MV-CV equations\n", 92 | "\n", 93 | "$$\n", 94 | "G =\n", 95 | " \\begin{bmatrix}\n", 96 | " -0.200 & -0.072 & 0.0774 \\\\\n", 97 | " 0.125 & -0.954 & 0.0063 \\\\\n", 98 | " 0.025 & 0.101 & −0.0143\n", 99 | " \\end{bmatrix}\n", 100 | "$$\n", 101 | "\n", 102 | "Using the gain matrix, the CV relationship can be written in terms of its MVs, starting with:\n", 103 | "\n", 104 | "$$\n", 105 | "\\Delta \\text{CV}_{1} = G_{11} \\Delta \\text{MV}_{1} + G_{12} \\Delta \\text{MV}_{2} + G_{13} \\Delta \\text{MV}_{3} \\\\ \n", 106 | "\\Delta \\text{CV}_{2} = G_{21} \\Delta \\text{MV}_{1} + G_{22} \\Delta \\text{MV}_{2} + G_{23} \\Delta \\text{MV}_{3} \\\\\n", 107 | "\\Delta \\text{CV}_{3} = G_{31} \\Delta \\text{MV}_{1} + G_{32} \\Delta \\text{MV}_{2} + G_{33} \\Delta \\text{MV}_{3}\n", 108 | "$$\n", 109 | "\n", 110 | "We can impose upper and lower limits on the MVs:\n", 111 | "\n", 112 | "$$\n", 113 | "\\text{MV}_{1, \\text{Lo}} \\leq \\text{MV}_{1} \\leq \\text{MV}_{1, \\text{Hi}}\\\\ \n", 114 | "\\text{MV}_{2, \\text{Lo}} \\leq \\text{MV}_{2} \\leq \\text{MV}_{2, \\text{Hi}}\\\\\n", 115 | "\\text{MV}_{3, \\text{Lo}} \\leq \\text{MV}_{3} \\leq \\text{MV}_{3, \\text{Hi}}\\\\\n", 116 | "$$\n", 117 | "\n", 118 | "As well as the CVs:\n", 119 | "\n", 120 | "$$\n", 121 | "\\text{CV}_{1, \\text{Lo}} \\leq \\text{CV}_{1} \\leq \\text{CV}_{1, \\text{Hi}}\\\\ \n", 122 | "\\text{CV}_{2, \\text{Lo}} \\leq \\text{CV}_{2} \\leq \\text{CV}_{2, \\text{Hi}}\\\\\n", 123 | "\\text{CV}_{3, \\text{Lo}} \\leq \\text{CV}_{3} \\leq \\text{CV}_{3, \\text{Hi}}\\\\\n", 124 | "$$\n", 125 | "\n", 126 | "Since the CVs are related to the MVs by the gain matrix, we can substitute the equations to get CV limits in terms of MV movements:\n", 127 | "\n", 128 | "$$\n", 129 | "G_{11} \\Delta \\text{MV}_{1} + G_{12} \\Delta \\text{MV}_{2} + G_{13} \\Delta \\text{MV}_{3} \\leq \\Delta \\text{CV}_{1, \\text{Hi}}\\\\\n", 130 | "G_{11} \\Delta \\text{MV}_{1} + G_{12} \\Delta \\text{MV}_{2} + G_{13} \\Delta \\text{MV}_{3} \\geq \\Delta \\text{CV}_{1, \\text{Lo}}\\\\\n", 131 | "G_{21} \\Delta \\text{MV}_{1} + G_{22} \\Delta \\text{MV}_{2} + G_{23} \\Delta \\text{MV}_{3} \\leq \\Delta \\text{CV}_{2, \\text{Hi}}\\\\\n", 132 | "G_{21} \\Delta \\text{MV}_{1} + G_{22} \\Delta \\text{MV}_{2} + G_{23} \\Delta \\text{MV}_{3} \\geq \\Delta \\text{CV}_{2, \\text{Lo}}\\\\\n", 133 | "G_{31} \\Delta \\text{MV}_{1} + G_{32} \\Delta \\text{MV}_{2} + G_{33} \\Delta \\text{MV}_{3} \\leq \\Delta \\text{CV}_{3, \\text{Hi}}\\\\\n", 134 | "G_{31} \\Delta \\text{MV}_{1} + G_{22} \\Delta \\text{MV}_{2} + G_{33} \\Delta \\text{MV}_{3} \\geq \\Delta \\text{CV}_{3, \\text{Lo}}\\\\\n", 135 | "$$\n", 136 | "\n", 137 | "Let's see what this looks like:" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "id": "90f372d2", 143 | "metadata": {}, 144 | "source": [ 145 | "# Gains and CV Limits" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 3, 151 | "id": "6873757d", 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "G11 = -0.200\n", 156 | "G12 = -0.072\n", 157 | "G13 = 0.0774\n", 158 | "G14 = 0.0574\n", 159 | "\n", 160 | "G21 = 0.125\n", 161 | "G22 = -0.954\n", 162 | "G23 = 0.0063\n", 163 | "G24 = 0.0374\n", 164 | "\n", 165 | "G31 = 0.025\n", 166 | "G32 = 0.101\n", 167 | "G33 = -0.0143\n", 168 | "G34 = -0.1143\n", 169 | "\n", 170 | "G41 = 0.120\n", 171 | "G42 = 0.150\n", 172 | "G43 = -0.1143\n", 173 | "G44 = -0.0943\n", 174 | "\n", 175 | "CV1Lo = -6\n", 176 | "CV1Hi = 6\n", 177 | "\n", 178 | "CV2Lo = -10\n", 179 | "CV2Hi = 10.5\n", 180 | "\n", 181 | "CV3Lo = -3\n", 182 | "CV3Hi = 3.5\n", 183 | "\n", 184 | "CV4Lo = -4.5\n", 185 | "CV4Hi = 4\n", 186 | "\n", 187 | "cost_MV1 = -1\n", 188 | "cost_MV2 = -1\n", 189 | "cost_MV3 = -1\n", 190 | "cost_MV4 = 1\n", 191 | "\n", 192 | "limits = 10\n", 193 | "plot_limits = 20\n", 194 | "\n", 195 | "MV1Lo = -limits\n", 196 | "MV1Hi = limits\n", 197 | "MV2Lo = -limits\n", 198 | "MV2Hi = limits\n", 199 | "MV3Lo = -limits\n", 200 | "MV3Hi = 15\n", 201 | "MV4Lo = -limits\n", 202 | "MV4Hi = -limits" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 4, 208 | "id": "562bb6e4", 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "G = [(G11, G12, G13, G14), \n", 213 | " (G21, G22, G23, G24), \n", 214 | " (G31, G32, G33, G34), \n", 215 | " (G41, G42, G43, G44)]\n", 216 | "\n", 217 | "CV_values = [(CV1Lo, CV1Hi), \n", 218 | " (CV2Lo, CV2Hi), \n", 219 | " (CV3Lo, CV3Hi)] \n", 220 | "# (CV4Lo, CV4Hi)]\n", 221 | "CV_init_vals= [(-2.5,2.0), \n", 222 | " (-4,4.5), \n", 223 | " (-0.9,0.2)] \n", 224 | "# (-4,4.5)]\n", 225 | "MV_costs = [cost_MV1, \n", 226 | " cost_MV2, \n", 227 | " cost_MV3]\n", 228 | "# cost_MV4]\n", 229 | "MV_values = [(MV1Lo, MV1Hi), \n", 230 | " (MV2Lo, MV2Hi), \n", 231 | " (MV3Lo, MV3Hi)] \n", 232 | "# (MV4Lo, MV4Hi)]\n", 233 | "\n", 234 | "# (x,y,c) triplets of MVs, c for constant, i.e. \n", 235 | "# (MV1, MV2, MV3) for the first plot, \n", 236 | "# (MV1, MV3, MV2) for the second, \n", 237 | "# (MV2, MV3, MV1) for the third\n", 238 | "# plot_MV_indices = [(0,1), (0,2), (1,2)]\n", 239 | "\n", 240 | "nCVs = len(CV_values)\n", 241 | "nMVs = len(MV_values)" 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": 5, 247 | "id": "a7fdf889-850d-40db-93d0-a93bd835d92e", 248 | "metadata": {}, 249 | "outputs": [ 250 | { 251 | "name": "stdout", 252 | "output_type": "stream", 253 | "text": [ 254 | "{'CV1 Limits': (-2.5, 2.0), 'CV2 Limits': (-4.0, 4.5), 'CV3 Limits': (-0.9, 0.2), 'MV1 Cost': -1.0, 'MV2 Cost': -1.0, 'MV3 Cost': -1.0, 'MV1 Limits': (-10.0, 10.0), 'MV2 Limits': (-10.0, 10.0), 'MV3 Limits': (-10.0, 15.0)}\n" 255 | ] 256 | } 257 | ], 258 | "source": [ 259 | "CV_widgets = []\n", 260 | "MV_widgets = []\n", 261 | "cost_widgets = []\n", 262 | "stepsize = 0.2\n", 263 | "\n", 264 | "# Make CV sliders\n", 265 | "for i in range(nCVs):\n", 266 | " widget = widgets.FloatRangeSlider(\n", 267 | " value=CV_init_vals[i], min=CV_values[i][0], max=CV_values[i][1], step=stepsize,\n", 268 | " description=f'CV{i+1} Limits', continuous_update=False)\n", 269 | " CV_widgets.append(widget)\n", 270 | "\n", 271 | "# Make MV sliders\n", 272 | "for i in range(nMVs):\n", 273 | " widget = widgets.FloatRangeSlider(\n", 274 | " value=MV_values[i], min=MV_values[i][0], max=MV_values[i][1], step=stepsize,\n", 275 | " description=f'MV{i+1} Limits', continuous_update=False)\n", 276 | " MV_widgets.append(widget)\n", 277 | " \n", 278 | "# Make cost sliders\n", 279 | "for i in range(nMVs):\n", 280 | " widget = widgets.FloatSlider(\n", 281 | " value=MV_costs[i], min=-3, max=3, step=stepsize/2, # finer step for costs\n", 282 | " description=f'MV{i+1} Cost', continuous_update=True)\n", 283 | " cost_widgets.append(widget)\n", 284 | " \n", 285 | "sliders = CV_widgets + cost_widgets + MV_widgets\n", 286 | "values = [slider.value for slider in sliders]\n", 287 | "values_dict = {}\n", 288 | "for slider in sliders:\n", 289 | " values_dict[slider.description] = slider.value\n", 290 | "print(values_dict)" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 6, 296 | "id": "cb507e02-fab8-41d7-a338-89f60cbe41f2", 297 | "metadata": {}, 298 | "outputs": [], 299 | "source": [ 300 | "# display ui\n", 301 | "ncols = 3\n", 302 | "\n", 303 | "nrows = nCVs // ncols + 1\n", 304 | "CV_row = []\n", 305 | "for i in range(nrows):\n", 306 | " CV_row.append(widgets.VBox(CV_widgets[(ncols)*i:(ncols)*i+ncols]))\n", 307 | " \n", 308 | "nrows = nMVs // ncols + 1\n", 309 | "MV_row = []\n", 310 | "cost_row = []\n", 311 | "for i in range(nrows):\n", 312 | " MV_row.append(widgets.VBox(MV_widgets[(ncols)*i:(ncols)*i+ncols]))\n", 313 | " cost_row.append(widgets.VBox(cost_widgets[(ncols)*i:(ncols)*i+ncols]))\n", 314 | "\n", 315 | "ui = widgets.VBox([widgets.VBox(CV_row),\n", 316 | " widgets.VBox(MV_row),\n", 317 | " widgets.VBox(cost_row)])" 318 | ] 319 | }, 320 | { 321 | "cell_type": "code", 322 | "execution_count": 7, 323 | "id": "337d52ee", 324 | "metadata": {}, 325 | "outputs": [], 326 | "source": [ 327 | "# Solve the LP\n", 328 | "def run_lp(values_dict):\n", 329 | " prob = LpProblem(\"DMC_problem\",LpMinimize)\n", 330 | " \n", 331 | " # How many MVs\n", 332 | " MVs = []\n", 333 | " for i in range(nMVs):\n", 334 | " MVs.append(LpVariable(f\"MV{i+1}\",-limits))\n", 335 | " \n", 336 | " # the objective function\n", 337 | " obj = 0\n", 338 | " for indx, MV in enumerate(MVs):\n", 339 | " obj += values_dict[f\"MV{indx+1} Cost\"]*MV\n", 340 | " \n", 341 | " prob += obj, \"Cost function of MVs\"\n", 342 | " \n", 343 | " # constraint formulation in terms of MV1 and MV2\n", 344 | " CV_contraint_lo = []\n", 345 | " CV_contraint_hi = []\n", 346 | " \n", 347 | " for i in range(nCVs):\n", 348 | " c = 0\n", 349 | " for indx, MV in enumerate(MVs):\n", 350 | " c += G[i][indx]*MV\n", 351 | " prob += c <= values_dict[f'CV{i+1} Limits'][1], f'CV{i+1} High Limit'\n", 352 | " prob += c >= values_dict[f'CV{i+1} Limits'][0], f'CV{i+1} Low Limit'\n", 353 | " \n", 354 | "# prob += G[i][0]*MV1 + G[i][1]*MV2 <= values_dict[f'CV{i+1} Limits'][1], f'CV{i+1} High Limit'\n", 355 | "# prob += G[i][0]*MV1 + G[i][1]*MV2 >= values_dict[f'CV{i+1} Limits'][0], f'CV{i+1} Low Limit'\n", 356 | " \n", 357 | " for indx, MV in enumerate(MVs):\n", 358 | " prob += MV <= values_dict[f'MV{indx+1} Limits'][1], f'MV{indx+1} High Limit'\n", 359 | " prob += MV >= values_dict[f'MV{indx+1} Limits'][0], f'MV{indx+1} Low Limit' \n", 360 | " \n", 361 | " if (prob.solve(PULP_CBC_CMD(msg=0)) == 1):\n", 362 | "# print([v.varValue for v in prob.variables()])\n", 363 | "# print([v for v in prob.variables()])\n", 364 | " return [v.varValue for v in prob.variables()], value(prob.objective)\n", 365 | " else:\n", 366 | " print(\"NOT SOLVED - Infeasibility!\")\n", 367 | " return np.zeros(nMVs), 0" 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 8, 373 | "id": "f0e50a42", 374 | "metadata": {}, 375 | "outputs": [], 376 | "source": [ 377 | "d = np.linspace(-plot_limits, plot_limits, 1000)\n", 378 | "x,y = np.meshgrid(d,d)\n", 379 | "\n", 380 | "# Recalculate shaded regions\n", 381 | "constraints = []\n", 382 | "for i in range(nCVs):\n", 383 | " c_hi = G[i][0]*x + G[i][1]*y <= values_dict[f'CV{i+1} Limits'][1]\n", 384 | " c_lo = G[i][0]*x + G[i][1]*y >= values_dict[f'CV{i+1} Limits'][0]\n", 385 | " constraints.append(c_lo)\n", 386 | " constraints.append(c_hi)\n", 387 | "\n", 388 | "# the MVs\n", 389 | "for indx, var in enumerate([x,y]):\n", 390 | " c_hi = var <= values_dict[f'MV{indx+1} Limits'][1]\n", 391 | " c_lo = var >= values_dict[f'MV{indx+1} Limits'][0]\n", 392 | " constraints.append(c_lo)\n", 393 | " constraints.append(c_hi)" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": 9, 399 | "id": "3b8daec8", 400 | "metadata": {}, 401 | "outputs": [], 402 | "source": [ 403 | "def handle_slider_change(change):\n", 404 | " ## grab slider vals\n", 405 | " values_dict = {}\n", 406 | " for slider in sliders:\n", 407 | " values_dict[slider.description] = slider.value\n", 408 | " \n", 409 | " # Find the LP soln\n", 410 | " soln, V = run_lp(values_dict)\n", 411 | " \n", 412 | " for indx, key in enumerate(axs_dict):\n", 413 | " ax = axs_dict[key]\n", 414 | " \n", 415 | " # remove previous line labels (TODO could be more efficient, modify labelLine library to return handles)\n", 416 | " # for txt in ax.texts:\n", 417 | " # txt.remove()\n", 418 | " \n", 419 | " # figure out which MVs we are plotting, and which ones are constant\n", 420 | " x_MV = key[1] # column is x-axis\n", 421 | " y_MV = key[0]+1 # row is y-axis\n", 422 | " c_MV = [a for a in range(nMVs) if a != x_MV and a != y_MV] \n", 423 | "\n", 424 | " # Recalculate shaded regions \n", 425 | " constraints = []\n", 426 | " \n", 427 | " # the obj func\n", 428 | " y_obj = (V - sum([values_dict[f'MV{cmv+1} Cost']*soln[cmv] for cmv in c_MV]) - values_dict[f'MV{x_MV+1} Cost']*d)/values_dict[f'MV{y_MV+1} Cost']\n", 429 | " \n", 430 | " # Update CV constraint lines\n", 431 | " for i in range(nCVs):\n", 432 | " cv_lim_lo = values_dict[f'CV{i+1} Limits'][0]\n", 433 | " cv_lim_hi = values_dict[f'CV{i+1} Limits'][1]\n", 434 | "\n", 435 | " # for CV constraint lines\n", 436 | " y_lo = (cv_lim_lo - G[i][x_MV]*d - sum([G[i][cmv]*soln[cmv] for cmv in c_MV]))/G[i][y_MV]\n", 437 | " y_hi = (cv_lim_hi - G[i][x_MV]*d - sum([G[i][cmv]*soln[cmv] for cmv in c_MV]))/G[i][y_MV]\n", 438 | " lines_handler_dict[key]['CV_lines_lo'][i].set_data(d, y_lo)\n", 439 | " lines_handler_dict[key]['CV_lines_hi'][i].set_data(d, y_hi)\n", 440 | "\n", 441 | " # for shading\n", 442 | " c_hi = G[i][x_MV]*x + G[i][y_MV]*y + sum([G[i][cmv]*soln[cmv] for cmv in c_MV]) <= cv_lim_hi\n", 443 | " c_lo = G[i][x_MV]*x + G[i][y_MV]*y + sum([G[i][cmv]*soln[cmv] for cmv in c_MV]) >= cv_lim_lo\n", 444 | " constraints.append(c_lo)\n", 445 | " constraints.append(c_hi)\n", 446 | " \n", 447 | " # search for valid xvals where the yvals are within plots limits\n", 448 | " # x_valid_lo = d[(y_lo < plot_limits) & (y_lo > -plot_limits)]\n", 449 | " # x_valid_hi = d[(y_hi < plot_limits) & (y_hi > -plot_limits)]\n", 450 | " # offset_lo = max(0,min(10, len(x_valid_lo)-3))\n", 451 | " # offset_hi = max(0,min(10, len(x_valid_hi)-3))\n", 452 | " # if len(x_valid_lo) > 0:\n", 453 | " # txt_cv_lo = labelLine(line_lo, x_valid_lo[offset_lo], fontsize=5, zorder=2.5)\n", 454 | " # if len(x_valid_hi) > 0:\n", 455 | " # txt_cv_hi = labelLine(line_hi, x_valid_hi[-offset_hi], fontsize=5, zorder=2.5) \n", 456 | "\n", 457 | " # Update MV constraint lines\n", 458 | " lines_handler_dict[key]['v_lo'].set_data([values_dict[f'MV{x_MV+1} Limits'][0],values_dict[f'MV{x_MV+1} Limits'][0]], [-limits, limits])\n", 459 | " lines_handler_dict[key]['v_hi'].set_data([values_dict[f'MV{x_MV+1} Limits'][1],values_dict[f'MV{x_MV+1} Limits'][1]], [-limits, limits])\n", 460 | " lines_handler_dict[key]['h_lo'].set_data([-limits, limits], [values_dict[f'MV{y_MV+1} Limits'][0],values_dict[f'MV{y_MV+1} Limits'][0]])\n", 461 | " lines_handler_dict[key]['h_hi'].set_data([-limits, limits], [values_dict[f'MV{y_MV+1} Limits'][1],values_dict[f'MV{y_MV+1} Limits'][1]]) \n", 462 | "\n", 463 | " # the 4 MV limits for this ax\n", 464 | " constraints.append(x >= values_dict[f'MV{x_MV+1} Limits'][0])\n", 465 | " constraints.append(x <= values_dict[f'MV{x_MV+1} Limits'][1])\n", 466 | " constraints.append(y >= values_dict[f'MV{y_MV+1} Limits'][0])\n", 467 | " constraints.append(y <= values_dict[f'MV{y_MV+1} Limits'][1]) \n", 468 | "\n", 469 | " # Shade the right regions\n", 470 | " lines_handler_dict[key]['im'].set_data((np.logical_and.reduce(constraints)).astype(float))\n", 471 | " \n", 472 | " # the quiver/vector field\n", 473 | " z_obj = (values_dict[f'MV{x_MV+1} Cost'] * xv) + (values_dict[f'MV{y_MV+1} Cost'] * yv) + sum([values_dict[f'MV{cmv+1} Cost']*soln[cmv] for cmv in c_MV])\n", 474 | " lines_handler_dict[key]['quiver'].set_UVC(-values_dict[f'MV{x_MV+1} Cost'],-values_dict[f'MV{y_MV+1} Cost'], z_obj)\n", 475 | "\n", 476 | " # the soln\n", 477 | " lines_handler_dict[key]['soln_marker'].set_data(soln[x_MV], soln[y_MV]);\n", 478 | " lines_handler_dict[key]['soln_text'].set_position((soln[x_MV], soln[y_MV]));\n", 479 | " lines_handler_dict[key]['soln_text'].set_text(\"({:.1f}, {:.1f})\".format(soln[x_MV], soln[y_MV]))\n", 480 | " lines_handler_dict[key]['soln_func'].set_data(d, y_obj);\n", 481 | " \n", 482 | " fig.canvas.draw()" 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": 10, 488 | "id": "8236d3a0-005d-440d-9482-15ec484d788a", 489 | "metadata": {}, 490 | "outputs": [ 491 | { 492 | "data": { 493 | "application/vnd.jupyter.widget-view+json": { 494 | "model_id": "9fb33089b9d34fc9b14df25515cf3d64", 495 | "version_major": 2, 496 | "version_minor": 0 497 | }, 498 | "text/plain": [ 499 | "HBox(children=(Output(), VBox(children=(HTMLMath(value='

Use the controls to interact with this linear progr…" 500 | ] 501 | }, 502 | "metadata": {}, 503 | "output_type": "display_data" 504 | } 505 | ], 506 | "source": [ 507 | "# Initialize plots\n", 508 | "d = np.linspace(-plot_limits, plot_limits, 100)\n", 509 | "x,y = np.meshgrid(d,d)\n", 510 | "\n", 511 | "gs = gridspec.GridSpec(nMVs-1, nMVs-1)\n", 512 | "gs.update(wspace=0.05, hspace=0.05)\n", 513 | "gs_indices = [(row, col) for row in range(nMVs-2,-1,-1) for col in range(row+1)]\n", 514 | "axs_dict = {}\n", 515 | "\n", 516 | "# mesh for vector field\n", 517 | "dvec = np.linspace(-plot_limits + (0.1*plot_limits), plot_limits - (0.1*plot_limits), 12)\n", 518 | "xv, yv = np.meshgrid(dvec, dvec)\n", 519 | "\n", 520 | "# init soln\n", 521 | "soln, V = run_lp(values_dict)\n", 522 | "\n", 523 | "# initialize line handlers\n", 524 | "CV_lines_lo = []\n", 525 | "CV_lines_hi = []\n", 526 | "MV_lines_lo = []\n", 527 | "MV_lines_hi = []\n", 528 | "lines_handler_dict = {}\n", 529 | "\n", 530 | "colors = ['r', 'b', 'y', 'g'] # TODO generalize this using a cmap instead of defining manual colors??!\n", 531 | "\n", 532 | "# plot as widget\n", 533 | "output = widgets.Output()\n", 534 | "with output:\n", 535 | " fig = plt.figure(figsize=(6,6), dpi=100, facecolor='white')\n", 536 | " plt.show()\n", 537 | " \n", 538 | "for r,c in gs_indices:\n", 539 | " # build the shared axes correctly and handle the tick labels\n", 540 | " if (r == nMVs-2 and c == 0): # no shared axis for the ax, bottom left corner\n", 541 | " ax = plt.subplot(gs[r,c])\n", 542 | " elif(c != 0): # all non-first columns share a y-axis with stuff to the left of it\n", 543 | " ax = plt.subplot(gs[r,c], sharey=axs_dict[(r,0)])\n", 544 | " plt.setp(ax.get_yticklabels(), visible=False)\n", 545 | " elif(r != nMVs-2): # all non-first rows share a x-axis with stuff below it\n", 546 | " ax = plt.subplot(gs[r,c], sharex=axs_dict[(nMVs-2,c)])\n", 547 | " plt.setp(ax.get_xticklabels(), visible=False)\n", 548 | "\n", 549 | " # add the labels\n", 550 | " if(r == nMVs-2):\n", 551 | " ax.set_xlabel(f'$\\Delta MV_{c+1}$')\n", 552 | " if(c == 0):\n", 553 | " ax.set_ylabel(f'$\\Delta MV_{r+2}$')\n", 554 | "\n", 555 | " axs_dict[(r,c)] = ax\n", 556 | "\n", 557 | "for indx, key in enumerate(axs_dict):\n", 558 | " ax = axs_dict[key]\n", 559 | " lines_handler_dict[key] = {}\n", 560 | "\n", 561 | " # figure out which MVs we are plotting, and which ones are constant\n", 562 | " x_MV = key[1] # column is x-axis\n", 563 | " y_MV = key[0]+1 # row is y-axis\n", 564 | " c_MV = [a for a in range(nMVs) if a != x_MV and a != y_MV]\n", 565 | "\n", 566 | " # plot the MV limits\n", 567 | " v_lo = ax.axvline(x=values_dict[f'MV{x_MV+1} Limits'][0], color='gray', lw=1, label=f'MV{x_MV+1} Lo')\n", 568 | " v_hi = ax.axvline(x=values_dict[f'MV{x_MV+1} Limits'][1], color='gray', lw=1, label=f'MV{x_MV+1} Hi')\n", 569 | " h_lo = ax.axhline(y=values_dict[f'MV{y_MV+1} Limits'][0], color='gray', lw=1, label=f'MV{y_MV+1} Lo')\n", 570 | " h_hi = ax.axhline(y=values_dict[f'MV{y_MV+1} Limits'][1], color='gray', lw=1, label=f'MV{y_MV+1} Hi')\n", 571 | "\n", 572 | " # labelLines([v_lo, v_hi], fontsize=4)\n", 573 | " # labelLine(h_hi, fontsize=4)\n", 574 | " \n", 575 | " # store the line handlers per ax in a dict\n", 576 | " lines_handler_dict[key]['v_lo'] = v_lo\n", 577 | " lines_handler_dict[key]['v_hi'] = v_hi\n", 578 | " lines_handler_dict[key]['h_lo'] = h_lo\n", 579 | " lines_handler_dict[key]['h_hi'] = h_hi\n", 580 | "\n", 581 | " # Recalculate shaded regions \n", 582 | " constraints = []\n", 583 | "\n", 584 | " # calculate CV lines\n", 585 | " lines_handler_dict[key]['CV_lines_lo'] = []\n", 586 | " lines_handler_dict[key]['CV_lines_hi'] = [] \n", 587 | " for i in range(nCVs):\n", 588 | " cv_lim_lo = values_dict[f'CV{i+1} Limits'][0]\n", 589 | " cv_lim_hi = values_dict[f'CV{i+1} Limits'][1]\n", 590 | "\n", 591 | " # for CV constraint lines\n", 592 | " y_lo = (cv_lim_lo - G[i][x_MV]*d - sum([G[i][cmv]*soln[cmv] for cmv in c_MV]))/G[i][y_MV]\n", 593 | " y_hi = (cv_lim_hi - G[i][x_MV]*d - sum([G[i][cmv]*soln[cmv] for cmv in c_MV]))/G[i][y_MV]\n", 594 | " line_lo, = ax.plot(d, y_lo, f'--{colors[i]}', label=f'$CV_{i+1}$ Lo');\n", 595 | " line_hi, = ax.plot(d, y_hi, f'-{colors[i]}', label=f'$CV_{i+1}$ Hi');\n", 596 | " lines_handler_dict[key]['CV_lines_lo'].append(line_lo)\n", 597 | " lines_handler_dict[key]['CV_lines_hi'].append(line_hi)\n", 598 | " \n", 599 | " # # search for valid xvals where the yvals are within plots limits\n", 600 | " # x_valid_lo = d[(y_lo < plot_limits) & (y_lo > -plot_limits)]\n", 601 | " # x_valid_hi = d[(y_hi < plot_limits) & (y_hi > -plot_limits)]\n", 602 | " # offset_lo = max(0,min(10, len(x_valid_lo)-3))\n", 603 | " # offset_hi = max(0,min(10, len(x_valid_hi)-3))\n", 604 | " # if len(x_valid_lo) > 0:\n", 605 | " # labelLine(line_lo, x_valid_lo[offset_lo], fontsize=5, zorder=2.5)\n", 606 | " # if len(x_valid_hi) > 0:\n", 607 | " # labelLine(line_hi, x_valid_hi[-offset_hi], fontsize=5, zorder=2.5)\n", 608 | " \n", 609 | " # for shading\n", 610 | " c_hi = G[i][x_MV]*x + G[i][y_MV]*y + sum([G[i][cmv]*soln[cmv] for cmv in c_MV]) <= cv_lim_hi\n", 611 | " c_lo = G[i][x_MV]*x + G[i][y_MV]*y + sum([G[i][cmv]*soln[cmv] for cmv in c_MV]) >= cv_lim_lo\n", 612 | " constraints.append(c_lo)\n", 613 | " constraints.append(c_hi)\n", 614 | " \n", 615 | "\n", 616 | " # the 4 MV limits for this ax\n", 617 | " constraints.append(x >= values_dict[f'MV{x_MV+1} Limits'][0])\n", 618 | " constraints.append(x <= values_dict[f'MV{x_MV+1} Limits'][1])\n", 619 | " constraints.append(y >= values_dict[f'MV{y_MV+1} Limits'][0])\n", 620 | " constraints.append(y <= values_dict[f'MV{y_MV+1} Limits'][1]) \n", 621 | "\n", 622 | " # the obj function \n", 623 | " y_obj = (V - sum([values_dict[f'MV{cmv+1} Cost']*soln[cmv] for cmv in c_MV]) - values_dict[f'MV{x_MV+1} Cost']*d)/values_dict[f'MV{y_MV+1} Cost']\n", 624 | " soln_func, = ax.plot(d, y_obj, '--k')\n", 625 | " soln_marker, = ax.plot(soln[x_MV], soln[y_MV], 'ok', zorder=10)\n", 626 | " soln_text = ax.text(soln[x_MV], soln[y_MV], '({:.2f},{:.2f})'.format(soln[x_MV], soln[y_MV]))\n", 627 | "\n", 628 | " lines_handler_dict[key]['soln_func'] = soln_func\n", 629 | " lines_handler_dict[key]['soln_marker'] = soln_marker\n", 630 | " lines_handler_dict[key]['soln_text'] = soln_text\n", 631 | " \n", 632 | " z_obj = (values_dict[f'MV{x_MV+1} Cost'] * xv) + (values_dict[f'MV{y_MV+1} Cost'] * yv) + sum([values_dict[f'MV{cmv+1} Cost']*soln[cmv] for cmv in c_MV])\n", 633 | " lines_handler_dict[key]['quiver'] = ax.quiver(xv, yv,-values_dict[f'MV{x_MV+1} Cost'],-values_dict[f'MV{y_MV+1} Cost'], z_obj, cmap='gray', headwidth=4, width=0.003, scale=40, alpha=0.5)\n", 634 | "\n", 635 | " ax.plot(0,0,'kx');\n", 636 | " ax.set_aspect('equal')\n", 637 | " im = ax.imshow(np.logical_and.reduce(constraints).astype(int), extent=(x.min(),x.max(),y.min(),y.max()), origin=\"lower\", cmap=\"Blues\", alpha=0.1)\n", 638 | " lines_handler_dict[key]['im'] = im\n", 639 | " \n", 640 | "####################################################\n", 641 | "# END OF INIT FUNC\n", 642 | "####################################################\n", 643 | "\n", 644 | "# register slides\n", 645 | "for widget in sliders:\n", 646 | " widget.observe(handle_slider_change, names='value')\n", 647 | "\n", 648 | "widgets.HBox([output, \n", 649 | " widgets.VBox([widgets.HTMLMath(\n", 650 | " value=r\"

Use the controls to interact with this linear program



\"), \n", 651 | " ui])])\n" 652 | ] 653 | }, 654 | { 655 | "cell_type": "code", 656 | "execution_count": null, 657 | "id": "c752d9b1-788f-4c3d-b3ae-4f98877f6d83", 658 | "metadata": {}, 659 | "outputs": [], 660 | "source": [] 661 | } 662 | ], 663 | "metadata": { 664 | "kernelspec": { 665 | "display_name": "Python 3 (ipykernel)", 666 | "language": "python", 667 | "name": "python3" 668 | }, 669 | "language_info": { 670 | "codemirror_mode": { 671 | "name": "ipython", 672 | "version": 3 673 | }, 674 | "file_extension": ".py", 675 | "mimetype": "text/x-python", 676 | "name": "python", 677 | "nbconvert_exporter": "python", 678 | "pygments_lexer": "ipython3", 679 | "version": "3.8.8" 680 | } 681 | }, 682 | "nbformat": 4, 683 | "nbformat_minor": 5 684 | } 685 | -------------------------------------------------------------------------------- /1.2 LP Problem - DMC - 3x3 and 4x4 (Generalized).py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # --- 3 | # jupyter: 4 | # jupytext: 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.4.0 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # # Linear Programming: DMC Formulation (3x3) 17 | # - Author: Siang Lim, Shams Elnawawi 18 | # - Last Updated: June 7th 2022 19 | # - Created March 2nd 2022 20 | # 21 | # ## References 22 | # - Morshedi, A. M., Cutler, C. R., & Skrovanek, T. A. (1985). Optimal solution of dynamic matrix control with linear programing techniques (LDMC). In 1985 American Control Conference (pp. 199-208). IEEE. 23 | # - Sorensen, R. C., & Cutler, C. R. (1998). LP integrates economics into dynamic matrix control. Hydrocarbon Processing, 77(9), 57-65. 24 | # - Ranade, S. M., & Torres, E. (2009). From dynamic mysterious control to dynamic manageable control. Hydrocarbon Processing, 88(3), 77-81. 25 | # - Godoy, J. L., Ferramosca, A., & González, A. H. (2017). Economic performance assessment and monitoring in LP-DMC type controller applications. Journal of Process Control, 57, 26-37. 26 | 27 | # + 28 | # # !pip install pulp 29 | 30 | # + 31 | # Just importing libraries and tweaking the plot settings 32 | import numpy as np 33 | import matplotlib.pyplot as plt 34 | # %matplotlib widget 35 | 36 | from matplotlib.ticker import StrMethodFormatter 37 | import matplotlib.gridspec as gridspec 38 | from labellines import labelLine, labelLines 39 | 40 | from ipywidgets.widgets.interaction import interact 41 | import ipywidgets.widgets as widgets 42 | from ipywidgets import Layout 43 | 44 | # Import PuLP modeler functions 45 | from pulp import * 46 | 47 | fsize = 8 48 | tsize = 12 49 | tdir = 'in' 50 | major = 5.0 51 | minor = 3.0 52 | lwidth = 0.8 53 | lhandle = 2.0 54 | plt.style.use('default') 55 | plt.rcParams['font.size'] = fsize 56 | plt.rcParams['legend.fontsize'] = tsize 57 | plt.rcParams['xtick.direction'] = tdir 58 | plt.rcParams['ytick.direction'] = tdir 59 | plt.rcParams['xtick.major.size'] = major 60 | plt.rcParams['xtick.minor.size'] = minor 61 | plt.rcParams['ytick.major.size'] = 5.0 62 | plt.rcParams['ytick.minor.size'] = 3.0 63 | plt.rcParams['axes.linewidth'] = lwidth 64 | plt.rcParams['legend.handlelength'] = lhandle 65 | # - 66 | 67 | # ## MV-CV equations 68 | # 69 | # $$ 70 | # G = 71 | # \begin{bmatrix} 72 | # -0.200 & -0.072 & 0.0774 \\ 73 | # 0.125 & -0.954 & 0.0063 \\ 74 | # 0.025 & 0.101 & −0.0143 75 | # \end{bmatrix} 76 | # $$ 77 | # 78 | # Using the gain matrix, the CV relationship can be written in terms of its MVs, starting with: 79 | # 80 | # $$ 81 | # \Delta \text{CV}_{1} = G_{11} \Delta \text{MV}_{1} + G_{12} \Delta \text{MV}_{2} + G_{13} \Delta \text{MV}_{3} \\ 82 | # \Delta \text{CV}_{2} = G_{21} \Delta \text{MV}_{1} + G_{22} \Delta \text{MV}_{2} + G_{23} \Delta \text{MV}_{3} \\ 83 | # \Delta \text{CV}_{3} = G_{31} \Delta \text{MV}_{1} + G_{32} \Delta \text{MV}_{2} + G_{33} \Delta \text{MV}_{3} 84 | # $$ 85 | # 86 | # We can impose upper and lower limits on the MVs: 87 | # 88 | # $$ 89 | # \text{MV}_{1, \text{Lo}} \leq \text{MV}_{1} \leq \text{MV}_{1, \text{Hi}}\\ 90 | # \text{MV}_{2, \text{Lo}} \leq \text{MV}_{2} \leq \text{MV}_{2, \text{Hi}}\\ 91 | # \text{MV}_{3, \text{Lo}} \leq \text{MV}_{3} \leq \text{MV}_{3, \text{Hi}}\\ 92 | # $$ 93 | # 94 | # As well as the CVs: 95 | # 96 | # $$ 97 | # \text{CV}_{1, \text{Lo}} \leq \text{CV}_{1} \leq \text{CV}_{1, \text{Hi}}\\ 98 | # \text{CV}_{2, \text{Lo}} \leq \text{CV}_{2} \leq \text{CV}_{2, \text{Hi}}\\ 99 | # \text{CV}_{3, \text{Lo}} \leq \text{CV}_{3} \leq \text{CV}_{3, \text{Hi}}\\ 100 | # $$ 101 | # 102 | # Since the CVs are related to the MVs by the gain matrix, we can substitute the equations to get CV limits in terms of MV movements: 103 | # 104 | # $$ 105 | # G_{11} \Delta \text{MV}_{1} + G_{12} \Delta \text{MV}_{2} + G_{13} \Delta \text{MV}_{3} \leq \Delta \text{CV}_{1, \text{Hi}}\\ 106 | # G_{11} \Delta \text{MV}_{1} + G_{12} \Delta \text{MV}_{2} + G_{13} \Delta \text{MV}_{3} \geq \Delta \text{CV}_{1, \text{Lo}}\\ 107 | # G_{21} \Delta \text{MV}_{1} + G_{22} \Delta \text{MV}_{2} + G_{23} \Delta \text{MV}_{3} \leq \Delta \text{CV}_{2, \text{Hi}}\\ 108 | # G_{21} \Delta \text{MV}_{1} + G_{22} \Delta \text{MV}_{2} + G_{23} \Delta \text{MV}_{3} \geq \Delta \text{CV}_{2, \text{Lo}}\\ 109 | # G_{31} \Delta \text{MV}_{1} + G_{32} \Delta \text{MV}_{2} + G_{33} \Delta \text{MV}_{3} \leq \Delta \text{CV}_{3, \text{Hi}}\\ 110 | # G_{31} \Delta \text{MV}_{1} + G_{22} \Delta \text{MV}_{2} + G_{33} \Delta \text{MV}_{3} \geq \Delta \text{CV}_{3, \text{Lo}}\\ 111 | # $$ 112 | # 113 | # Let's see what this looks like: 114 | 115 | # # Gains and CV Limits 116 | 117 | # + 118 | G11 = -0.200 119 | G12 = -0.072 120 | G13 = 0.0774 121 | G14 = 0.0574 122 | 123 | G21 = 0.125 124 | G22 = -0.954 125 | G23 = 0.0063 126 | G24 = 0.0374 127 | 128 | G31 = 0.025 129 | G32 = 0.101 130 | G33 = -0.0143 131 | G34 = -0.1143 132 | 133 | G41 = 0.120 134 | G42 = 0.150 135 | G43 = -0.1143 136 | G44 = -0.0943 137 | 138 | CV1Lo = -6 139 | CV1Hi = 6 140 | 141 | CV2Lo = -10 142 | CV2Hi = 10.5 143 | 144 | CV3Lo = -3 145 | CV3Hi = 3.5 146 | 147 | CV4Lo = -4.5 148 | CV4Hi = 4 149 | 150 | cost_MV1 = -1 151 | cost_MV2 = -1 152 | cost_MV3 = -1 153 | cost_MV4 = 1 154 | 155 | limits = 10 156 | plot_limits = 20 157 | 158 | MV1Lo = -limits 159 | MV1Hi = limits 160 | MV2Lo = -limits 161 | MV2Hi = limits 162 | MV3Lo = -limits 163 | MV3Hi = 15 164 | MV4Lo = -limits 165 | MV4Hi = -limits 166 | 167 | # + 168 | G = [(G11, G12, G13, G14), 169 | (G21, G22, G23, G24), 170 | (G31, G32, G33, G34), 171 | (G41, G42, G43, G44)] 172 | 173 | CV_values = [(CV1Lo, CV1Hi), 174 | (CV2Lo, CV2Hi), 175 | (CV3Lo, CV3Hi)] 176 | # (CV4Lo, CV4Hi)] 177 | CV_init_vals= [(-2.5,2.0), 178 | (-4,4.5), 179 | (-0.9,0.2)] 180 | # (-4,4.5)] 181 | MV_costs = [cost_MV1, 182 | cost_MV2, 183 | cost_MV3] 184 | # cost_MV4] 185 | MV_values = [(MV1Lo, MV1Hi), 186 | (MV2Lo, MV2Hi), 187 | (MV3Lo, MV3Hi)] 188 | # (MV4Lo, MV4Hi)] 189 | 190 | # (x,y,c) triplets of MVs, c for constant, i.e. 191 | # (MV1, MV2, MV3) for the first plot, 192 | # (MV1, MV3, MV2) for the second, 193 | # (MV2, MV3, MV1) for the third 194 | # plot_MV_indices = [(0,1), (0,2), (1,2)] 195 | 196 | nCVs = len(CV_values) 197 | nMVs = len(MV_values) 198 | 199 | # + 200 | CV_widgets = [] 201 | MV_widgets = [] 202 | cost_widgets = [] 203 | stepsize = 0.2 204 | 205 | # Make CV sliders 206 | for i in range(nCVs): 207 | widget = widgets.FloatRangeSlider( 208 | value=CV_init_vals[i], min=CV_values[i][0], max=CV_values[i][1], step=stepsize, 209 | description=f'CV{i+1} Limits', continuous_update=False) 210 | CV_widgets.append(widget) 211 | 212 | # Make MV sliders 213 | for i in range(nMVs): 214 | widget = widgets.FloatRangeSlider( 215 | value=MV_values[i], min=MV_values[i][0], max=MV_values[i][1], step=stepsize, 216 | description=f'MV{i+1} Limits', continuous_update=False) 217 | MV_widgets.append(widget) 218 | 219 | # Make cost sliders 220 | for i in range(nMVs): 221 | widget = widgets.FloatSlider( 222 | value=MV_costs[i], min=-3, max=3, step=stepsize/2, # finer step for costs 223 | description=f'MV{i+1} Cost', continuous_update=True) 224 | cost_widgets.append(widget) 225 | 226 | sliders = CV_widgets + cost_widgets + MV_widgets 227 | values = [slider.value for slider in sliders] 228 | values_dict = {} 229 | for slider in sliders: 230 | values_dict[slider.description] = slider.value 231 | print(values_dict) 232 | 233 | # + 234 | # display ui 235 | ncols = 3 236 | 237 | nrows = nCVs // ncols + 1 238 | CV_row = [] 239 | for i in range(nrows): 240 | CV_row.append(widgets.VBox(CV_widgets[(ncols)*i:(ncols)*i+ncols])) 241 | 242 | nrows = nMVs // ncols + 1 243 | MV_row = [] 244 | cost_row = [] 245 | for i in range(nrows): 246 | MV_row.append(widgets.VBox(MV_widgets[(ncols)*i:(ncols)*i+ncols])) 247 | cost_row.append(widgets.VBox(cost_widgets[(ncols)*i:(ncols)*i+ncols])) 248 | 249 | ui = widgets.VBox([widgets.VBox(CV_row), 250 | widgets.VBox(MV_row), 251 | widgets.VBox(cost_row)]) 252 | 253 | 254 | # + 255 | # Solve the LP 256 | def run_lp(values_dict): 257 | prob = LpProblem("DMC_problem",LpMinimize) 258 | 259 | # How many MVs 260 | MVs = [] 261 | for i in range(nMVs): 262 | MVs.append(LpVariable(f"MV{i+1}",-limits)) 263 | 264 | # the objective function 265 | obj = 0 266 | for indx, MV in enumerate(MVs): 267 | obj += values_dict[f"MV{indx+1} Cost"]*MV 268 | 269 | prob += obj, "Cost function of MVs" 270 | 271 | # constraint formulation in terms of MV1 and MV2 272 | CV_contraint_lo = [] 273 | CV_contraint_hi = [] 274 | 275 | for i in range(nCVs): 276 | c = 0 277 | for indx, MV in enumerate(MVs): 278 | c += G[i][indx]*MV 279 | prob += c <= values_dict[f'CV{i+1} Limits'][1], f'CV{i+1} High Limit' 280 | prob += c >= values_dict[f'CV{i+1} Limits'][0], f'CV{i+1} Low Limit' 281 | 282 | # prob += G[i][0]*MV1 + G[i][1]*MV2 <= values_dict[f'CV{i+1} Limits'][1], f'CV{i+1} High Limit' 283 | # prob += G[i][0]*MV1 + G[i][1]*MV2 >= values_dict[f'CV{i+1} Limits'][0], f'CV{i+1} Low Limit' 284 | 285 | for indx, MV in enumerate(MVs): 286 | prob += MV <= values_dict[f'MV{indx+1} Limits'][1], f'MV{indx+1} High Limit' 287 | prob += MV >= values_dict[f'MV{indx+1} Limits'][0], f'MV{indx+1} Low Limit' 288 | 289 | if (prob.solve(PULP_CBC_CMD(msg=0)) == 1): 290 | # print([v.varValue for v in prob.variables()]) 291 | # print([v for v in prob.variables()]) 292 | return [v.varValue for v in prob.variables()], value(prob.objective) 293 | else: 294 | print("NOT SOLVED - Infeasibility!") 295 | return np.zeros(nMVs), 0 296 | 297 | 298 | # + 299 | d = np.linspace(-plot_limits, plot_limits, 1000) 300 | x,y = np.meshgrid(d,d) 301 | 302 | # Recalculate shaded regions 303 | constraints = [] 304 | for i in range(nCVs): 305 | c_hi = G[i][0]*x + G[i][1]*y <= values_dict[f'CV{i+1} Limits'][1] 306 | c_lo = G[i][0]*x + G[i][1]*y >= values_dict[f'CV{i+1} Limits'][0] 307 | constraints.append(c_lo) 308 | constraints.append(c_hi) 309 | 310 | # the MVs 311 | for indx, var in enumerate([x,y]): 312 | c_hi = var <= values_dict[f'MV{indx+1} Limits'][1] 313 | c_lo = var >= values_dict[f'MV{indx+1} Limits'][0] 314 | constraints.append(c_lo) 315 | constraints.append(c_hi) 316 | 317 | 318 | # - 319 | 320 | def handle_slider_change(change): 321 | ## grab slider vals 322 | values_dict = {} 323 | for slider in sliders: 324 | values_dict[slider.description] = slider.value 325 | 326 | # Find the LP soln 327 | soln, V = run_lp(values_dict) 328 | 329 | for indx, key in enumerate(axs_dict): 330 | ax = axs_dict[key] 331 | 332 | # remove previous line labels (TODO could be more efficient, modify labelLine library to return handles) 333 | # for txt in ax.texts: 334 | # txt.remove() 335 | 336 | # figure out which MVs we are plotting, and which ones are constant 337 | x_MV = key[1] # column is x-axis 338 | y_MV = key[0]+1 # row is y-axis 339 | c_MV = [a for a in range(nMVs) if a != x_MV and a != y_MV] 340 | 341 | # Recalculate shaded regions 342 | constraints = [] 343 | 344 | # the obj func 345 | y_obj = (V - sum([values_dict[f'MV{cmv+1} Cost']*soln[cmv] for cmv in c_MV]) - values_dict[f'MV{x_MV+1} Cost']*d)/values_dict[f'MV{y_MV+1} Cost'] 346 | 347 | # Update CV constraint lines 348 | for i in range(nCVs): 349 | cv_lim_lo = values_dict[f'CV{i+1} Limits'][0] 350 | cv_lim_hi = values_dict[f'CV{i+1} Limits'][1] 351 | 352 | # for CV constraint lines 353 | y_lo = (cv_lim_lo - G[i][x_MV]*d - sum([G[i][cmv]*soln[cmv] for cmv in c_MV]))/G[i][y_MV] 354 | y_hi = (cv_lim_hi - G[i][x_MV]*d - sum([G[i][cmv]*soln[cmv] for cmv in c_MV]))/G[i][y_MV] 355 | lines_handler_dict[key]['CV_lines_lo'][i].set_data(d, y_lo) 356 | lines_handler_dict[key]['CV_lines_hi'][i].set_data(d, y_hi) 357 | 358 | # for shading 359 | c_hi = G[i][x_MV]*x + G[i][y_MV]*y + sum([G[i][cmv]*soln[cmv] for cmv in c_MV]) <= cv_lim_hi 360 | c_lo = G[i][x_MV]*x + G[i][y_MV]*y + sum([G[i][cmv]*soln[cmv] for cmv in c_MV]) >= cv_lim_lo 361 | constraints.append(c_lo) 362 | constraints.append(c_hi) 363 | 364 | # search for valid xvals where the yvals are within plots limits 365 | # x_valid_lo = d[(y_lo < plot_limits) & (y_lo > -plot_limits)] 366 | # x_valid_hi = d[(y_hi < plot_limits) & (y_hi > -plot_limits)] 367 | # offset_lo = max(0,min(10, len(x_valid_lo)-3)) 368 | # offset_hi = max(0,min(10, len(x_valid_hi)-3)) 369 | # if len(x_valid_lo) > 0: 370 | # txt_cv_lo = labelLine(line_lo, x_valid_lo[offset_lo], fontsize=5, zorder=2.5) 371 | # if len(x_valid_hi) > 0: 372 | # txt_cv_hi = labelLine(line_hi, x_valid_hi[-offset_hi], fontsize=5, zorder=2.5) 373 | 374 | # Update MV constraint lines 375 | lines_handler_dict[key]['v_lo'].set_data([values_dict[f'MV{x_MV+1} Limits'][0],values_dict[f'MV{x_MV+1} Limits'][0]], [-limits, limits]) 376 | lines_handler_dict[key]['v_hi'].set_data([values_dict[f'MV{x_MV+1} Limits'][1],values_dict[f'MV{x_MV+1} Limits'][1]], [-limits, limits]) 377 | lines_handler_dict[key]['h_lo'].set_data([-limits, limits], [values_dict[f'MV{y_MV+1} Limits'][0],values_dict[f'MV{y_MV+1} Limits'][0]]) 378 | lines_handler_dict[key]['h_hi'].set_data([-limits, limits], [values_dict[f'MV{y_MV+1} Limits'][1],values_dict[f'MV{y_MV+1} Limits'][1]]) 379 | 380 | # the 4 MV limits for this ax 381 | constraints.append(x >= values_dict[f'MV{x_MV+1} Limits'][0]) 382 | constraints.append(x <= values_dict[f'MV{x_MV+1} Limits'][1]) 383 | constraints.append(y >= values_dict[f'MV{y_MV+1} Limits'][0]) 384 | constraints.append(y <= values_dict[f'MV{y_MV+1} Limits'][1]) 385 | 386 | # Shade the right regions 387 | lines_handler_dict[key]['im'].set_data((np.logical_and.reduce(constraints)).astype(float)) 388 | 389 | # the quiver/vector field 390 | z_obj = (values_dict[f'MV{x_MV+1} Cost'] * xv) + (values_dict[f'MV{y_MV+1} Cost'] * yv) + sum([values_dict[f'MV{cmv+1} Cost']*soln[cmv] for cmv in c_MV]) 391 | lines_handler_dict[key]['quiver'].set_UVC(-values_dict[f'MV{x_MV+1} Cost'],-values_dict[f'MV{y_MV+1} Cost'], z_obj) 392 | 393 | # the soln 394 | lines_handler_dict[key]['soln_marker'].set_data(soln[x_MV], soln[y_MV]); 395 | lines_handler_dict[key]['soln_text'].set_position((soln[x_MV], soln[y_MV])); 396 | lines_handler_dict[key]['soln_text'].set_text("({:.1f}, {:.1f})".format(soln[x_MV], soln[y_MV])) 397 | lines_handler_dict[key]['soln_func'].set_data(d, y_obj); 398 | 399 | fig.canvas.draw() 400 | 401 | 402 | # + 403 | # Initialize plots 404 | d = np.linspace(-plot_limits, plot_limits, 100) 405 | x,y = np.meshgrid(d,d) 406 | 407 | gs = gridspec.GridSpec(nMVs-1, nMVs-1) 408 | gs.update(wspace=0.05, hspace=0.05) 409 | gs_indices = [(row, col) for row in range(nMVs-2,-1,-1) for col in range(row+1)] 410 | axs_dict = {} 411 | 412 | # mesh for vector field 413 | dvec = np.linspace(-plot_limits + (0.1*plot_limits), plot_limits - (0.1*plot_limits), 12) 414 | xv, yv = np.meshgrid(dvec, dvec) 415 | 416 | # init soln 417 | soln, V = run_lp(values_dict) 418 | 419 | # initialize line handlers 420 | CV_lines_lo = [] 421 | CV_lines_hi = [] 422 | MV_lines_lo = [] 423 | MV_lines_hi = [] 424 | lines_handler_dict = {} 425 | 426 | colors = ['r', 'b', 'y', 'g'] # TODO generalize this using a cmap instead of defining manual colors??! 427 | 428 | # plot as widget 429 | output = widgets.Output() 430 | with output: 431 | fig = plt.figure(figsize=(6,6), dpi=100, facecolor='white') 432 | plt.show() 433 | 434 | for r,c in gs_indices: 435 | # build the shared axes correctly and handle the tick labels 436 | if (r == nMVs-2 and c == 0): # no shared axis for the ax, bottom left corner 437 | ax = plt.subplot(gs[r,c]) 438 | elif(c != 0): # all non-first columns share a y-axis with stuff to the left of it 439 | ax = plt.subplot(gs[r,c], sharey=axs_dict[(r,0)]) 440 | plt.setp(ax.get_yticklabels(), visible=False) 441 | elif(r != nMVs-2): # all non-first rows share a x-axis with stuff below it 442 | ax = plt.subplot(gs[r,c], sharex=axs_dict[(nMVs-2,c)]) 443 | plt.setp(ax.get_xticklabels(), visible=False) 444 | 445 | # add the labels 446 | if(r == nMVs-2): 447 | ax.set_xlabel(f'$\Delta MV_{c+1}$') 448 | if(c == 0): 449 | ax.set_ylabel(f'$\Delta MV_{r+2}$') 450 | 451 | axs_dict[(r,c)] = ax 452 | 453 | for indx, key in enumerate(axs_dict): 454 | ax = axs_dict[key] 455 | lines_handler_dict[key] = {} 456 | 457 | # figure out which MVs we are plotting, and which ones are constant 458 | x_MV = key[1] # column is x-axis 459 | y_MV = key[0]+1 # row is y-axis 460 | c_MV = [a for a in range(nMVs) if a != x_MV and a != y_MV] 461 | 462 | # plot the MV limits 463 | v_lo = ax.axvline(x=values_dict[f'MV{x_MV+1} Limits'][0], color='gray', lw=1, label=f'MV{x_MV+1} Lo') 464 | v_hi = ax.axvline(x=values_dict[f'MV{x_MV+1} Limits'][1], color='gray', lw=1, label=f'MV{x_MV+1} Hi') 465 | h_lo = ax.axhline(y=values_dict[f'MV{y_MV+1} Limits'][0], color='gray', lw=1, label=f'MV{y_MV+1} Lo') 466 | h_hi = ax.axhline(y=values_dict[f'MV{y_MV+1} Limits'][1], color='gray', lw=1, label=f'MV{y_MV+1} Hi') 467 | 468 | # labelLines([v_lo, v_hi], fontsize=4) 469 | # labelLine(h_hi, fontsize=4) 470 | 471 | # store the line handlers per ax in a dict 472 | lines_handler_dict[key]['v_lo'] = v_lo 473 | lines_handler_dict[key]['v_hi'] = v_hi 474 | lines_handler_dict[key]['h_lo'] = h_lo 475 | lines_handler_dict[key]['h_hi'] = h_hi 476 | 477 | # Recalculate shaded regions 478 | constraints = [] 479 | 480 | # calculate CV lines 481 | lines_handler_dict[key]['CV_lines_lo'] = [] 482 | lines_handler_dict[key]['CV_lines_hi'] = [] 483 | for i in range(nCVs): 484 | cv_lim_lo = values_dict[f'CV{i+1} Limits'][0] 485 | cv_lim_hi = values_dict[f'CV{i+1} Limits'][1] 486 | 487 | # for CV constraint lines 488 | y_lo = (cv_lim_lo - G[i][x_MV]*d - sum([G[i][cmv]*soln[cmv] for cmv in c_MV]))/G[i][y_MV] 489 | y_hi = (cv_lim_hi - G[i][x_MV]*d - sum([G[i][cmv]*soln[cmv] for cmv in c_MV]))/G[i][y_MV] 490 | line_lo, = ax.plot(d, y_lo, f'--{colors[i]}', label=f'$CV_{i+1}$ Lo'); 491 | line_hi, = ax.plot(d, y_hi, f'-{colors[i]}', label=f'$CV_{i+1}$ Hi'); 492 | lines_handler_dict[key]['CV_lines_lo'].append(line_lo) 493 | lines_handler_dict[key]['CV_lines_hi'].append(line_hi) 494 | 495 | # # search for valid xvals where the yvals are within plots limits 496 | # x_valid_lo = d[(y_lo < plot_limits) & (y_lo > -plot_limits)] 497 | # x_valid_hi = d[(y_hi < plot_limits) & (y_hi > -plot_limits)] 498 | # offset_lo = max(0,min(10, len(x_valid_lo)-3)) 499 | # offset_hi = max(0,min(10, len(x_valid_hi)-3)) 500 | # if len(x_valid_lo) > 0: 501 | # labelLine(line_lo, x_valid_lo[offset_lo], fontsize=5, zorder=2.5) 502 | # if len(x_valid_hi) > 0: 503 | # labelLine(line_hi, x_valid_hi[-offset_hi], fontsize=5, zorder=2.5) 504 | 505 | # for shading 506 | c_hi = G[i][x_MV]*x + G[i][y_MV]*y + sum([G[i][cmv]*soln[cmv] for cmv in c_MV]) <= cv_lim_hi 507 | c_lo = G[i][x_MV]*x + G[i][y_MV]*y + sum([G[i][cmv]*soln[cmv] for cmv in c_MV]) >= cv_lim_lo 508 | constraints.append(c_lo) 509 | constraints.append(c_hi) 510 | 511 | 512 | # the 4 MV limits for this ax 513 | constraints.append(x >= values_dict[f'MV{x_MV+1} Limits'][0]) 514 | constraints.append(x <= values_dict[f'MV{x_MV+1} Limits'][1]) 515 | constraints.append(y >= values_dict[f'MV{y_MV+1} Limits'][0]) 516 | constraints.append(y <= values_dict[f'MV{y_MV+1} Limits'][1]) 517 | 518 | # the obj function 519 | y_obj = (V - sum([values_dict[f'MV{cmv+1} Cost']*soln[cmv] for cmv in c_MV]) - values_dict[f'MV{x_MV+1} Cost']*d)/values_dict[f'MV{y_MV+1} Cost'] 520 | soln_func, = ax.plot(d, y_obj, '--k') 521 | soln_marker, = ax.plot(soln[x_MV], soln[y_MV], 'ok', zorder=10) 522 | soln_text = ax.text(soln[x_MV], soln[y_MV], '({:.2f},{:.2f})'.format(soln[x_MV], soln[y_MV])) 523 | 524 | lines_handler_dict[key]['soln_func'] = soln_func 525 | lines_handler_dict[key]['soln_marker'] = soln_marker 526 | lines_handler_dict[key]['soln_text'] = soln_text 527 | 528 | z_obj = (values_dict[f'MV{x_MV+1} Cost'] * xv) + (values_dict[f'MV{y_MV+1} Cost'] * yv) + sum([values_dict[f'MV{cmv+1} Cost']*soln[cmv] for cmv in c_MV]) 529 | lines_handler_dict[key]['quiver'] = ax.quiver(xv, yv,-values_dict[f'MV{x_MV+1} Cost'],-values_dict[f'MV{y_MV+1} Cost'], z_obj, cmap='gray', headwidth=4, width=0.003, scale=40, alpha=0.5) 530 | 531 | ax.plot(0,0,'kx'); 532 | ax.set_aspect('equal') 533 | im = ax.imshow(np.logical_and.reduce(constraints).astype(int), extent=(x.min(),x.max(),y.min(),y.max()), origin="lower", cmap="Blues", alpha=0.1) 534 | lines_handler_dict[key]['im'] = im 535 | 536 | #################################################### 537 | # END OF INIT FUNC 538 | #################################################### 539 | 540 | # register slides 541 | for widget in sliders: 542 | widget.observe(handle_slider_change, names='value') 543 | 544 | widgets.HBox([output, 545 | widgets.VBox([widgets.HTMLMath( 546 | value=r"

Use the controls to interact with this linear program



"), 547 | ui])]) 548 | 549 | # - 550 | 551 | 552 | -------------------------------------------------------------------------------- /DMC.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Implementing Dynamic Matrix Control in Python\n", 8 | "\n", 9 | "#### Author: Siang Lim, February 2020\n", 10 | "\n", 11 | "** work in progress **\n", 12 | "\n", 13 | "- Contents of this notebook are derived from DMC literature:\n", 14 | " - Hokanson, D. A., & Gerstle, J. G. (1992). **Dynamic Matrix Control Multivariable Controllers.** Practical Distillation Control, 248–271.\n", 15 | " - Sorensen, R. C., & Cutler, C. R. (1998). **LP integrates economics into dynamic matrix control.** Hydrocarbon processing (International ed.), 77(9), 57-65.\n", 16 | " - Morshedi, A. M., Cutler, C. R., & Skrovanek, T. A. (1985, June). **Optimal solution of dynamic matrix control with linear programing techniques (LDMC).** In 1985 American Control Conference (pp. 199-208). IEEE." 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": { 23 | "collapsed": true 24 | }, 25 | "outputs": [], 26 | "source": [ 27 | "import numpy as np\n", 28 | "import matplotlib.pyplot as plt" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "# Definitions\n", 36 | "\n", 37 | "#### Independent Variables\n", 38 | "\n", 39 | "- MV (Manipulated Variables)\n", 40 | " - Variables that we can move directly, valves, feed rate etc.\n", 41 | "\n", 42 | "- FF (Feedforward Variables)\n", 43 | " - Disturbances that we can't control directly - ambient temperature.\n", 44 | "\n", 45 | "#### Dependent Variables\n", 46 | "- Variables that change due to a change in the independent variables, e.g. temperatures, pressures\n" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "## Step Response Convolution Model" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "The sequence $a_j$, where $j = 0,1,2,3 \\dots$ is the step response convolution model, which is the basis for DMC.\n", 61 | "\n", 62 | "Remembering that everything so far is in deviation variables, at is no more than a series of numbers based on\n", 63 | "a set length (time to steady state) and a set sampling interval.\n", 64 | "\n", 65 | "There is no set form of the model, other than that steady state is reached by the last interval. In fact, the last\n", 66 | "value $a_t$ is the process steady-state gain. " 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 2, 72 | "metadata": { 73 | "collapsed": true 74 | }, 75 | "outputs": [], 76 | "source": [ 77 | "a = -np.array([0.5,1.0,1.5,1.8,1.9,1.95,1.98,1.99,1.999,2])" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 3, 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "data": { 87 | "text/plain": [ 88 | "Text(0,0.5,'Response')" 89 | ] 90 | }, 91 | "execution_count": 3, 92 | "metadata": {}, 93 | "output_type": "execute_result" 94 | }, 95 | { 96 | "data": { 97 | "image/png": "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\nTmRmnciFw8pjgw3SMN2amjRM9/XXcycys07iwmHls8kmcNttMH9+anl88EHuRGbWCVw4rLx23BGu\nugoeeggOOwy64T01s57GS8da+R1wADz/PJxyCmy2mYfqmnVxLhxWGSedBHPmwOTJqXg0XdPczLoU\nFw6rDAmmTYO5c9OT5cOGwRe+kDuVmbWD73FY5fTtCzffnOa2Gjs2dV+ZWZfjwmGVNWgQzChMDrDP\nPvDGG3nzmFnJXDis8jbdFH73O3j55bSOx7JluROZWQlcOCyPXXZJS87+93+nJWg9TNesy/DNccvn\nO99J9zkmTYKRI9NwXTOrei4clteECal4TJiQurAOPDB3IjNbC3dVWV4SXHYZ7LorHHoo/M//5E5k\nZmvhwmH5rbMO3HJLerZjzBh44YXcicysDS4cVh0+/vE0m+7KlWmY7j//mTuRmbXChcOqx8iRcOut\n8OKL8I1veJiuWZXKtXTs/pKekrRSUqsrTkn6j8J5syRdJ6lfJXNaBrvuCldcAffdB4cf7mG6ZlUo\nV4tjFjAOeKC1EyRtCBwF1EfEVkBv4NuViWdZHXQQTJyYnvMYNAh69UrTlEyfnjuZmZFpOG5EzAaQ\ntLZTa4D+kpYDtcDCMkezarH55tC7N7z5ZtqfOxfGj0+vPbOuWVZVe48jIl4BzgPmAYuAtyLiT3lT\nWcWcfDJ8+GHzY0uWwIkn5sljZv9StsIh6e7CvYk1tzFF/vwGwBhgY2AIsK6kg9o4f7ykRkmNixcv\n7pw/hOUzb17Lx+fPhx//GGbO9P0Ps0zKVjgiYo+I2KqF7bYi32IP4KWIWBwRy4FbgJ3auN60iKiP\niPq6urrO+CNYTsOGtXx83XXTUrT19TB6NFx0kYfumlVY1XZVkbqoPi+pVulmyO7A7MyZrFKmToXa\n2ubHamvh0kth4UL45S/TPZAjj4TBg9MN9fvuS8+BmFlZ5RqOO1bSAmBH4A5JdxaOD5E0AyAiHgZu\nAh4FnixknZYjr2XQ0JBWDBw+PE1LMnx42m9ogPXXX91d9eijcNhh8Ic/wG67pZvqZ56ZiouZlYWi\nG/YT19fXR2NjY+4YVklLl6bVBS+/PE3V3rt3egL9sMPga1+DGs/nadYWSTMjotXn6pqq5q4qs+L1\n75+6q+6/H557Do47Dv76V9hvv3S/5KSTvFStWSdx4bDuZ+TI1F01b15aabC+Hs4+Ox3fbbf0IOHS\n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\n", 98 | "text/plain": [ 99 | "
" 100 | ] 101 | }, 102 | "metadata": {}, 103 | "output_type": "display_data" 104 | } 105 | ], 106 | "source": [ 107 | "%matplotlib inline\n", 108 | "plt.plot(a, '-or')\n", 109 | "plt.xlabel(\"Time (s)\")\n", 110 | "plt.ylabel(\"Response\")" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "## Inputs" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "For a lined out process with a single step in $u$ at time 0:\n", 125 | "\n", 126 | "$$ \\Delta u_0 = u_0 - u_{-1} $$\n", 127 | "\n", 128 | "then\n", 129 | "\n", 130 | "$$ y_1 - y_0 = a_1 \\Delta u_0 $$\n", 131 | "$$ y_2 - y_0 = a_2 \\Delta u_0 $$\n", 132 | "$$ y_3 - y_0 = a_3 \\Delta u_0 $$\n", 133 | "$$ y_4 - y_0 = a_4 \\Delta u_0 $$\n", 134 | "$$ \\vdots $$\n", 135 | "$$ y_t - y_0 = a_t \\Delta u_0 $$\n", 136 | "\n", 137 | "This is essentially the definition of the step response convolution model described before. What is interesting about this is that we can calculate what y will be at each interval for successive moves in u.\n", 138 | "\n", 139 | "For example purposes only, let us examine a series of three moves \"into the future\": one now, one at the next time interval, and one two time intervals into the future: \n", 140 | "\n", 141 | "$$ \\Delta u_0 = u_0 - u_{-1} $$\n", 142 | "$$ \\Delta u_1 = u_1 - u_{0} $$\n", 143 | "$$ \\Delta u_2 = u_2 - u_{1} $$" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": 4, 149 | "metadata": {}, 150 | "outputs": [ 151 | { 152 | "name": "stdout", 153 | "output_type": "stream", 154 | "text": [ 155 | "[[ 0]\n", 156 | " [ 1]\n", 157 | " [ 0]\n", 158 | " [ 0]\n", 159 | " [-1]]\n" 160 | ] 161 | } 162 | ], 163 | "source": [ 164 | "U = np.atleast_2d(np.array([0,1,0,0,-1])).T\n", 165 | "print(U)" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 5, 171 | "metadata": {}, 172 | "outputs": [ 173 | { 174 | "data": { 175 | "text/plain": [ 176 | "[]" 177 | ] 178 | }, 179 | "execution_count": 5, 180 | "metadata": {}, 181 | "output_type": "execute_result" 182 | }, 183 | { 184 | "data": { 185 | "image/png": "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\nAABJRU5ErkJggg==\n", 186 | "text/plain": [ 187 | "
" 188 | ] 189 | }, 190 | "metadata": {}, 191 | "output_type": "display_data" 192 | } 193 | ], 194 | "source": [ 195 | "I_obs = np.pad(np.cumsum(U),(0,8),'constant')\n", 196 | "plt.step(I_obs, '-bx', where=\"post\")" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "then\n", 204 | "\n", 205 | "$$ \n", 206 | "\\begin{align}\n", 207 | "y_1 - y_0 & = a_1 \\Delta u_0 \\\\\n", 208 | "y_2 - y_0 & = a_2 \\Delta u_0 + a_1 \\Delta u_1 \\\\\n", 209 | "y_3 - y_0 & = a_3 \\Delta u_0 + a_2 \\Delta u_1 + + a_1 \\Delta u_2 \\\\\n", 210 | "y_4 - y_0 & = a_4 \\Delta u_0 + a_3 \\Delta u_1 + + a_2 \\Delta u_2 \\\\\n", 211 | "\\vdots & \\\\\n", 212 | "y_t - y_0 & = a_t \\Delta u_0 + a_{t-1} \\Delta u_1 + + a_{t-2} \\Delta u_2 \\\\\n", 213 | "\\end{align}\n", 214 | "$$\n" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": 6, 220 | "metadata": {}, 221 | "outputs": [ 222 | { 223 | "name": "stdout", 224 | "output_type": "stream", 225 | "text": [ 226 | "[[-0.5 0. 0. 0. 0. ]\n", 227 | " [-1. -0.5 0. 0. 0. ]\n", 228 | " [-1.5 -1. -0.5 0. 0. ]\n", 229 | " [-1.8 -1.5 -1. -0.5 0. ]\n", 230 | " [-1.9 -1.8 -1.5 -1. -0.5 ]\n", 231 | " [-1.95 -1.9 -1.8 -1.5 -1. ]\n", 232 | " [-1.98 -1.95 -1.9 -1.8 -1.5 ]\n", 233 | " [-1.99 -1.98 -1.95 -1.9 -1.8 ]\n", 234 | " [-1.999 -1.99 -1.98 -1.95 -1.9 ]\n", 235 | " [-2. -1.999 -1.99 -1.98 -1.95 ]]\n" 236 | ] 237 | } 238 | ], 239 | "source": [ 240 | "# Build dynamic matrix\n", 241 | "t = a.shape[0]\n", 242 | "u = U.shape[0]\n", 243 | "\n", 244 | "A = np.tile(a, (u,1)).T\n", 245 | "row_indices = np.atleast_2d(np.arange(t)).T - np.arange(u)\n", 246 | "col_indices = np.arange(u)\n", 247 | "A = np.tril(A[row_indices,col_indices])\n", 248 | "print(A)" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 7, 254 | "metadata": { 255 | "collapsed": true 256 | }, 257 | "outputs": [], 258 | "source": [ 259 | "Y = np.multiply(A,U.T)\n", 260 | "Y_obs = np.matmul(A,U)\n", 261 | "U_obs = np.pad(np.cumsum(U),(0,a.shape[0]-U.shape[0]),'constant')" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 8, 267 | "metadata": {}, 268 | "outputs": [ 269 | { 270 | "data": { 271 | "text/plain": [ 272 | "array([[-0. , 0. , 0. , 0. , -0. ],\n", 273 | " [-0. , -0.5 , 0. , 0. , -0. ],\n", 274 | " [-0. , -1. , -0. , 0. , -0. ],\n", 275 | " [-0. , -1.5 , -0. , -0. , -0. ],\n", 276 | " [-0. , -1.8 , -0. , -0. , 0.5 ],\n", 277 | " [-0. , -1.9 , -0. , -0. , 1. ],\n", 278 | " [-0. , -1.95 , -0. , -0. , 1.5 ],\n", 279 | " [-0. , -1.98 , -0. , -0. , 1.8 ],\n", 280 | " [-0. , -1.99 , -0. , -0. , 1.9 ],\n", 281 | " [-0. , -1.999, -0. , -0. , 1.95 ]])" 282 | ] 283 | }, 284 | "execution_count": 8, 285 | "metadata": {}, 286 | "output_type": "execute_result" 287 | } 288 | ], 289 | "source": [ 290 | "Y" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 9, 296 | "metadata": {}, 297 | "outputs": [ 298 | { 299 | "data": { 300 | "text/plain": [ 301 | "(array([1, 2, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9]),\n", 302 | " array([1, 1, 1, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4]))" 303 | ] 304 | }, 305 | "execution_count": 9, 306 | "metadata": {}, 307 | "output_type": "execute_result" 308 | } 309 | ], 310 | "source": [ 311 | "np.nonzero(Y)" 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": 10, 317 | "metadata": {}, 318 | "outputs": [ 319 | { 320 | "data": { 321 | "text/plain": [ 322 | "array([1, 4])" 323 | ] 324 | }, 325 | "execution_count": 10, 326 | "metadata": {}, 327 | "output_type": "execute_result" 328 | } 329 | ], 330 | "source": [ 331 | "np.nonzero(np.any(Y != 0, axis=0))[0]" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": 11, 337 | "metadata": {}, 338 | "outputs": [ 339 | { 340 | "data": { 341 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAecAAAFpCAYAAACmt+D8AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMi4yLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvhp/UCwAAIABJREFUeJzt3Xl8VNX9//HXJxthC0lI2BPCDhEQ\nMKIigigim4BVrIq4dKG1Umv1W+vS/dvW2m/la/vFLrSuP3EBVESk4oZ1QZQAyioYQCCAsq8Bsp3f\nH2dCEggQZJK5Sd7Px2MeM3PvnTufTCDvOefee4455xAREZHgiIp0ASIiIlKewllERCRgFM4iIiIB\no3AWEREJGIWziIhIwCicRUREAkbhLCIiEjAKZxERkYBROIuIiASMwllERCRgYiL1xikpKS4jIyNS\nby8iIlKtFi1atMM5l1qZbSMWzhkZGWRnZ0fq7UVERKqVmW2o7Lan7NY2s8fMbJuZLT/BejOzv5hZ\njpktNbM+p1OsiIiIlFeZY85PAENPsn4Y0Cl0mwD87czLqln++EeYN6/8snnz/PLqNHw4TJpUftmk\nSX55XawjKL8XEZHTdcpwds69C+w6ySajgaectwBINLOW4SqwJjj3XLjmmtIgmDfPPz/33OqtY/Bg\n+K//Kg3GSZP888GD62YdQfm9iIicLqvMfM5mlgHMds51r2DdbOAPzrn3Q8/fAn7qnDvpAeWsrCxX\nm445z5sHo0ZBcjJs3QrdukFSUvXXsWkTrFsHCQmwbx+0bw9paXW3jt27YdUqGDIEPvoIpk2DQYOq\nvw4RETNb5JzLqsy24biUyipYVmHim9kEM8s2s+zt27eH4a2DY9AgH8wbN0LLlpEJZvABWBKICQmR\nCcQg1ZGUBFFR8OqrcOutCmYRqRnCcbZ2LlD2T28bYEtFGzrnpgBTwLecw/DegTFvnm8xp6dDXh78\n8peRCYJJk+Ddd+Gii+D9931r/s47624d8+bB5Zf738vf/uZ/JwpoEQm6cLScZwE3hs7aPh/Y65zb\nGob91hglxzK7dYN27XzXadljndWl5Njun/7kg/FPfyp/7Leu1RGU34uIyOmqzKVUzwIfAl3MLNfM\nvm1m3zez74c2mQOsA3KAfwI/qLJqA2rhQv+Hv6Qre9Ag/3zhwuqt4803fRCWtFDvvNM/f/PNullH\nUH4vIiKnq1InhFWF2nZCGMDFF/v7d96JZBVyLP1eRCQIqvuEMBEREQkjhbOIiEjAKJxFREQCRuEs\nIiISMApnERGRgFE4i4iIBIzCWUREJGAUziIiIgGjcBYREQkYhbOIiEjAKJxFREQCRuEsIiISMApn\nERGRgFE4i4iIBIzCWUREJGAUziIiIgGjcBYREQkYhbOIiEjAVCqczWyoma02sxwzu6eC9elmNs/M\nlpjZUjMbHv5SRURE6oZThrOZRQOPAMOATOA6M8s8ZrOfAdOcc72Ba4G/hrtQERGRuqIyLee+QI5z\nbp1zLh94Dhh9zDYOSAg9bgJsCV+JIiIidUtMJbZpDWwq8zwXOO+YbX4FvG5mPwQaAoPDUp2IiEgd\nVJmWs1WwzB3z/DrgCedcG2A48P/M7Lh9m9kEM8s2s+zt27effrUiIiJ1QGXCORdIK/O8Dcd3W38b\nmAbgnPsQiAdSjt2Rc26Kcy7LOZeVmpr69SoWERGp5SoTzguBTmbWzszi8Cd8zTpmm43ApQBm1g0f\nzmoai4iIfA2nDGfnXCEwEZgLrMKflb3CzH5jZqNCm90FfNfMPgWeBW52zh3b9S0iIiKVUJkTwnDO\nzQHmHLPsF2UerwQuDG9pIiIidZNGCBMREQkYhbOIiEjAKJxFREQCRuEsIiISMApnERGRgFE4i4iI\nBIzCWUREJGAUziIiIgGjcBYREQkYhbOIiEjAKJxFREQCRuEsIiISMApnERGRgFE4i4iIBIzCWURE\nJGAUziIiIgGjcBYREQkYhbOIiEjAVCqczWyoma02sxwzu+cE21xjZivNbIWZPRPeMkVEROqOmFNt\nYGbRwCPAZUAusNDMZjnnVpbZphNwL3Chc263mTWrqoJFRERqu8q0nPsCOc65dc65fOA5YPQx23wX\neMQ5txvAObctvGWKiIjUHZUJ59bApjLPc0PLyuoMdDazD8xsgZkNDVeBIiIidc0pu7UBq2CZq2A/\nnYCLgTbAe2bW3Tm3p9yOzCYAEwDS09NPu1gREZG6oDIt51wgrczzNsCWCrZ52TlX4JxbD6zGh3U5\nzrkpzrks51xWamrq161ZRESkVqtMOC8EOplZOzOLA64FZh2zzUxgEICZpeC7udeFs1AREZG64pTh\n7JwrBCYCc4FVwDTn3Aoz+42ZjQptNhfYaWYrgXnAT5xzO6uqaBERkdqsMseccc7NAeYcs+wXZR47\n4M7QTURERM6ARggTEREJGIWziIhIwCicRUREAkbhLCIiEjAKZxERkYBROIuIiASMwllERCRgFM4i\nIiIBo3AWEREJGIWziIhIwCicRUREAkbhLCIiEjAKZxERkYBROIuIiASMwllERCRgFM4iIiIBo3AW\nEREJGIWziIhIwFQqnM1sqJmtNrMcM7vnJNtdbWbOzLLCV6KIiEjdcspwNrNo4BFgGJAJXGdmmRVs\n1xi4Hfgo3EWKiIjUJZVpOfcFcpxz65xz+cBzwOgKtvtv4I/A4TDWJyIiUudUJpxbA5vKPM8NLTvK\nzHoDac652WGsTUREpE6qTDhbBcvc0ZVmUcD/AnedckdmE8ws28yyt2/fXvkqRURE6pDKhHMukFbm\neRtgS5nnjYHuwDtm9gVwPjCropPCnHNTnHNZzrms1NTUr1+1iIhILVaZcF4IdDKzdmYWB1wLzCpZ\n6Zzb65xLcc5lOOcygAXAKOdcdpVULCIiUsudMpydc4XARGAusAqY5pxbYWa/MbNRVV2giIhIXRNT\nmY2cc3OAOccs+8UJtr34zMsSERGpuzRCmIiISMAonEVERAJG4SwiIhIwCmcREZGAUTiLiIgEjMJZ\nREQkYBTOIiIiAaNwFhERCRiFs4iISMAonEVERAJG4SwiIhIwCmcREZGAUTiLiIgEjMJZREQkYBTO\nIiIiAaNwFhERCRiFs4iISMAonEVERAKmUuFsZkPNbLWZ5ZjZPRWsv9PMVprZUjN7y8zahr9UERGR\nuuGU4Wxm0cAjwDAgE7jOzDKP2WwJkOWc6wnMAP4Y7kJFRETqisq0nPsCOc65dc65fOA5YHTZDZxz\n85xzeaGnC4A24S1TRESk7qhMOLcGNpV5nhtadiLfBv59JkWJiIjUZTGV2MYqWOYq3NDsBiALGHiC\n9ROACQDp6emVLFFERKRuqUzLORdIK/O8DbDl2I3MbDBwPzDKOXekoh0556Y457Kcc1mpqalfp14R\nEZFarzLhvBDoZGbtzCwOuBaYVXYDM+sN/AMfzNvCX6aIiEjdccpwds4VAhOBucAqYJpzboWZ/cbM\nRoU2+x+gETDdzD4xs1kn2J2IiIicQmWOOeOcmwPMOWbZL8o8HhzmukREROosjRAmIiISMApnERGR\ngFE4i4iIBIzCWUREJGAUziIiIgGjcBYREQkYhbOIiEjAKJxFREQCRuEsIiISMApnERGRgFE4i4iI\nBIzCWUREJGAUziIiIgGjcBYREQkYhbOIiEjAKJxFREQCRuEsIiISMApnERGRgKlUOJvZUDNbbWY5\nZnZPBevrmdnzofUfmVlGuAsVERGpK04ZzmYWDTwCDAMygevMLPOYzb4N7HbOdQT+F3gw3IVW5I9/\nhHnzyi+bN88vr05BqUPKC8LvZfhwmDSp/LJJk/zy6qQ6VEfQ6whCDUGqA+fcSW/ABcDcMs/vBe49\nZpu5wAWhxzHADsBOtt9zzjnHnam333YuJcXfV/S8upS8b8+ezg0cGLk6pLwg/F4eesg5M39f0XPV\noTpUR3BqqOo6gGx3iswtuZnf/sTM7GpgqHPuO6Hn44HznHMTy2yzPLRNbuj52tA2O06036ysLJed\nnf21vlCUNW8ejBgBqamwdSt06wZJSWe829O2ezcsWwZpaZCXB9OmwaBB1V+HlDdvHlx+OURFQXFx\nZP59bNoE69ZBQgLs2wft2/t/J9VNdaiOoNcRhBrK1tGqlc+VP/0J7rzzzPdrZoucc1mV2bYyx5yt\ngmXHJnpltsHMJphZtpllb9++vTL1ndKgQdCjB2zcCC1bRiaYwb9vWpqv49ZbFcxBMWgQDBkCR45E\n7t9HWlrpH5uEhMj8sVEdqqMm1BGEGsrWsWUL9O8fnmA+badqWhPgbm3nSrsqf/7zyHYlB6UOKS8I\nv5eSbrGLLopMN53qUB01pY4g1FCVdXAa3dqVCecYYB3QDogDPgXOOmab24C/hx5fC0w71X5r4zHn\nSNch5QXh91IXjqOpDtVRW2qo6jrCGs5+fwwH1gBrgftDy34DjAo9jgemAznAx0D7U+0zHOH84IPH\n/6F9+22/vDoFpQ4pLwi/l2HDjv9P/dBDfnl1Uh2qI+h1BKGGqq7jdML5lCeEVZVwnRAmIiJSE4T7\nhDARERGpRgpnERGRgIlYt7aZbQc2hHGXKfizxMXT51GePo9S+izK0+dRnj6PUuH+LNo651Irs2HE\nwjnczCy7sn35dYE+j/L0eZTSZ1GePo/y9HmUiuRnoW5tERGRgFE4i4iIBExtCucpkS4gYPR5lKfP\no5Q+i/L0eZSnz6NUxD6LWnPMWUREpLaoTS1nERGRWqFWhLOZDTWz1WaWY2b3RLqeSDKzNDObZ2ar\nzGyFmf0o0jVFmplFm9kSM5sd6VoizcwSzWyGmX0W+jdyQaRrihQz+3Ho/8hyM3vWzOIjXVN1MrPH\nzGxbaMrfkmXJZvaGmX0euo/QPH/V7wSfx/+E/q8sNbOXzCyxuuqp8eFsZtHAI8AwIBO4zswyI1tV\nRBUCdznnugHnA7fV8c8D4EfAqkgXERB/Bl5zznUFzqaOfi5m1hq4HchyznUHovGT9tQlTwBDj1l2\nD/CWc64T8FboeV3xBMd/Hm8A3Z1zPfHzS9xbXcXU+HAG+gI5zrl1zrl84DlgdIRrihjn3Fbn3OLQ\n4/34P76tI1tV5JhZG2AE8K9I1xJpZpYADAAeBXDO5Tvn9kS2qoiKAeqbWQzQANgS4XqqlXPuXWDX\nMYtHA0+GHj8JjKnWoiKoos/DOfe6c64w9HQB0Ka66qkN4dwa2FTmeS51OIzKMrMMoDfwUWQriaiH\ngbuB4kgXEgDtge3A46Fu/n+ZWcNIFxUJzrnNwJ+AjcBWYK9z7vXIVhUIzZ1zW8F/0QeaRbieIPkW\n8O/qerPaEM5WwbI6fwq6mTUCXgDucM7ti3Q9kWBmI4FtzrlFka4lIGKAPsDfnHO9gYPUrW7Lo0LH\nUkfj56lvBTQ0sxsiW5UElZndjz9kOLW63rM2hHMukFbmeRvqWPfUscwsFh/MU51zL0a6ngi6EBhl\nZl/gD3dcYmZPR7akiMoFcp1zJT0pM/BhXRcNBtY757Y75wqAF4F+Ea4pCL4ys5YAofttEa4n4szs\nJmAkMM5V47XHtSGcFwKdzKydmcXhT+qYFeGaIsbMDH9McZVzblKk64kk59y9zrk2zrkM/L+Lt51z\ndbZ15Jz7EthkZl1Ciy4FVkawpEjaCJxvZg1C/2cupY6eHHeMWcBNocc3AS9HsJaIM7OhwE+BUc65\nvOp87xofzqGD9ROBufj/XNOccysiW1VEXQiMx7cSPwndhke6KAmMHwJTzWwp0Av4fYTriYhQ78EM\nYDGwDP+3sE6NjGVmzwIfAl3MLNfMvg38AbjMzD4HLgs9rxNO8HlMBhoDb4T+lv692urRCGEiIiLB\nUuNbziIiIrWNwllERCRgFM4iIiIBo3AWEREJGIWziIhIwCicRUREAkbhLCIiEjAKZxERkYBROIuI\niASMwllERCRgFM4iIiIBo3AWEREJGIWziIhIwCicRUREAiYmUm+ckpLiMjIyIvX2IiIi1WrRokU7\nnHOpldk2LOFsZmnAU0ALoBiY4pz788lek5GRQXZ2djjeXkREJPDMbENltw1Xy7kQuMs5t9jMGgOL\nzOwN59zKMO1fRESkzgjLMWfn3Fbn3OLQ4/3AKqB1OPYtIiJSbd5/GNa/W37Z+nf98moU9hPCzCwD\n6A18FO59i4iIVKnWfWD6zaUBvf5d/7x1n2otI6wnhJlZI+AF4A7n3L4K1k8AJgCkp6eH861FRCRo\n3n/Yh1q7AaXL1r8LmxdD/zsiU9PhvZCfBwV5UHAICg9DXENo1s2vP7ANelwDz3wTsr4Nnz4DY58o\n/zNUg7CFs5nF4oN5qnPuxYq2cc5NAaYAZGVluXC9t4iIBFBJK7Qk3EpaoWOfKN2muBiKjkBsff98\nby7k7fTBWRKgUbHQeYhfv3Q67FobWnfY3zduAZf8zK+feRt8tTz0+tA+Wp8D46b59f8YCLvXl6+z\n8zC4/jn/eO59cOAr//jD/4MBd1d7MEP4ztY24FFglXNuUjj2KSIiNUB+HhzaBU3a+Oe52bBlCeTt\n8iHbvDs8fRVceAdkPwoZF8GL3/OhWXjY3xqmwk9y/Ovn3A2rXy3/HoltofNS//iTp2HdOxBdzwd6\nbANo0b1027gG0Kh5aF3oltKldP3An0LhIf+62PoQU9+He4nvvOlb9rN/DFnf8jW3u6jaA9qcO/MG\nrJn1B94DluEvpQK4zzk350SvycrKcrqUSkSkkqqrizhvl29Z5u32oZu30y+78EdQrxEsehI+/mfp\nusLD/nX3bfXB+Np9sOARv6xeE2iQBIVHYP9W3wpt0toHeElwxtSH+CZwwQ/8azZ+BHk7StfF1od6\njaFpB7++8IhvSUdV0RhaZVv3x7b2zzCgzWyRcy6rMtuGpeXsnHsfsHDsS0REKlCZLuISxcVweA8c\n2l0arm3OhYZNIXcRLH7SLz+0u7SFe9Mr0KwrLJsO/777mB0a9LrOh3NcQ99KbtkTGiRD/WR/b6EI\nuOgu/2WhfhJEx5bWOeBu3wod+wScc/OJf870807+OcTUq9zn9XVtXlw+iNsN8M83L67W1nNYWs5f\nh1rOIiKnaf27MO1GaNMXvngPOl8OsQ19uPa/A9LPh5y3YOrV4IrLv/aGF6HjpfDZq/DKHdCgaShc\nk/zji+6EpAzYtR62f1Yaug2a+pZtVPTXq7eKWqE1UbW3nEVEJMycg72bYNPHsOkj6DQEOl0GmaNh\n0RN+m8/mhEK2KRw54Jclt/et1/rJZQI4GVI6+fVdR/jbiSS387dwCEgrtCZSOIuIBEnBYXjpez6U\n92/xy2Ib+lZtTD1Y9Qqc9z1/1vLYJ6D9wPKvT25XeuZypFV0LLzdAAVzJSicRUQi4eBOyA21ijd9\n7M9IvvJvEBvvL+XJuBDSzoO0vtDsLNg4v3yXcNeRdbqLuLZTOIuIVLXiYtiXC4mhwZeeGwefzfaP\no2Kh5dmlZyMDfOu14/ehLuI6ReEsIhJuRw7A5kWlx4tzP4biIvjpBoiOgY6DoU0WpJ0PrXqVDsBx\nMuoirlMUziIiZ8I52LPRB3GXof6a3Pn/B//5g1+f2g3OutJ3UbsiIAayboloyRJ8CmcRkdO1byus\neLH0ePH+rX75+JegwyXQY6y/rrhNFtRPjGytUiMpnEVETubgjtLu6Q6X+LOjD3zpx2BObOuHo0zr\n61vGzTL9a1I6+pvI16RwFpG651RDYRYcgtl3+kDetdavj4qFhik+nJv3gLtWlx+TWSSMFM4iUveU\nDIU55u8QEwdLp/lbp8v8+ph4+HIppHaFc27yreKWvfxlTuBP6lIwSxVSOItI3VNylvMzY0uXJWZA\nix7+sRnc+kFEShMBqKJpPUREAmbPRvjgz/6SJvDHh1ue7R/3ux3u+BQG3Re5+kTKUDiLSO2Vn+e7\nq58cBQ/3hDd+AVs+8evSz4e9uX62pE+m+mPOIgGhbm0RqZ22rYJHh8CRff6s6ovv9dMeJqYfPztS\nu4s0FKYEisJZRGqHfVth6XMQ28BPDJHSGc6+FrqNgrYXQlSZjkINhSkBp/mcRaTmKjwCq+fAkqmw\n9i0/h3HmGLjmyUhXJnIczecsInXDq3fCkqchoTX0/zH0Gld+AgmRGkrhLCI1w8EdsPR5+OQZ+MY/\noXkm9P2eH7e6/SCIio50hSJho3AWkeAqKoTPX/dnU695DYoLoVVvf5IXQMueka1PpIoonEUkeI7s\n97M7FeTBjG9BvUZw3vd9t3XzzEhXJ1LlFM4iEgyHdsPyF/zJXa4IvvcuxCfAt+f6AUOiYyNdoUi1\nUTiLSGTlLoIFj8Cq2VB0BJqdBb3H+ZG8oqJLR/ESqUMUziJS/XauhQbJUD8Jtq2EtW/7CSZ6jfNh\nbBbpCkUiSuEsItXjyH5YMdOf3LXxQ7j8AbjgB9DzGn+LqRfpCkUCI2zhbGaPASOBbc657uHar4jU\ncMVFMOt2WPGiP8GraScY/Ct/CRQolEUqEM6JL54AhoZxfyJSE7z/8PGTRiydDjO+7R9HRcPhPdBj\nLHz7DZi40A8YktCy+msVqSHC1nJ2zr1rZhnh2p+I1BCt+/hJI8b8Aw7tgg8nw5dLwaJhxENQPxGu\nnRrpKkVqFB1zFpEz024AXHAbPDMWcGBRcPb1cPE9PphF5LRV63zOZjbBzLLNLHv79u3V+dYiEk77\ntvru7LVv++c9rvHXIgP0vwuu/BsktY1cfSI1XLWGs3NuinMuyzmXlZqaWp1vLSJnqvCIP9t66lj4\n30x485ewdp5ft3s9HPgSBtwNix47/hi0iJwWdWuLSOU8MRJyP4bGreDCO/w1ySkdfRBPv7l0fuR2\nF5V/LiKnLZyXUj0LXAykmFku8Evn3KPh2r+IVKODO2DpNPhsNtzwIsTGQ/87ILoedDhmBqjNi8sH\ncbsB/vnmxQpnka/JnHMReeOsrCyXnZ0dkfcWkQoUFULOG35+5DVzobjAzwB19WOQ3D7S1YnUeGa2\nyDmXVZlt1a0tUtcVFfhJJbZ+Cs9eCw1T4bzvaQYokQhSOIvURYf2wPIZ8Mkz0KIHXPFnf73yDS/6\nrmjNACUSUQpnkbpk/Xuw6PHyM0C17OXXmUHHSyNbn4gACmeR2m/XekjK8OG7cibkvAV9bvTTMrbs\npRmgRAJI4SxSGx3ZDytfhiVTYeN8+NZcSD8fBt0PQ37nz74WkcBSOIvUJgd3wus/88FccBCadoRL\nf1l6tnWD5MjWJyKVonAWqen2bIK9m6BtP6jXGDZ8AD2ugl43QFpfdVuL1EAKZ5GaKD/PDxCy5Gk/\nQldSBty+BGLi4PZPIKpaR+YVkTBTOIvUNAsfhTd/BUf2QWK6n/3p7OtKW8gKZpEaT+EsEiTvP+yv\nNy477OWKmfDJ0zD8IT/TU5M06DLcn23dtr/CWKQWUjiLBEnrPn7SiG/8E/IPwAd/gc2hYW7XvgVZ\n34LOQ/xNRGothbNIkLQb4IP56asABxYF3a/2XdcpnSJdnYhUE/WHiUTawZ2w4G8w63b/vOOl0P5i\n/7j/nXD1owpmkTpGLWeRSCiZAeqTqbD6tdIZoPLzfDf2l0thwN2Q/Si0H6ipF0XqGIWzSHVyzp9V\nveQpmP1jaJASmgHqemh+lr8savrNpfMjt7uo/HMRqRMUziJV7dAeWP6CbyX3uRHOuRkyx0Cj5tBp\nSPkZoDYvLh/E7Qb455sXK5xF6hCFs0hVWfu2H9v6s9lQeBiaZUK9BL+uQTJ0HXH8a/rfcfyydgMU\nzCJ1jMJZJJwO7oSGTf3jt38LO9dC7/G+27pVbw2lKSKVonAWOVNHDvipGJdMhS1L4K7PoH4iXPUo\nNG6pGaBE5LQpnEW+rp1r4b2H/AheBQchuQMM/Enp+uR2katNRGo0hbPI6dizCQqPQEpHcMWwchZ0\n/wb0vgHSzlO3tYiEhcJZ5FQKDsGq2X5863X/gcxRcM1TfmCQn+So21pEwk7hLHIy8x7wo3cd2QtN\n0mHgT6HXdaXrFcwiUgUUziJl7f8Slr8Ifb/rrz+OjoUuQ6HXOMi4SDNAiUi1UDiLFObDmn/7s61z\n3gRXBC3PhowLYcB/Rbo6EamDwhbOZjYU+DMQDfzLOfeHcO1bpMrsWgf/vBQO7fKXPV14u28la6IJ\nEYmgsPTRmVk08AgwDMgErjOzzHDs+2QWPPVzlv/l15CR4bsbMzJY/pdfs+Cpn1f1W3997z/sx08u\na/27fnmQ1cS6K6p51Wx45lp/HBkgMQPOuhLGzYAfr4DBv1Iwi0jEhesAWl8gxzm3zjmXDzwHjA7T\nvk+o0Z4YWu+YwvLmceAcy5vH0XrHFBrtCXBvfes+fiKDktAomeigdZ9IVnVqNbHukprXzoM1c+Gx\nYfD8ON+FnfOm3yYqCkZOgk6XQVR0RMsVESlhzrkz34nZ1cBQ59x3Qs/HA+c55yae6DVZWVkuOzv7\nzN44I8MH8uUH2X2kIRnx2ygsMOKKHDRtCg2awsSP/bYv3Qqfv17+9YlpMOEd//j58bBhfvn1qV3h\nllf94//3Ddj6afn1rc+BcdP840cvh5055de3GwBjH/eP/94f9m31j4vy4ch+aNEd9m3xExu88iM4\nvK/863tcDcMe9I//1BmKi8qvP+cmuPQX/lKf/+1+/OdzwQ/gorvg4A545Lzj1w+828+ItHsD/POS\n49df9mt//e62VfDEyNK6Y+L9WNEDfgKX3A+52fDMN49//Zi/QufLYd07MOPbx6+/5knI6A+fvVo6\nl3FZN8zwQ14unQ6v3XP8+m+95lu52Y/7oTKP9b13YddaX1tBHmDQbZT/uVtU8HmJiABMnQr33w8b\nN0J6OvzudzBu3Bnv1swWOeeyKrNtuJqYFY28cFzqm9kEYAJAenr6mb/rxo103+BY0PUczm/3OUuL\nM/g0qiMph/bQIb8+bTt3o17JtmnnQmz98q9vkFz6uG0/aJhafn1Cy9LH7QZAUkb59WVHgOpwiZ/y\nr6zUrqWPOw3xsxOV2LwItn7i5+xtNwC6DPchW1ar3qWPu43yg16U1aKHv7doyKygoyK1m7+PqVfx\n+qYd/X1co4rXl/y89RJK15fU3bKX/5kA6idV/PrGoc+vUfOK1zds5u8TWlW8vn6Sv09Mr3h9vcb+\nPrl9xetj6/vPtusIWDYd+t87oB8KAAAdSklEQVQJg39x/HYiUnWqKOjCorgYDh+GI0f8/eHDMGMG\n/OIX/jHAhg0wYYJ/XI11h6vlfAHwK+fc5aHn9wI45x440WvC3XJevaElndtu5aWV5zEtcTCrU9oy\ncvUHTG6wAW65BTdoEBYdkG7Lki7hrG9D9qM1Z67emlh3TaxZpLaYOtUHW15e6bIGDWDKFB90RUWl\noXiyW9nwDOetoKDyP0vbtvDFF2f0cZxOyzlc4RwDrAEuBTYDC4HrnXMrTvSacITz8r/8mtY7prB5\nbkO6f/w5y/t2ovXlB9ncdALF3YYQ+8rLdHv6H6yzBowf9wDfaHyIq8cOoG3vbmf0vmekJCxKQuLY\n50FVE+uuiTWL1ATOwf79sGsX7N59/H3J46lTywdzCTOIjobCwjOrwwzi40/vVq/eydffcov/+Sp6\nr+Li45efVrnVHM6hNx0OPIy/lOox59zvTrZ9OMJ5wVM/p9GeGLpPevxol8nyO2/hQGIh59/4336j\nw4dZ+ewr/GHhdt5rnIazKPru28TYzk244qbhxCc1OaMaTtv7D/sTlcqGw/p3YfPiiufyDYqaWHdN\nrFnkZMLdRXzoUMXhemzIHnu/Z49v9Z5IXBwkJ8OXX554m/vuO/NgjY0N/3j2GRm+K/tYNbHl/HWE\npVv7NG39bB0vPvs2M3ZEs7l+Igsf/z5NRo9gx/U30fSSizCN/iQiQXWiLuK//x2GDv16IXvkyInf\nLyoKEhN9yCYl+VvJ41Pd16/vQ7MKg67KnKor/gwonE/BFRez/o33aT/9KXj+ecZc+St2JzTl6uQC\nrrr2ElpldohIXSIiFdq5EzIzYdu2039to0YnDtGTBWxCwpkPV1uFQVelAnC2dp0M57LcgQO89Nhs\npq/azYdN0jFXTP/9m/herxT6j7/CfwMUEakue/fC4sWwcCFkZ/vb+vUnf81f/nLi8I2NrZ66TyTI\nZ2tXM4Xz17Tpk8+YMf1dZuyL57b3nuH6Lxaw97rxrB99LWcPuUDd3iISXgcPwpIlpSG8cCGsWVO6\nPiMDzj0XsrLgoYcqbjkHuYtYyonEdc61Qlqvrvy4V1d+VFhE0Tut4cknmLV4Ez9vsodOrzzO2ObG\nmHGX0axDWqRLFZGa5vBhWLq0NISzs2HlytIzgFu39iE8frwP5HPOgZSU0te3bl1xF/HvTnrurdRQ\najmfwr6vdvLq068xPecAi5u0Ibq4iEEHNvHIJS2pN2qkPytRRKSsggJYsaJ81/SyZaXX1aamlraI\nS24tW558n6Au4hpO3dpVJGfBUmbMnM/GL77ir8//ClJSmHbjTzhr5MWcNahvpMsTkUgoKoLPPivf\nNf3JJ6VnQicmlgZwSSCnpYX/EiAJPIVzVSsshDfe4NATT3Fuq29woF4DMvdtYWybOMaMH0JSWiW+\nAYtIzVNcDGvXlm8RL17sjx2DPzO6T5/yreIOHRTEAiicq9Xu3C+Z9fRcpm/MZ3lCK2KLCnho+weM\nGnsxXH45xOiwvkggnaqL2Dl/jW7ZFvGiRf5savCDYPTuXb5V3LmzH/lKpAIK5whZ9Z9sZsxeyE0v\nTiZ93Ure63MJ7w8ey9gxF9DxgrMjXZ6IlKjo+tv69eHWW/1JViWBvGOHXxcbCz17lu+azsyM/GVK\nUqMonCMtPx/mzOGRWUuYlHIORVHR9NqXy9h2Ddne7Wz6bl5Jvwd+evQb+/x7H2Rp1yy+P1CDn4hU\nixONXAW+5XvWWeVP1urZ0w8dKXIGdClVpMXFwZgx3DZmDNesy2Xm1DeYvi+G+3cm0frVhTwVG89k\nl0A/55jvEpi4spjJZIPCWaRq5ebC9OknDmYz2LfPt55FIkgt52riiotZ9uZH7Lzzburt38tto+8h\ntqiQffEN+cO//8KYvA0aSECkKnz1lZ+j9/nn4b33/LLY2IqnC9SAHlKFTqflrCGvqolFRdFzyAUM\nWvkB/TYu48oV89jWuCmHY+pxx6i7+Wa/7zPjz8+St2tvpEsVqfl27PDjN196KbRqBRMn+vGpf/1r\nf9nT448f3zrWgB4SIOrWrm7p6cx3Ccw8axC3f/AsT/UZweVrPuSj9B7819YE0s8fTN/+PcgbfzP1\nB/bXkKEilbVnD8yc6VvIb7zhrz/u1MlPTfjNb0L37qXbduni7zWghwSUwrmazb/3QX+MeeYD9Nu4\njPM3LmXimHuZ3C2Keq0b0mfH2fD88/xhUz3efWEdVycX8o1rB2mmLJGK7N8Pr7wCzz0Hc+f6kzHb\ntoW77oJrr4VevU58jfG4cQpjCSyFczVb2jWLyWTT7+V9YEY/28fkzKjSs7WvuBj+/GfOf/wV1qzc\nzZ/y03noyZX03/8643qmMPSmkZopS+q2vDx49VXfQn71VT9mdevWcNttvoXct68G/ZAaTyeEBdzG\n0ExZL+yN56K12fzhw6dw113Hiitv4KzBmilL6ogjR+C113wgz5rlR+Rq3hyuvtoH8oUXnvncwyJV\nTJdS1SLpvbpyZ6+u3FFYxMG3WkHTXXzy7w+4sslIOr2smbKkFisogDff9F3WM2f6S5yaNvVd0d/8\nJgwcqNG4pNZSy7kG2r9tJ6/8v9eYvvYASxL8TFkXH8zltxe1pOU3RmimLKm5CgvhnXd8C/nFF2HX\nLmjSBK680h9DvuQSjcolNZZGCKtDchYsZfrM+by1J5rZj04kPjGBD8b/kKRRw8i8+NxIlydyasXF\n8P77PpBnzIBt2/wEEqNH+xbykCEanUtqBYVzHeQKCrA33oDHH2dowsV8lprBWfu2MDYtjtHjLyep\nTYtIlyhSyjlYsMAH8vTpsGWLP9Fx5EgfyMOH68RHqXUUznXc7twvefn/zWX6pnxWJLQirrCAO/Yu\n5QdXZvlWiGbKkkhwzk+v+Pzz/rZxo28RDxvmA3nkSN9iFqmlFM5y1Mp3FjJjdjYXvv0Cly55iy0d\nMnnyqh8y9sp+dDy/Z6TLk9rOOVi2zIfxtGmQk+O/HA4Z4gN59Gh/TFmkDlA4y/Hy8+HVV5n1wrv8\nuNUgiqKi6b0vl7HtGzJy/DASmjeNdIVSkx07N/Jtt/nrkZ9/Hlat8pc5XXqpD+Qrr4Tk5EhXLFLt\nFM5yUtvWbuLlqW8w/UvHmoQWNMrP46Ptr9Lw5vEwaJCuF5XTU9HcyCUGDvSBfNVV0KxZ9dcmEiDV\nGs5mNhb4FdAN6Oucq1TiKpwjzxUXs/T1D1n67/cZ/9QfYM8e7rrmZ7TumMbV1wwk/ewukS5Rgqyo\nCD76yB8z3rfv+PWtW/spGkUEqP5w7gYUA/8A/kvhXEMdPkzBSzP5zns7ebdxOs6iOG/fJsZ2SWT4\njcN5aul2en6WTb8Hfnq063L+vQ+WDjsqdcOBA/D6634869mz/exPJ2LmL5MSEaCaRwhzzq0KvemZ\n7koiKT6e2Ouu5cnrYMvKtbz43DxmRMXzX1sbs3fkd+jZtB63ZQznEZdAP+eY7xL8BB5kg8K5dtu0\nyQfxrFnw9tv+/IWkJH+50xVXwE9+4rc5Vnp69dcqUkvomho5TqvMDkz8TQduKy5m4Sv/odP2ZJIe\nn8I1A6K54Zu/pe+m5XzWrB1/nfmAn8Dje9+MdMkSTiWXPM2a5VvIS5b45R07+nmRR43yY1mXXJJX\nWHj8MWfNjSxyRioVzmb2JlDRKBb3O+deruybmdkEYAJAur5VB55FRdF39CAYPQgen0K/jcuY3W0A\nC9qeDc7xt/PHsmPZm1xx6BCmASNqtkOHfKv4lVf8bcsWf2Jgv37wxz/6FnKXLhXP9lQy7aLmRhYJ\nm0qFs3NucDjezDk3BZgC/phzOPYp1SQ9ndiiAg7FxnPjoleY1vMyVqW24+/nXcWoVq3guuvYdM2N\ntBnQVzNl1RRffeWnXJw1C954w7d8GzWCoUN9GA8fDikplduX5kYWCSt1a0ulzL/3QX+MeeYD9Nu4\njKFr5jNxzL3c1jwfhg9n/9TnuKz+paRPe5yxLaMYM+4yUtu3iXTZUpZzsGJFaXf1Rx/5ZenpcMst\nvrt64ECNYy0SAGfcxDGzK80sF7gAeNXM5p55WRI0S7tmMTkzin62D8zoZ/uYnBnFpmFXwtSpxOR8\nzi9aHqIhhfzuYDPO//tivnPbX1k1daY/gUgiIz/fT7v4ox9B+/bQo4fvfi4qgt/8Bj75BL74AiZP\n1gQTIgGiQUgk7HI+/JTpMz/kpUONefrZ++hMHjnjJ1Aw+kq6DazUVQRyJnbtgn//27eQX3vNX4Mc\nHw+XXea7q0eOhJYtI12lSJ2jEcIkEIryC4h+43V4/HF+nN+OlzIvpvu+LYxNj2P0+KEktm4e6RJr\nj88/L+2ufv993zJu0cIH8RVXwODB/gxqEYkYhbMEzq5NX/Ly03OZUWamrOsO5vDrKzI1U9bXUVgI\nH37ow3jWLFi92i/v2dOH8ahRkJWloVhFAuR0wln/c6VaJKe14JZ7b+LVv36XOUObMY4tNFuzHEaM\noCgjgz/f81fWfrws0mUGw9SpkJHhgzUjwz8H2L8fZsyAG2/0reIBA+Dhh/0JXf/3f/7Y8aefwm9/\nC337KphFajC1nCVy8vNh9myWPTebMRljKIqKps/eXMZ2bMTIG4fROLUOzlxU0SQSsbH+GuPVq6Gg\nwM/oNGKEbyFffjkkJESuXhGpNHVrS42zbe0mZk59g+lfOT5v3IL4giPM3PkWXcd/o+7MlLV7N2Rm\nwpdfHr8uJgbuuMN3V19wgQ4DiNRACmepsVxxMZ++/iFz5i7ip0/8kug9e/jXkFvYl3U+Y68ZSFpN\nnykrPx/WrvWt4DVr/H3JTZNIiNRqCmepHQ4dgpkzufPtzbyU3AVnUZy/byNXd01m+PhhNEgKaHeu\nc7B16/EBvGYNrF/vz6Qu0by577IuuT34IGzffvw+27b1x5RFpMZSOEuts3nlWl56bh4zdsXwRaNU\nrlr1Hx5K2Qm33ILr1y8yQ4YeOOAvYSrb+l2zxt/27y/drn596NzZ38oGcefO0KRJ+X1WdMy5QQOY\nMkXDY4rUcApnqbVcaKashNkv0/XZf7G6fgq3XvNLrkop4qrrLqVF13bhfcOiItiwoXz4ljzevLl0\nOzN/1vSx4dulC7Rpc3rHzKdO1SQSIrWQwlnqhgMH+OTpl/n90v18nJBGVHER/Q/kMrZncy6/YRiP\nLdpKz8+y6ffAT48G3fx7H2Rp1yy+f+wc1Dt3VnwcOCen/PCjiYnHh2+XLn46Rc3MJSIncTrhrFM+\npeZq1Ihe3x/HNGDDklXMmP4eL0Q35u41MKh9B3peeDm3pV/OIy6Bfs4x3yUwcUURk1dPhQ/jyreG\nd+4s3W9MDHTo4EN3xIjyYZyaWvG0iSIiYaSWs9QqRYVFrJ39Fp2nPwnPPMPF3/0HmxJbcPaWNaxJ\nTeePcx5m+JoP/cYtWlTcCm7XTpcqiUjYqVtbBCiOiua5npcx+YJr2NKkdBzvmxe9wq9mPIhLSGD1\nV/vp1Kwx0VFqDYtI1VK3tggQlZ5Gxu4tHI6N5wcfPs/TvUdwxcp3GXhgIzRpwubdeQx9+D0a1Yuh\nd3oifdKTOKdtEn3aJtGonv5riEjk6C+Q1Frz732QiSuLmTzzAfptXEb/Lz5h4ph7GZE5AIAm9WN5\n+Ju9WLRhN4s27Ob/3v6cYgd/vrYXo3u1ZtOuPD5ev4tz2ibRtmkDTMeaRaSaKJyl1lraNYvJZNPv\n5X1gRj/bx+TMKJZ2zaIf0Dg+ljG9WzOmd2sADhwp5NNNe8hs6Qc3eWf1Nn7+8goAUhrFHW1ZX9s3\nnSb1YyP1Y4lIHaBjziInUFzs+HzbARZt2E32hl0s3rCbjbvy+PSXQ2gcH8v07E2s+Wq/7wpPT6JZ\nQnykSxaRANMxZ5EwiIoyurRoTJcWjbn+vHQA9uTl0zjet5pXf7mfpxZs4J/vrQcgLbk+/Tum8sA3\nekSsZhGpHRTOIqchsUHc0cc/G5nJ3UO7snzLXhaHjlvvP1xwdP03//EhMdHGOelJnJORTK+0RHWH\ni0ilKJxFzkBcTBR90n239ncuKl3unCOzVQIfrdvF5Hk5FDs/dskt/drxiysyAdi4M4+05PrlTjT7\n+3/W0rNNE/p1SDm6bP7aHSzN3Xv8qGYiUmspnEWqgJnxyyvOAkpPNFu8YTedWzQG4Kt9hxnwP/No\n2jCOPm39iWbntE2ia4vGTHxmCZOv702/DinMX7vj6HMRqTt0QphIBOw9VMCcZVuPXsa1fsdBACZd\nczYtmsTzg6cX07NNExZt3M0PL+nEhR1SSGwQS4sm8cRGR2AGLhE5YxohTKSG2XngCIs37qFXWiKp\njetxy+MLmbd623HbzZp4IT3bJPLyJ5v52ztrSWwQS1KDOJIaxpHUIJbv9G9PUsM4cnfnsW3/EZJD\n6xLiY3SdtkiE6WxtkRqmaaN6XJbphxidv3YHn27aw3cvase07Fx+PLgTrZMasPtgPm2TGwKQUD+W\ntOQG7MnL5/NtB9h9MJ89hwq46YIMAGYsyuXhNz8/uv/oKCOpQSxv3XUxTerH8tKSXBas3UViQx/u\nyQ3iSGwQy+BuzYmKMg7lFxEbbcRUopWu4+Qi4adwFgmQo8eYx/ljzoO6Njt6zPmyzLSj2w3q0oxB\nXZqVe21xsTs6YdZVfdpwdptEdufls+tgPnvyCtidl390WNINO/OYt3obe/IKyC8qBvzJbav/eygA\nP5u5nBcW59KkfixJDWJJahhH68T6TL6+DwBzV3zJzgP5JDWIJSbKuPXpxTx4VQ+Gdm9ZI46T6wuF\nBN0Zd2ub2f8AVwD5wFrgFufcnlO9Tt3aIser7tBwzpGXX8Sug/nsP1xIZis/Otpbq75iae5edufl\nszuvgN0H84mLieKxm88F4IZ/fcT7OTvK7Sva4LZBHXn6o420bBJP7u5DxMdGUT82mvjYaHq2acIf\nrz4bgN/PWcWOA0eoHxt9dH3HZo2OjtY2d8WXFBc74kPr6sdF07RhHGnJDQDYf7iAejHRxEbb1+qu\nL/sF4tgT78p+9kGiLxQ1X7UeczazIcDbzrlCM3sQwDn301O9TuEsUnMdLihiT15BqFXuA/zVpVuY\ns/xLbr+kI0kN49iwM49D+UUcKvC3dikNuW94NwBufvxjPv/qAIcLijgcWj+wcyqP39IXgPN+/yZf\n7TtS7j1H9GzJI6GWe49fzWX/4UKioywU7lF8o0+bo/u/dsqHxMVEEx8TRf04/wVgYOdUhvVoSUFR\nMY9/sJ4tew4zPXsT/Tul8EHOTn51RSZXZ6WRX1jMp7l7iI2OIibKiIuJIjY6iqaN4kiIj6Wo2JGX\nX0hstF9eXTOa6QtF9ajKmqv1mLNz7vUyTxcAV5/pPkUk2OJjo2nRJJoWTfyQpfPX7mDB+l3cfolv\nOU++vje3XNjuhK9/IhTCJZxzFBWXNhRmfL8fB/MLOZRfxOGCYg4XFNG0UekAMHde1pmDRwo5XFDM\noVDAd2nuL1Mr2c++QwVsKxP+LZrEM6xHS/KOFPH7OZ8d3dfcFV8BsHnPYQB2HjzC2L9/eFzNPxvR\nje9c1J71Ow4weNK7R5dHGcRGR/HbMd0Zm5XGyi37+O5T2aFQNx/y0VH8ZEgX+ndKYeWWfUx6Y83R\ndbHRUcTFGDf3a0eXFo3J2baflz/ZcnRdbLT/gnD5WS2YfH1vbn16MRd0aMr7n+/guxe1Y//hQt5Y\n+RXntU8mIT6WzXsOkbPtAFEGUWZY6L5XWiLxsdF8te8wW/YcIsqs3PrOzRsREx3FzgNH2J1XcPT1\nJdu0TqxPVJSx/3ABhwqKjq6LMn/pYMkAO/mFxRQ7R5QZZ7VKYOLU0BeIjjXj0sCebZoE4nLGcB9z\n/hbwfJj3KSIBdmwL7vwOTU+7RWdmxESXtkBLuq9P5GTBHx1lPDfhghOuT6gfw4pfX857n2/npy8s\nY9TZLZn16VY6Nfcn2yU1iGPqd84jv6iYgsJiCoocBUXFnBXq8k9uWI/7h3ejoLiYgkK/rqComE6h\nLwcN4qI5r30yBUWOwtC6/CJHbOjnO1xYxJY9h46+rmT/V5zdCmhMzraDTJ6Xw7Gdmme1SqBfhxTO\nzUjiteVfAvC/ZU76+/ePLiKhZSxvrvyKX85acdzP/d7dg0hLbsALi3P542urj1u/+OeXkdwwjsc+\nWM8j89Yet/6z/x5KfFQ0D72+hifmf1FuXUyUkfP74QDc99IyZizKLbd+3L8+4oehL26dmzfipsc+\nxkLBHmVGq8T6vHnnQABum7qY+Wt3hL4U+C8GHVIbHv2d3vr0IpZv2Vv6xQHIbJVw9HyIW59exBc7\n88p9OemdlsivR3c/uv/t+49gxtEvJudmJPPjyzoD8OzHm2ib3ICbHvuY6/um88rSrRHpnahUOJvZ\nm0CLClbd75x7ObTN/UAhMPUk+5kATABIT08/7WJFJHiW5u4t98erX4cUJl/fm6W5ewPZ3WpmfJq7\nh/teWs7fbuhDvw4pDOvRkonPLCGxQRz9OqRwYccT153cMI7vDmh/wvUZKQ2ZdE2vE67vk57EnB9d\ndML1Q7u3YP0DIygqdqFg918SGsfHMn/tDhZt2M2489KZvXQr9w7rSo82TXAOMpr6LxfDerSge+sm\nOOcodlDsHMXOkdq4HgAjerSkW8sEv764ZD00rBcNwMierejcvDEu9NqS+5Lr60f0bEmHZo2gzP7L\nGt6jBe1TG/rXFfttPlq/k7+8ncPtl3SkQ7NG9E5P8q8Lvb5kvHqA89onk9wwDod/rXOO1Malk8p0\na5lAfGx0uZ+v5GcHaJ4QT0GRA0rXN4ovjbqYaCM6yvzPXQxF+C9JJXYdPEJhsSOxfixPfriB2y/p\nGJF/x2G5ztnMbgK+D1zqnMurzGt0zFlEIqUmHguticecobTuG85LP3rII8j1QtXVXN0nhA0FJgED\nnXPbK/s6hbOISOXpC0X1qMqaqzucc4B6wM7QogXOue+f6nUKZxGR2q0mfqEIytnaGr5TRESkGpxO\nOGsEfRERkYBROIuIiARMxLq1zWw7sCGMu0wBdpxyKwkHfdbVQ59z9dDnXD30OUNb51xqZTaMWDiH\nm5llV7YvX86MPuvqoc+5euhzrh76nE+PurVFREQCRuEsIiISMLUpnKdEuoA6RJ919dDnXD30OVcP\nfc6nodYccxYREaktalPLWUREpFaoFeFsZkPNbLWZ5ZjZPZGupzYyszQzm2dmq8xshZn9KNI11WZm\nFm1mS8xsdqRrqa3MLNHMZpjZZ6F/1yeeZ1LOiJn9OPR3Y7mZPWtm8ad+Vd1W48PZzKKBR4BhQCZw\nnZllRraqWqkQuMs51w04H7hNn3OV+hGwKtJF1HJ/Bl5zznUFzkafd5Uws9bA7UCWc647EA1cG9mq\ngq/GhzPQF8hxzq1zzuUDzwGjI1xTreOc2+qcWxx6vB//h6x1ZKuqncysDTAC+Feka6mtzCwBGAA8\nCuCcy3fO7YlsVbVaDFDfzGKABsCWCNcTeLUhnFsDm8o8z0WhUaXMLAPoDXwU2UpqrYeBu4HiU20o\nX1t7YDvweOjwwb/MrGGki6qNnHObgT8BG4GtwF7n3OuRrSr4akM4WwXLdAp6FTGzRsALwB3OuX2R\nrqe2MbORwDbn3KJI11LLxQB9gL8553oDBwGdr1IFzCwJ35vZDmgFNDSzGyJbVfDVhnDOBdLKPG+D\nukyqhJnF4oN5qnPuxUjXU0tdCIwysy/wh2guMbOnI1tSrZQL5DrnSnp/ZuDDWsJvMLDeObfdOVcA\nvAj0i3BNgVcbwnkh0MnM2plZHP5Eg1kRrqnWMTPDH59b5ZybFOl6aivn3L3OuTbOuQz8v+W3nXNq\nZYSZc+5LYJOZdQktuhRYGcGSarONwPlm1iD0d+RSdPLdKcVEuoAz5ZwrNLOJwFz8WYCPOedWRLis\n2uhCYDywzMw+CS27zzk3J4I1iZyJHwJTQ1/q1wG3RLieWsk595GZzQAW46/6WIJGCzsljRAmIiIS\nMLWhW1tERKRWUTiLiIgEjMJZREQkYBTOIiIiAaNwFhERCRiFs4iISMAonEVERAJG4SwiIhIw/x92\nNkUOBhyXKgAAAABJRU5ErkJggg==\n", 342 | "text/plain": [ 343 | "
" 344 | ] 345 | }, 346 | "metadata": {}, 347 | "output_type": "display_data" 348 | } 349 | ], 350 | "source": [ 351 | "plt.figure(figsize=(8,6));\n", 352 | "plt.subplot(2, 1, 1)\n", 353 | "plt.step(I_obs, '-bx', where=\"post\")\n", 354 | "plt.subplot(2, 1, 2)\n", 355 | "plt.plot(Y_obs,'-or')\n", 356 | "\n", 357 | "# Plot non-zero observations\n", 358 | "for i in np.nonzero(np.any(Y != 0, axis=0))[0]:\n", 359 | " plt.plot(Y[:,i],'--x')" 360 | ] 361 | }, 362 | { 363 | "cell_type": "markdown", 364 | "metadata": {}, 365 | "source": [ 366 | "Unfortunately, the process is rarely at steady state when control is initiated. If we somehow knew the past 10 moves in $u$, we could get a series of 10 future predictions of $y$ formulated by extending the preceding logic\n", 367 | "and going back in time. \n", 368 | "\n", 369 | "Let us call this the vector $y_p$" 370 | ] 371 | }, 372 | { 373 | "cell_type": "code", 374 | "execution_count": 12, 375 | "metadata": { 376 | "collapsed": true 377 | }, 378 | "outputs": [], 379 | "source": [ 380 | "U_hist = np.array([1,0,0,0,-1,0,0,0,0,0])" 381 | ] 382 | }, 383 | { 384 | "cell_type": "code", 385 | "execution_count": 13, 386 | "metadata": {}, 387 | "outputs": [ 388 | { 389 | "name": "stdout", 390 | "output_type": "stream", 391 | "text": [ 392 | "[[-0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 -0.5 ]\n", 393 | " [-1. -1. -1. -1. -1. -1. -1. -1. -1. -1. ]\n", 394 | " [-1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 -1.5 ]\n", 395 | " [-1.8 -1.8 -1.8 -1.8 -1.8 -1.8 -1.8 -1.8 -1.8 -1.8 ]\n", 396 | " [-1.9 -1.9 -1.9 -1.9 -1.9 -1.9 -1.9 -1.9 -1.9 -1.9 ]\n", 397 | " [-1.95 -1.95 -1.95 -1.95 -1.95 -1.95 -1.95 -1.95 -1.95 -1.95 ]\n", 398 | " [-1.98 -1.98 -1.98 -1.98 -1.98 -1.98 -1.98 -1.98 -1.98 -1.98 ]\n", 399 | " [-1.99 -1.99 -1.99 -1.99 -1.99 -1.99 -1.99 -1.99 -1.99 -1.99 ]\n", 400 | " [-1.999 -1.999 -1.999 -1.999 -1.999 -1.999 -1.999 -1.999 -1.999 -1.999]\n", 401 | " [-2. -2. -2. -2. -2. -2. -2. -2. -2. -2. ]]\n" 402 | ] 403 | } 404 | ], 405 | "source": [ 406 | "A_hist = np.tile(a, (U_hist.shape[0], 1)).T\n", 407 | "print(A_hist)" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": 14, 413 | "metadata": { 414 | "collapsed": true 415 | }, 416 | "outputs": [], 417 | "source": [ 418 | "row_shift_indices = np.atleast_2d(np.arange(a.shape[0])).T + np.flip(np.arange(A_hist.shape[0]))\n", 419 | "row_shift_indices = np.triu(row_shift_indices)\n", 420 | "col_indices = np.arange(A_hist.shape[0])" 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": 15, 426 | "metadata": {}, 427 | "outputs": [ 428 | { 429 | "name": "stdout", 430 | "output_type": "stream", 431 | "text": [ 432 | "[[-2. -1.999 -1.99 -1.98 -1.95 -1.9 -1.8 -1.5 -1. -0.5 ]\n", 433 | " [-2. -2. -1.999 -1.99 -1.98 -1.95 -1.9 -1.8 -1.5 -1. ]\n", 434 | " [-2. -2. -2. -1.999 -1.99 -1.98 -1.95 -1.9 -1.8 -1.5 ]\n", 435 | " [-2. -2. -2. -2. -1.999 -1.99 -1.98 -1.95 -1.9 -1.8 ]\n", 436 | " [-2. -2. -2. -2. -2. -1.999 -1.99 -1.98 -1.95 -1.9 ]\n", 437 | " [-2. -2. -2. -2. -2. -2. -1.999 -1.99 -1.98 -1.95 ]\n", 438 | " [-2. -2. -2. -2. -2. -2. -2. -1.999 -1.99 -1.98 ]\n", 439 | " [-2. -2. -2. -2. -2. -2. -2. -2. -1.999 -1.99 ]\n", 440 | " [-2. -2. -2. -2. -2. -2. -2. -2. -2. -1.999]\n", 441 | " [-2. -2. -2. -2. -2. -2. -2. -2. -2. -2. ]]\n" 442 | ] 443 | } 444 | ], 445 | "source": [ 446 | "M = A_hist[row_shift_indices,col_indices]\n", 447 | "M[np.tril_indices(M.shape[0])] = a[-1]\n", 448 | "print(M)" 449 | ] 450 | }, 451 | { 452 | "cell_type": "code", 453 | "execution_count": 16, 454 | "metadata": { 455 | "collapsed": true 456 | }, 457 | "outputs": [], 458 | "source": [ 459 | "y_initial = 0" 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": 17, 465 | "metadata": {}, 466 | "outputs": [ 467 | { 468 | "name": "stdout", 469 | "output_type": "stream", 470 | "text": [ 471 | "[-0.05 -0.02 -0.01 -0.001 0. 0. 0. 0. 0. 0. ]\n" 472 | ] 473 | } 474 | ], 475 | "source": [ 476 | "Y_pred = np.matmul(M,U_hist) + y_initial\n", 477 | "print(Y_pred)" 478 | ] 479 | }, 480 | { 481 | "cell_type": "markdown", 482 | "metadata": {}, 483 | "source": [ 484 | "The preceding convolution model will be wrong due to unmodelled load disturbances, nonlinearity in the models, and\n", 485 | "modelling errors. One way to correct for these is by calculating a prediction error $b$ at the current time and then adjusting the predictions. In equation form:\n", 486 | "\n", 487 | "$$ b = y_0^{actual} - y_0^{pred} $$" 488 | ] 489 | }, 490 | { 491 | "cell_type": "code", 492 | "execution_count": 18, 493 | "metadata": { 494 | "collapsed": true 495 | }, 496 | "outputs": [], 497 | "source": [ 498 | "y0_actual = 0" 499 | ] 500 | }, 501 | { 502 | "cell_type": "markdown", 503 | "metadata": {}, 504 | "source": [ 505 | "Then the vector y^{pred} is corrected by $b$ for predictions 1 to 10 as \n", 506 | "\n", 507 | "$$ y_i^{pc} = y_i^{pred} + b $$\n", 508 | "\n", 509 | "Where $y^{pc}$ is the vector of corrected predictions." 510 | ] 511 | }, 512 | { 513 | "cell_type": "code", 514 | "execution_count": 19, 515 | "metadata": { 516 | "collapsed": true 517 | }, 518 | "outputs": [], 519 | "source": [ 520 | "b = y0_actual - Y_pred[0]" 521 | ] 522 | }, 523 | { 524 | "cell_type": "markdown", 525 | "metadata": {}, 526 | "source": [ 527 | "The equation becomes\n", 528 | "\n", 529 | "\n", 530 | "$$ \n", 531 | "\\begin{align}\n", 532 | "y_1 - y_0 & = a_1 \\Delta u_0 + y_1^{pc} \\\\\n", 533 | "y_2 - y_0 & = a_2 \\Delta u_0 + a_1 \\Delta u_1 + y_2^{pc} \\\\\n", 534 | "y_3 - y_0 & = a_3 \\Delta u_0 + a_2 \\Delta u_1 + + a_1 \\Delta u_2 + y_3^{pc} \\\\\n", 535 | "y_4 - y_0 & = a_4 \\Delta u_0 + a_3 \\Delta u_1 + + a_2 \\Delta u_2 + y_4^{pc} \\\\\n", 536 | "\\vdots & \\\\\n", 537 | "y_t - y_0 & = a_t \\Delta u_0 + a_{t-1} \\Delta u_1 + + a_{t-2} \\Delta u_2 + y_t^{pc} \\\\\n", 538 | "\\end{align}\n", 539 | "$$\n", 540 | "\n", 541 | "In vector form:\n", 542 | "\n", 543 | "$$ y = y^{pc} + A\\Delta u $$" 544 | ] 545 | }, 546 | { 547 | "cell_type": "markdown", 548 | "metadata": {}, 549 | "source": [ 550 | "# Control Law\n", 551 | "\n", 552 | "From the preceding text, we have a set of\n", 553 | "future predictions of $y$ based on past moves\n", 554 | "and a set of future values of $y$ with a set of\n", 555 | "three moves. \n", 556 | "\n", 557 | "Let us now assume that we\n", 558 | "have a desired target for $y$ called $y^{SP}$. (We,\n", 559 | "of course, need this for any control action!)\n", 560 | "\n", 561 | "One criterion we can use is to minimize the\n", 562 | "error squared over the time between now\n", 563 | "and the future (that is, to use the least squares criterion). In vector equation form:\n", 564 | "\n", 565 | "$$ \\min (y^{sp} - y)^2 $$\n", 566 | "\n", 567 | "Substituting the previous equation:\n", 568 | "\n", 569 | "$$ \\min (y^{sp} - y^{pc} + A\\Delta u)^2 $$" 570 | ] 571 | }, 572 | { 573 | "cell_type": "code", 574 | "execution_count": null, 575 | "metadata": { 576 | "collapsed": true 577 | }, 578 | "outputs": [], 579 | "source": [] 580 | }, 581 | { 582 | "cell_type": "code", 583 | "execution_count": null, 584 | "metadata": { 585 | "collapsed": true 586 | }, 587 | "outputs": [], 588 | "source": [] 589 | } 590 | ], 591 | "metadata": { 592 | "kernelspec": { 593 | "display_name": "Python 3", 594 | "language": "python", 595 | "name": "python3" 596 | }, 597 | "language_info": { 598 | "codemirror_mode": { 599 | "name": "ipython", 600 | "version": 3 601 | }, 602 | "file_extension": ".py", 603 | "mimetype": "text/x-python", 604 | "name": "python", 605 | "nbconvert_exporter": "python", 606 | "pygments_lexer": "ipython3", 607 | "version": "3.6.7" 608 | } 609 | }, 610 | "nbformat": 4, 611 | "nbformat_minor": 2 612 | } 613 | -------------------------------------------------------------------------------- /DMC.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # --- 3 | # jupyter: 4 | # jupytext: 5 | # text_representation: 6 | # extension: .py 7 | # format_name: light 8 | # format_version: '1.5' 9 | # jupytext_version: 1.4.0 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # # Implementing Dynamic Matrix Control in Python 17 | # 18 | # #### Author: Siang Lim, February 2020 19 | # 20 | # ** work in progress ** 21 | # 22 | # - Contents of this notebook are derived from DMC literature: 23 | # - Hokanson, D. A., & Gerstle, J. G. (1992). **Dynamic Matrix Control Multivariable Controllers.** Practical Distillation Control, 248–271. 24 | # - Sorensen, R. C., & Cutler, C. R. (1998). **LP integrates economics into dynamic matrix control.** Hydrocarbon processing (International ed.), 77(9), 57-65. 25 | # - Morshedi, A. M., Cutler, C. R., & Skrovanek, T. A. (1985, June). **Optimal solution of dynamic matrix control with linear programing techniques (LDMC).** In 1985 American Control Conference (pp. 199-208). IEEE. 26 | 27 | import numpy as np 28 | import matplotlib.pyplot as plt 29 | 30 | # # Definitions 31 | # 32 | # #### Independent Variables 33 | # 34 | # - MV (Manipulated Variables) 35 | # - Variables that we can move directly, valves, feed rate etc. 36 | # 37 | # - FF (Feedforward Variables) 38 | # - Disturbances that we can't control directly - ambient temperature. 39 | # 40 | # #### Dependent Variables 41 | # - Variables that change due to a change in the independent variables, e.g. temperatures, pressures 42 | # 43 | 44 | # ## Step Response Convolution Model 45 | 46 | # The sequence $a_j$, where $j = 0,1,2,3 \dots$ is the step response convolution model, which is the basis for DMC. 47 | # 48 | # Remembering that everything so far is in deviation variables, at is no more than a series of numbers based on 49 | # a set length (time to steady state) and a set sampling interval. 50 | # 51 | # There is no set form of the model, other than that steady state is reached by the last interval. In fact, the last 52 | # value $a_t$ is the process steady-state gain. 53 | 54 | a = -np.array([0.5,1.0,1.5,1.8,1.9,1.95,1.98,1.99,1.999,2]) 55 | 56 | # %matplotlib inline 57 | plt.plot(a, '-or') 58 | plt.xlabel("Time (s)") 59 | plt.ylabel("Response") 60 | 61 | # ## Inputs 62 | 63 | # For a lined out process with a single step in $u$ at time 0: 64 | # 65 | # $$ \Delta u_0 = u_0 - u_{-1} $$ 66 | # 67 | # then 68 | # 69 | # $$ y_1 - y_0 = a_1 \Delta u_0 $$ 70 | # $$ y_2 - y_0 = a_2 \Delta u_0 $$ 71 | # $$ y_3 - y_0 = a_3 \Delta u_0 $$ 72 | # $$ y_4 - y_0 = a_4 \Delta u_0 $$ 73 | # $$ \vdots $$ 74 | # $$ y_t - y_0 = a_t \Delta u_0 $$ 75 | # 76 | # This is essentially the definition of the step response convolution model described before. What is interesting about this is that we can calculate what y will be at each interval for successive moves in u. 77 | # 78 | # For example purposes only, let us examine a series of three moves "into the future": one now, one at the next time interval, and one two time intervals into the future: 79 | # 80 | # $$ \Delta u_0 = u_0 - u_{-1} $$ 81 | # $$ \Delta u_1 = u_1 - u_{0} $$ 82 | # $$ \Delta u_2 = u_2 - u_{1} $$ 83 | 84 | U = np.atleast_2d(np.array([0,1,0,0,-1])).T 85 | print(U) 86 | 87 | I_obs = np.pad(np.cumsum(U),(0,8),'constant') 88 | plt.step(I_obs, '-bx', where="post") 89 | 90 | # then 91 | # 92 | # $$ 93 | # \begin{align} 94 | # y_1 - y_0 & = a_1 \Delta u_0 \\ 95 | # y_2 - y_0 & = a_2 \Delta u_0 + a_1 \Delta u_1 \\ 96 | # y_3 - y_0 & = a_3 \Delta u_0 + a_2 \Delta u_1 + + a_1 \Delta u_2 \\ 97 | # y_4 - y_0 & = a_4 \Delta u_0 + a_3 \Delta u_1 + + a_2 \Delta u_2 \\ 98 | # \vdots & \\ 99 | # y_t - y_0 & = a_t \Delta u_0 + a_{t-1} \Delta u_1 + + a_{t-2} \Delta u_2 \\ 100 | # \end{align} 101 | # $$ 102 | # 103 | 104 | # + 105 | # Build dynamic matrix 106 | t = a.shape[0] 107 | u = U.shape[0] 108 | 109 | A = np.tile(a, (u,1)).T 110 | row_indices = np.atleast_2d(np.arange(t)).T - np.arange(u) 111 | col_indices = np.arange(u) 112 | A = np.tril(A[row_indices,col_indices]) 113 | print(A) 114 | # - 115 | 116 | Y = np.multiply(A,U.T) 117 | Y_obs = np.matmul(A,U) 118 | U_obs = np.pad(np.cumsum(U),(0,a.shape[0]-U.shape[0]),'constant') 119 | 120 | Y 121 | 122 | np.nonzero(Y) 123 | 124 | np.nonzero(np.any(Y != 0, axis=0))[0] 125 | 126 | # + 127 | plt.figure(figsize=(8,6)); 128 | plt.subplot(2, 1, 1) 129 | plt.step(I_obs, '-bx', where="post") 130 | plt.subplot(2, 1, 2) 131 | plt.plot(Y_obs,'-or') 132 | 133 | # Plot non-zero observations 134 | for i in np.nonzero(np.any(Y != 0, axis=0))[0]: 135 | plt.plot(Y[:,i],'--x') 136 | # - 137 | 138 | # Unfortunately, the process is rarely at steady state when control is initiated. If we somehow knew the past 10 moves in $u$, we could get a series of 10 future predictions of $y$ formulated by extending the preceding logic 139 | # and going back in time. 140 | # 141 | # Let us call this the vector $y_p$ 142 | 143 | U_hist = np.array([1,0,0,0,-1,0,0,0,0,0]) 144 | 145 | A_hist = np.tile(a, (U_hist.shape[0], 1)).T 146 | print(A_hist) 147 | 148 | row_shift_indices = np.atleast_2d(np.arange(a.shape[0])).T + np.flip(np.arange(A_hist.shape[0])) 149 | row_shift_indices = np.triu(row_shift_indices) 150 | col_indices = np.arange(A_hist.shape[0]) 151 | 152 | M = A_hist[row_shift_indices,col_indices] 153 | M[np.tril_indices(M.shape[0])] = a[-1] 154 | print(M) 155 | 156 | y_initial = 0 157 | 158 | Y_pred = np.matmul(M,U_hist) + y_initial 159 | print(Y_pred) 160 | 161 | # The preceding convolution model will be wrong due to unmodelled load disturbances, nonlinearity in the models, and 162 | # modelling errors. One way to correct for these is by calculating a prediction error $b$ at the current time and then adjusting the predictions. In equation form: 163 | # 164 | # $$ b = y_0^{actual} - y_0^{pred} $$ 165 | 166 | y0_actual = 0 167 | 168 | # Then the vector y^{pred} is corrected by $b$ for predictions 1 to 10 as 169 | # 170 | # $$ y_i^{pc} = y_i^{pred} + b $$ 171 | # 172 | # Where $y^{pc}$ is the vector of corrected predictions. 173 | 174 | b = y0_actual - Y_pred[0] 175 | 176 | # The equation becomes 177 | # 178 | # 179 | # $$ 180 | # \begin{align} 181 | # y_1 - y_0 & = a_1 \Delta u_0 + y_1^{pc} \\ 182 | # y_2 - y_0 & = a_2 \Delta u_0 + a_1 \Delta u_1 + y_2^{pc} \\ 183 | # y_3 - y_0 & = a_3 \Delta u_0 + a_2 \Delta u_1 + + a_1 \Delta u_2 + y_3^{pc} \\ 184 | # y_4 - y_0 & = a_4 \Delta u_0 + a_3 \Delta u_1 + + a_2 \Delta u_2 + y_4^{pc} \\ 185 | # \vdots & \\ 186 | # y_t - y_0 & = a_t \Delta u_0 + a_{t-1} \Delta u_1 + + a_{t-2} \Delta u_2 + y_t^{pc} \\ 187 | # \end{align} 188 | # $$ 189 | # 190 | # In vector form: 191 | # 192 | # $$ y = y^{pc} + A\Delta u $$ 193 | 194 | # # Control Law 195 | # 196 | # From the preceding text, we have a set of 197 | # future predictions of $y$ based on past moves 198 | # and a set of future values of $y$ with a set of 199 | # three moves. 200 | # 201 | # Let us now assume that we 202 | # have a desired target for $y$ called $y^{SP}$. (We, 203 | # of course, need this for any control action!) 204 | # 205 | # One criterion we can use is to minimize the 206 | # error squared over the time between now 207 | # and the future (that is, to use the least squares criterion). In vector equation form: 208 | # 209 | # $$ \min (y^{sp} - y)^2 $$ 210 | # 211 | # Substituting the previous equation: 212 | # 213 | # $$ \min (y^{sp} - y^{pc} + A\Delta u)^2 $$ 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DMC-Python 2 | 3 | A series of educational notebook tutorials for implementing Dynamic Matrix Control (DMC) from scratch in Python. 4 | 5 | ![main.png](main.png) 6 | 7 | ## Usage 8 | 9 | `!git clone https://github.com/csianglim/DMC-Python.git` 10 | 11 | `!pip install -r 'DMC-Python/requirements.txt'` 12 | 13 | Note to developers: `requirements.txt` is generated automatically using `ipyreqsnb`. -------------------------------------------------------------------------------- /WhiskasModel.lp: -------------------------------------------------------------------------------- 1 | \* The_Whiskas_Problem *\ 2 | Minimize 3 | Total_cost_of_ingredients_per_100g_bag_of_Whiskas: 0.008 BeefPercent 4 | + 0.013 ChickenPercent 5 | Subject To 6 | FatRequirement: 0.1 BeefPercent + 0.08 ChickenPercent >= 6 7 | FibreRequirement: 0.005 BeefPercent + 0.001 ChickenPercent <= 2 8 | PercentagesSum: BeefPercent + ChickenPercent = 100 9 | ProteinRequirement: 0.2 BeefPercent + 0.1 ChickenPercent >= 8 10 | SaltRequirement: 0.005 BeefPercent + 0.002 ChickenPercent <= 0.4 11 | End 12 | -------------------------------------------------------------------------------- /assets/img/debut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csianglim/DMC-Python/c68b4c0cc069ccda81587023ac940b40d013dc38/assets/img/debut.png -------------------------------------------------------------------------------- /assets/img/ranade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csianglim/DMC-Python/c68b4c0cc069ccda81587023ac940b40d013dc38/assets/img/ranade.png -------------------------------------------------------------------------------- /main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csianglim/DMC-Python/c68b4c0cc069ccda81587023ac940b40d013dc38/main.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ipywidgets==7.7.0 2 | matplotlib==3.5.2 3 | numpy==1.21.6 4 | pandas==1.3.5 5 | pulp==2.6.0 6 | ipympl==0.9.1 7 | -------------------------------------------------------------------------------- /visualize_lp.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 16, 6 | "id": "04ae301a", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import numpy as np\n", 11 | "limits = 10\n", 12 | "d = np.linspace(-limits, limits, 200)\n", 13 | "x,y = np.meshgrid(d,d)" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 17, 19 | "id": "ae3ed735", 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "G11 = -0.200\n", 24 | "G12 = -0.072\n", 25 | "G21 = 0.125\n", 26 | "G22 = -0.954\n", 27 | "\n", 28 | "CV1Lo = -1\n", 29 | "CV1Hi = 1\n", 30 | "\n", 31 | "CV2Lo = -2\n", 32 | "CV2Hi = 2.5\n" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 18, 38 | "id": "5f8c6d07", 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "c1 = G11*x+G12*y <= CV1Hi\n", 43 | "y_c1 = (CV1Hi - G11*d)/G12" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 27, 49 | "id": "7eec6df2", 50 | "metadata": {}, 51 | "outputs": [ 52 | { 53 | "data": { 54 | "text/plain": [ 55 | "" 56 | ] 57 | }, 58 | "execution_count": 27, 59 | "metadata": {}, 60 | "output_type": "execute_result" 61 | }, 62 | { 63 | "data": { 64 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAU4AAAEzCAYAAABe7+p2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAAAZJUlEQVR4nO3dfYwdd33v8fdn5px9frbXeTaEEoWGqqSwcqFNq9CEkFhcXCramj/a8CBtoY1UdG/VhhspRaBKl/ZSpDYUy4WotOJC+mSwwCFxKFWKVEKcyE6cp8ZJg+LFJA5J/JA4dpx87x9n3B6fnLPe2XPmzDm7n5e02jkzv53z3dmZz87M+c2MIgIzM1u6pOwCzMz6jYPTzCwnB6eZWU4OTjOznBycZmY5OTjNzHLqSHBKulnS05L21o2bkbRT0qPZ9+kWP3tt1uZRSdd2oh4zsyJ1ao/zb4CrG8ZdD3wnIi4CvpO9Po2kGeCPgZ8HNgB/3Cpgzcx6RUeCMyLuBJ5tGL0J+HI2/GXgV5v86LuBnRHxbEQ8B+zktQFsZtZTijzHeVZEHMiGfwyc1aTNecCTda/3Z+PMzHpWpRtvEhEhqa1rOyXNA/MAo6Ojb3vTm97UkdpePvkq4MtOzVaL+/bsfiYiZtuZR5HB+ZSkcyLigKRzgKebtFkALq97fT7wr81mFhFbga0Ac3NzcffduzpQYvDSyVc5cuxlXjl+vAPzM7Ned+66qR+2O48iD9W3A6c+Jb8W+EaTNrcBV0mazj4Uuiob1yViqJKSpu6VZWZL16nuSF8F/h24WNJ+SR8B/g/wLkmPAldmr5E0J+mLABHxLPBp4O7s61PZODOznqV+vK1c5w7VayKCE6+8ynPPH+3YPM2sN527buqeiJhrZx4+RgUkkUhll2FmfcLBaWaWk4PTzCwnB2emkorZmQlIu9K11cz6mIMzI4R7JZnZUjgqzMxycnCameXk4DyNWDs5zMDIcNmFmFkPc3A2qCQJaeI+nWbWmoPTzCwnB6eZWU4OzibGBlImJkfLLsPMepR7ezeRJgmD+DynmTXnPU4zs5wcnGZmOTk4W0hE7Tynr103swYOzhYkMVKtoMSLyMxO51QwM8vJwXkGsxNDjIyPlF2GmfUQB+cZJBKpH6thZnUcnGZmOTk4zcxycnAuwXA1Zc30eNllmFmPcCfFJUik2r8YAf33GHoz6zDvcZqZ5VRocEq6WNLuuq/Dkj7e0OZySYfq2txYZE1mZu0q9FA9Ih4BLgWQlAILwLYmTf8tIt5TZC3tkmDN1DjPHj1OvHyi7HLMrETdPFS/AngsIn7YxffsGCGqaULix2qYrXrdDM7NwFdbTHuHpD2SbpX05i7WZGaWW1eCU9IA8F7gH5pMvhd4XUS8BfhL4Ost5jEvaZekXQcPHiysVjOzM+nWHuc1wL0R8VTjhIg4HBFHs+EdQFXS2ibttkbEXETMzc7OFl9xC1MjAwyN+tp1s9WsW8H5AVocpks6W6pdDC5pQ1bTT7pUV27VNGGo6l5cZqtZ4R3gJY0C7wJ+p27cRwEiYgvwfuBjkk4Cx4DNEeFu5mbWswoPzoh4AVjTMG5L3fBNwE1F19FJg5WE2ZkJDj53BJzxZquOjzmXQQjfGN5s9fLmb2aWk4PTzCwnB+cyCZidHqcyOFR2KWbWZb6t3LKJNAH5X4/ZquPN3swsJwenmVlODs42TQ0PMD4xWnYZZtZFDs42pYkYSL0YzVYTb/FmZjk5OM3McnJwdkCaiOmpMXwdptnq4C29AxKJgcqp5web2Urn4DQzy8nBaWaWk4OzQwRMTgyTDg6WXYqZFczB2TFiuJJSraRlF2JmBXNwmpnl5ODssImhCjPT42WXYWYFcnB2WCKRuFeS2Yrm4DQzy8nBaWaWk4OzAGki1s6MQ+ob7JutRA7OAghRSRIkn+w0W4kcnGZmORUenJKekHS/pN2SdjWZLkl/IWmfpPskvbXomszM2tGtk3DvjIhnWky7Brgo+/p54AvZ9743Mz7EkeMVTrx4rOxSzKyDeuFQfRPwt1HzfWBK0jllF9UJ1TSh6sdqmK043diqA7hd0j2S5ptMPw94su71/mycmVlP6sah+mURsSBpHbBT0sMRcWfemWShOw+wfv36TtdoZrZkhe9xRsRC9v1pYBuwoaHJAnBB3evzs3GN89kaEXMRMTc7O1tUuR03XE38+GCzFabQ4JQ0Kmn81DBwFbC3odl24LezT9ffDhyKiANF1tVNlSRhqOpbzZmtJEUfqp8FbMs6gleA/xcR35b0UYCI2ALsADYC+4AXgQ8VXFM5JIgouwoz64BCgzMiHgfe0mT8lrrhAH6vyDrKlgrWzYzzzOGXePXlE2WXY2Zt8sXUXZHdas5XYJqtCO5kaGaWk4PTzCwnB2cXrRkfYnTcXZPM+p2Ds4tSCV+Badb/vBmbmeXk4DQzy8nB2WVDlbT2+GDfHd6sb7kfZ5clEhX/uzLra96EzcxycnCameXk4CyBBNOToyQDA2WXYmbL4OAsgRCDlZQk8eI360fecs3McnJwmpnl5OAs0cRwlcHRkbLLMLOc3I+zRANpQlSD42UXYma5eI/TzCwnB2fJBioJszMTIP8pzPqFt9aSCZEk+LEaZn3EwWlmlpOD08wsJwdnDxCwdmqUytBQ2aWY2RK4O1JPEJVEJIlPdJr1A+9xmpnlVFhwSrpA0nclPSjpAUm/36TN5ZIOSdqdfd1YVD1mZp1S5KH6SeB/RcS9ksaBeyTtjIgHG9r9W0S8p8A6+sbkUIWXKqMcOfxC2aWY2SIK2+OMiAMRcW82fAR4CDivqPdbCdIkYcDPDzbreV3ZSiW9Hvg54K4mk98haY+kWyW9uRv1mJm1o/BP1SWNAf8EfDwiDjdMvhd4XUQclbQR+DpwUYv5zAPzAOvXry+uYDOzMyh0j1NSlVpofiUi/rlxekQcjoij2fAOoCppbbN5RcTWiJiLiLnZ2dkiyy5VmoipqTFI0rJLMbMWivxUXcCXgIci4s9btDk7a4ekDVk9Pymqpn6QSAxVEj933ayHFXmo/ovAbwH3S9qdjfvfwHqAiNgCvB/4mKSTwDFgc0REgTX1EQenWa8qLDgj4nucYeuPiJuAm4qqoX+JdVPDHH6pyksvvFh2MWbWwH1felQi+WjdrEc5OM3McnJwmpnl5ODsYeODFWamx8suw8wa+LZyPSyRSH2e06zneI/TzCwnB6eZWU4Ozh6XJLBmehwq1bJLMbOMg7PHCVFNE+ROnWY9w8FpZpaTg9PMLCcHZ5+YGRtkcGS47DLMDAdn36imCQMV/7nMeoG3RDOznBycZmY5OTj7yGAlYWxitOwyzFY9B2cfqSQJI9XUN4c3K5mDsy85Oc3K5ODsM4lgdmacZGCw7FLMVi3fVq7v1G415yswzcrjPU4zs5wcnGZmOTk4+9TM6CCj4+6aZFYGB2efShNR9XM1zErh4DQzy6nw4JR0taRHJO2TdH2T6YOSbsmm3yXp9UXXZGbWjkKDU1IKfB64BrgE+ICkSxqafQR4LiLeCHwO+EyRNa0kA2nC9NSY+yaZdVnRe5wbgH0R8XhEnAC+BmxqaLMJ+HI2/I/AFfJzIpYkkXyrObMSFL3VnQc8Wfd6fzauaZuIOAkcAtYUXJeZ2bL1ze6KpHlJuyTtOnjwYNnlmNkqVnRwLgAX1L0+PxvXtI2kCjAJ/KRxRhGxNSLmImJudna2oHL709TkqK9dN+uiooPzbuAiSRdKGgA2A9sb2mwHrs2G3w/8S0REwXWtGEIMVVJS9+k065pCb/IRESclXQfcBqTAzRHxgKRPAbsiYjvwJeDvJO0DnqUWrmZmPavwuyNFxA5gR8O4G+uGXwJ+veg6VrqZkQGOD1Z4/vmjZZdituL1zYdDtjhJpO7FZdYVDk4zs5wcnGZmOTk4V5BKKmZnxiFJyy7FbEXzozNWECES/ys0K5w3MzOznBycZmY5OThXGCHWTo5QHR4quxSzFcvBuQJV0oTUJzvNCuOty8wsJwenmVlODs4VamwwZXzCjw82K4KDc4WqJAmDfqyGWSG8ZZmZ5eTgNDPLycG5giUSE5OjvnbdrMMcnCtYIjFSrSD36TTrKG9RZmY5OThXgdnJYYbHRsouw2zFcHCuAolE4sdqmHWMg9PMLCcHp5lZTg7OVWJ0IGVmarzsMsxWBD86Y5VIJFJ35zTrCO9xmpnlVMgep6Q/A/4HcAJ4DPhQRDzfpN0TwBHgFeBkRMwVUY+ZWScVtce5E/iZiPhZ4D+ATyzS9p0RcalDs3iJYGZ6HFWqZZdi1tcKCc6IuD0iTmYvvw+cX8T7WD5CDKQJStyn06wd3TjH+WHg1hbTArhd0j2S5rtQi5lZ25Z9jlPSHcDZTSbdEBHfyNrcAJwEvtJiNpdFxIKkdcBOSQ9HxJ0t3m8emAdYv379css2M2vbsoMzIq5cbLqkDwLvAa6IiGgxj4Xs+9OStgEbgKbBGRFbga0Ac3NzTednSzM1OsgLlZTjLx4ruxSzvlTIobqkq4E/BN4bES+2aDMqafzUMHAVsLeIeux0A2nCYNU90cyWq6it5yZgnNrh925JWwAknStpR9bmLOB7kvYAPwC+FRHfLqgea+CPh8yWr5B+nBHxxhbjfwRszIYfB95SxPvbmQ1VUwZmJjj43BFofibFzFrw8doqJYTvNGe2PA5OM7OcHJxmZjk5OFexRDA7PU46OFh2KWZ9xbeVW9VEmoB8stMsF+9xmpnl5OA0M8vJwWlMjwwwNj5adhlmfcPBaaSJqKY+z2m2VA5OM7OcHJxmZjk5OA2AapowPTUG8iphdibeSgyoPT54oJL4tklmS+DgNDPLycFpZpaTg9P+i4DJ8RGSAV+7brYYB6fVEcPVlErFq4XZYryFmJnl5OC015garta6JplZUw5Oe41EIvGt5sxacnCameXk4DQzy8nBaU1VUrF2ZhyStOxSzHqOH51hTQlRScDPEDZ7Le9xmpnlVFhwSvqkpAVJu7OvjS3aXS3pEUn7JF1fVD1mZp1S9KH65yLi/7aaKCkFPg+8C9gP3C1pe0Q8WHBdtiRizcQwh186ycvHjpVdjFnPKPtQfQOwLyIej4gTwNeATSXXZHWqaULFj9UwO03RwXmdpPsk3Sxpusn084An617vz8aZmfWstoJT0h2S9jb52gR8Afgp4FLgAPDZNt9rXtIuSbsOHjzYzqzMzNrS1jnOiLhyKe0k/TXwzSaTFoAL6l6fn41r9l5bga0Ac3Nzka9Sa8foQEo6McrRwy+UXYpZTyjyU/Vz6l6+D9jbpNndwEWSLpQ0AGwGthdVky1PJUkYrrgjvNkpRX6q/qeSLgUCeAL4HQBJ5wJfjIiNEXFS0nXAbUAK3BwRDxRYk7VD1P6aZqtcYcEZEb/VYvyPgI11r3cAO4qqwzojSWDdzAQHDx0jTr5cdjlmpfIll7YkQrWrL30Jplnp/TjNzPqOg9PMLCcHp+WydmKI4bGRssswK5WD03JJJSqJz3Pa6ubgNDPLycFpZpaTg9NyG66mfnywrWoOTsstkaimSe1KIrNVyMFpZpaTg9PMLCcHpy2LBNOTY6g6UHYpZl3n4LRlEWKwkpK4T6etQg5OM7OcHJxmZjk5OK0tk8NVBkeGyy7DrKscnNaWgUrK8IAfq2Gri4PTzCwnB6e1bbCSMDsz4bvD26rh4LS2CZF4TbJVxKu7mVlODk4zs5wcnNYRAtZOj5EODpVdilnhHJzWIaKSJD7XaauCV3Mzs5wqRcxU0i3AxdnLKeD5iLi0SbsngCPAK8DJiJgroh4zs04qJDgj4jdPDUv6LHBokebvjIhniqjDum9yuMpL1ZSjh18ouxSzwhQSnKdIEvAbwK8U+T7WOypJwmAKR8suxKxARZ/j/CXgqYh4tMX0AG6XdI+k+YJrMTPriGXvcUq6Azi7yaQbIuIb2fAHgK8uMpvLImJB0jpgp6SHI+LOFu83D8wDrF+/frllm5m1bdnBGRFXLjZdUgX4NeBti8xjIfv+tKRtwAagaXBGxFZgK8Dc3Fwss2zrgjQRk1NjHDr8Irz6atnlmHVckYfqVwIPR8T+ZhMljUoaPzUMXAXsLbAe65JEYqiS4OcH20pVZHBupuEwXdK5knZkL88CvidpD/AD4FsR8e0C67Fu892SbIUq7FP1iPhgk3E/AjZmw48Dbynq/a1cQqybGuH5Yy9z4sVjZZdj1lG+csgKk0gk3uu0FcjBaWaWk4PTzCwnB6cVamKowvTUWNllmHWUg9MKlUikic9z2sri4DQzy8nBaWaWk4PTCpcmYs30OKSF3ozLrGscnFY4Iappgtyn01YIB6eZWU4OTjOznByc1jUz40MMDA+XXYZZ2xyc1jXVNKFa8Spn/c9rsZlZTg5OM7OcHJzWVcPVhNHx0bLLMGuLg9O6qpIkjAykZZdh1hYHp5lZTg5O67pEMLtmAlUHyi7FbFkcnNZ1QqSSn+VmfcvBaWaWk4PTzCwnB6eVZs3YECNjI2WXYZabg9NKkyaikvpEp/UfB6eZWU5tBaekX5f0gKRXJc01TPuEpH2SHpH07hY/f6Gku7J2t0hy/xQz63nt7nHuBX4NuLN+pKRLgM3Am4Grgb+S1Oxykc8An4uINwLPAR9psx7rM0OVtPb4YPdNsj7SVnBGxEMR8UiTSZuAr0XE8Yj4T2AfsKG+gWrPUfgV4B+zUV8GfrWdeqz/JKo9VsOsnxS1xp4HPFn3en82rt4a4PmIOLlIGzOznnPGxw5KugM4u8mkGyLiG50vqWUd88B89vJ4kmhvt957EWuBZ8ouAtfRyHWcznWc7uJ2Z3DG4IyIK5cx3wXggrrX52fj6v0EmJJUyfY6m7Wpr2MrsBVA0q6ImGvVtltch+twHf1ZR7vzKOpQfTuwWdKgpAuBi4Af1DeIiAC+C7w/G3Ut0LU9WDOz5Wq3O9L7JO0H3gF8S9JtABHxAPD3wIPAt4Hfi4hXsp/ZIencbBZ/BPxPSfuonfP8Ujv1mJl1wxkP1RcTEduAbS2m/QnwJ03Gb6wbfpyGT9uXaOsyfqYIruN0ruN0ruN0K6YO1Y6YzcxsqdyBzswsp54Nzl68nDObz+7s6wlJu1u0e0LS/Vm7tj/BazL/T0paqKtlY4t2V2fLaJ+k6wuo488kPSzpPknbJE21aNfx5XGm3y37YPKWbPpdkl7fifdteI8LJH1X0oPZuvr7TdpcLulQ3d/qxk7Xkb3PostYNX+RLY/7JL21gBourvs9d0s6LOnjDW0KWx6Sbpb0tPTfXRUlzUjaKenR7Pt0i5+9NmvzqKRrz/hmEdGTX8BPU+tv9a/AXN34S4A9wCBwIfAYkDb5+b8HNmfDW4CPdbi+zwI3tpj2BLC2wGXzSeAPztAmzZbNG4CBbJld0uE6rgIq2fBngM90Y3ks5XcDfhfYkg1vBm4p4O9wDvDWbHgc+I8mdVwOfLOodWGpyxjYCNwKCHg7cFfB9aTAj4HXdWt5AL8MvBXYWzfuT4Hrs+Hrm62jwAzwePZ9OhueXuy9enaPM3r4cs5s/r8BfLVT8yzABmBfRDweESeAr1Fbdh0TEbfHf1/59X1qfXG7YSm/2yZqf3eorQdXZH+3jomIAxFxbzZ8BHiI3r36bRPwt1HzfWp9qM8p8P2uAB6LiB8W+B6niYg7gWcbRtevB61y4N3Azoh4NiKeA3ZSu8dGSz0bnIvohcs5fwl4KiIebTE9gNsl3ZNd8VSE67JDrptbHH4sZTl10oep7dE00+nlsZTf7b/aZOvBIWrrRSGyUwE/B9zVZPI7JO2RdKukNxdUwpmWcbfXh8203rHoxvI45ayIOJAN/xg4q0mb3Mumre5I7VKPXM5Zb4k1fYDF9zYvi4gFSeuAnZIezv4bdqQO4AvAp6ltLJ+mdtrgw3nm34k6Ti0PSTcAJ4GvtJhN28ujl0kaA/4J+HhEHG6YfC+1w9Wj2bnor1O7IKTTemYZZ58nvBf4RJPJ3VoerxERIakj3YhKDc7okcs589QkqULtVnpvW2QeC9n3pyVto3ZomWslXuqykfTXwDebTFrKcmq7DkkfBN4DXBHZCaMm82h7eTRYyu92qs3+7G82SW296ChJVWqh+ZWI+OfG6fVBGhE7JP2VpLUR0dFrtpewjDuyPizRNcC9EfFUkzq7sjzqPCXpnIg4kJ2aeLpJmwVq515POZ/aZyst9eOhetmXc14JPBwR+5tNlDQqafzUMLUPUDp6Q5KGc1PvazH/u4GLVOtdMEDt0Gl7h+u4GvhD4L0R8WKLNkUsj6X8btup/d2hth78S6tgX67snOmXgIci4s9btDn71LlVSRuobXMdDfAlLuPtwG9nn66/HThUdwjbaS2PyLqxPBrUrwetcuA24CpJ09lpr6uyca0V8elWhz4hex+1cw3HgaeA2+qm3UDtU9VHgGvqxu8Azs2G30AtUPcB/wAMdqiuvwE+2jDuXGBH3fvuyb4eoHZI2+ll83fA/cB92YpxTmMd2euN1D7pfaygOvZROze0O/va0lhHUcuj2e8GfIpaiAMMZX/3fdl68IYCfv/LqJ0uua9uGWwEPnpqHQGuy37vPdQ+QPuFAupouowb6hDw+Wx53U9dT5UO1zJKLQgn68Z1ZXlQC+sDwMtZdnyE2nnt7wCPAncAM1nbOeCLdT/74Wxd2Qd86Ezv5SuHzMxy6sdDdTOzUjk4zcxycnCameXk4DQzy8nBaWaWk4PTzCwnB6eZWU4OTjOznP4/hCDOz0d4UoEAAAAASUVORK5CYII=\n", 65 | "text/plain": [ 66 | "
" 67 | ] 68 | }, 69 | "metadata": { 70 | "needs_background": "light" 71 | }, 72 | "output_type": "display_data" 73 | } 74 | ], 75 | "source": [ 76 | "import matplotlib.pyplot as plt\n", 77 | "plt.figure(figsize=(5,5))\n", 78 | "plt.imshow(c1.astype(int), extent=(x.min(),x.max(),y.min(),y.max()), origin=\"lower\", cmap=\"Blues\", alpha=0.1)" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 29, 84 | "id": "962a58c0", 85 | "metadata": {}, 86 | "outputs": [ 87 | { 88 | "data": { 89 | "text/plain": [ 90 | "(-10.0, 10.0)" 91 | ] 92 | }, 93 | "execution_count": 29, 94 | "metadata": {}, 95 | "output_type": "execute_result" 96 | }, 97 | { 98 | "data": { 99 | "image/png": "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\n", 100 | "text/plain": [ 101 | "
" 102 | ] 103 | }, 104 | "metadata": { 105 | "needs_background": "light" 106 | }, 107 | "output_type": "display_data" 108 | } 109 | ], 110 | "source": [ 111 | "plt.figure(figsize=(5,5))\n", 112 | "plt.plot(d, y_c1)\n", 113 | "plt.xlim([-limits, limits])\n", 114 | "plt.ylim([-limits, limits])" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 30, 120 | "id": "5a8cc977", 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "data": { 125 | "text/plain": [ 126 | "array([ 13.88888889, 13.60971524, 13.3305416 , 13.05136795,\n", 127 | " 12.7721943 , 12.49302066, 12.21384701, 11.93467337,\n", 128 | " 11.65549972, 11.37632607, 11.09715243, 10.81797878,\n", 129 | " 10.53880514, 10.25963149, 9.98045784, 9.7012842 ,\n", 130 | " 9.42211055, 9.14293691, 8.86376326, 8.58458961,\n", 131 | " 8.30541597, 8.02624232, 7.74706868, 7.46789503,\n", 132 | " 7.18872138, 6.90954774, 6.63037409, 6.35120045,\n", 133 | " 6.0720268 , 5.79285315, 5.51367951, 5.23450586,\n", 134 | " 4.95533222, 4.67615857, 4.39698492, 4.11781128,\n", 135 | " 3.83863763, 3.55946399, 3.28029034, 3.00111669,\n", 136 | " 2.72194305, 2.4427694 , 2.16359576, 1.88442211,\n", 137 | " 1.60524846, 1.32607482, 1.04690117, 0.76772753,\n", 138 | " 0.48855388, 0.20938023, -0.06979341, -0.34896706,\n", 139 | " -0.6281407 , -0.90731435, -1.186488 , -1.46566164,\n", 140 | " -1.74483529, -2.02400893, -2.30318258, -2.58235623,\n", 141 | " -2.86152987, -3.14070352, -3.41987716, -3.69905081,\n", 142 | " -3.97822446, -4.2573981 , -4.53657175, -4.81574539,\n", 143 | " -5.09491904, -5.37409269, -5.65326633, -5.93243998,\n", 144 | " -6.21161362, -6.49078727, -6.76996092, -7.04913456,\n", 145 | " -7.32830821, -7.60748185, -7.8866555 , -8.16582915,\n", 146 | " -8.44500279, -8.72417644, -9.00335008, -9.28252373,\n", 147 | " -9.56169738, -9.84087102, -10.12004467, -10.39921831,\n", 148 | " -10.67839196, -10.95756561, -11.23673925, -11.5159129 ,\n", 149 | " -11.79508654, -12.07426019, -12.35343384, -12.63260748,\n", 150 | " -12.91178113, -13.19095477, -13.47012842, -13.74930207,\n", 151 | " -14.02847571, -14.30764936, -14.586823 , -14.86599665,\n", 152 | " -15.1451703 , -15.42434394, -15.70351759, -15.98269123,\n", 153 | " -16.26186488, -16.54103853, -16.82021217, -17.09938582,\n", 154 | " -17.37855946, -17.65773311, -17.93690676, -18.2160804 ,\n", 155 | " -18.49525405, -18.77442769, -19.05360134, -19.33277499,\n", 156 | " -19.61194863, -19.89112228, -20.17029592, -20.44946957,\n", 157 | " -20.72864322, -21.00781686, -21.28699051, -21.56616415,\n", 158 | " -21.8453378 , -22.12451145, -22.40368509, -22.68285874,\n", 159 | " -22.96203238, -23.24120603, -23.52037968, -23.79955332,\n", 160 | " -24.07872697, -24.35790061, -24.63707426, -24.91624791,\n", 161 | " -25.19542155, -25.4745952 , -25.75376884, -26.03294249,\n", 162 | " -26.31211614, -26.59128978, -26.87046343, -27.14963707,\n", 163 | " -27.42881072, -27.70798437, -27.98715801, -28.26633166,\n", 164 | " -28.5455053 , -28.82467895, -29.1038526 , -29.38302624,\n", 165 | " -29.66219989, -29.94137353, -30.22054718, -30.49972083,\n", 166 | " -30.77889447, -31.05806812, -31.33724176, -31.61641541,\n", 167 | " -31.89558906, -32.1747627 , -32.45393635, -32.73310999,\n", 168 | " -33.01228364, -33.29145729, -33.57063093, -33.84980458,\n", 169 | " -34.12897822, -34.40815187, -34.68732552, -34.96649916,\n", 170 | " -35.24567281, -35.52484645, -35.8040201 , -36.08319375,\n", 171 | " -36.36236739, -36.64154104, -36.92071468, -37.19988833,\n", 172 | " -37.47906198, -37.75823562, -38.03740927, -38.31658291,\n", 173 | " -38.59575656, -38.87493021, -39.15410385, -39.4332775 ,\n", 174 | " -39.71245114, -39.99162479, -40.27079844, -40.54997208,\n", 175 | " -40.82914573, -41.10831937, -41.38749302, -41.66666667])" 176 | ] 177 | }, 178 | "execution_count": 30, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | } 182 | ], 183 | "source": [ 184 | "y_c1" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": null, 190 | "id": "be279192", 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "def visualize_lp(contraints, limits=10):\n", 195 | " # standard figure parameters (constraints)\n", 196 | " limits = 10\n", 197 | " d = np.linspace(-limits, limits, 100)\n", 198 | " x,y = np.meshgrid(d,d)\n", 199 | "\n", 200 | " # constraint formulation in terms of x and ya\n", 201 | " c1 = G11*x+G12*y <= CV1Hi\n", 202 | " c2 = G11*x+G12*y >= CV1Lo\n", 203 | " c3 = G21*x+G22*y <= CV2Hi\n", 204 | " c4 = G21*x+G22*y >= CV2Lo\n", 205 | "\n", 206 | " # equation of a line, y = mx + c\n", 207 | " y_c1 = (CV1Hi - G11*d)/G12\n", 208 | " y_c2 = (CV1Lo - G11*d)/G12\n", 209 | " y_c3 = (CV2Hi - G21*d)/G22\n", 210 | " y_c4 = (CV2Lo - G21*d)/G22 " 211 | ] 212 | } 213 | ], 214 | "metadata": { 215 | "kernelspec": { 216 | "display_name": "Python 3 (ipykernel)", 217 | "language": "python", 218 | "name": "python3" 219 | }, 220 | "language_info": { 221 | "codemirror_mode": { 222 | "name": "ipython", 223 | "version": 3 224 | }, 225 | "file_extension": ".py", 226 | "mimetype": "text/x-python", 227 | "name": "python", 228 | "nbconvert_exporter": "python", 229 | "pygments_lexer": "ipython3", 230 | "version": "3.8.8" 231 | } 232 | }, 233 | "nbformat": 4, 234 | "nbformat_minor": 5 235 | } 236 | --------------------------------------------------------------------------------