├── .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": "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\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 | 
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": "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\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 |
--------------------------------------------------------------------------------