Introduction to Chemical Engineering Computation with MATLAB ® Andrew S. Paluch, PhD Department of Chemical, Paper and Biomedical Engineering Miami University Oxford, Ohio USA PaluchAS@MiamiOH.edu December 3, 2021 The present text was inspired by and is a derivative of the text: “Physical Modeling in MATLAB® ”, version 1.1.8, by Allen B. Downey. The latest version of the book is available at http://greenteapress.com/wp/ physical-modeling-in-matlab/ and is published by Green Tea Press. Permission is granted to copy, distribute, and/or modify this document under the terms of the Creative Commons Attribution-NonCommercial 3.0 Unported License, which is available at http://creativecommons. org/licenses/by-nc/3.0/. This is the license under which “Physical Modeling in MATLAB® ” was originally published. Preface When I joined the faculty of the Department of Chemical, Paper and Biomedical Engineering at Miami University in Fall 2013, I was tasked with developing a computational methods course for our chemical and bio- engineering majors. I was encouraged to design the course around the use of MATLAB® ; our bioengineering students take a sequence of courses in electrical engineering which extensively use MATLAB® , and a comparable course taught in mechanical and manufacturing engineering also uses MATLAB® . The course created was CPB 324 Chemical and Bio- Engineering Computation and Statistics. With the time required for the new course to be approved by the university, and then to become a required course in our chemical and bio- engineering curriculum (which also requires university approval), I did not teach my first section of the course until Spring 2017. During my time at Miami University I have taught a total of 6 core chemical courses: fluid mechanics, mass transfer, thermodynamics 1 and 2, reactor design, and chemical engineering process design. I also work alongside undergraduate students in my computational thermodynamics laboratory, have attended workshops on e-learning and “open” textbooks, and taught a class at Universidade Federal Fluminense (UFF), in Niterói, Rio de Janerio, Brazil. All of these experiences have shaped the current version of this course, which differs from my original vision. Every semester I teach the course I continue to modify the content in an attempt to optimize student success. Beginning with the first offering of this course, I have kept four major concepts in mind: 1. Assume that the students in the class have never programmed before, and have no previous experience with MATLAB® . While students at Miami University are introduced to MATLAB® during the their first year in CPB 102 Introduction to Chemical and Bio- Engineering, the introduction is brief and students are too often overwhelmed with their first year studies such that it is as if the encounter never happened. 2. It is not my goal to teach everything there is to know about MATLAB® , or everything that will be encountered in the Chemical and Bio- Engineering curriculum in a single semester. My goal is to teach MATLAB® so that my students understand and appreciate it... and do not hate MATLAB® by the iii iv end of the semester! By teaching less breadth, I can cover material in greater depth, which I believe gives my students the skills and confidence for additional self-study of new material. Material that I can add to this text in the future. Also, for any material that I am unable to cover, the excellent documentation packaged with MATLAB® will always be available too. 3. To create an “open” (free) textbook for the class. This format allows me to: lower the barrier of education, allowing anyone and everyone to obtain a copy; completely customize the text for class; and to create a resource for future classes and future endeavors. Our students are asked to learn a tremendous amount of material in a short period of time. While a year after completing the course I can not expect them to remember everything, what is important is they know where to turn for help. They will always have access to the latest version of this text, which I plan to grow and improve every semester. 4. And lastly, to teach the course material so that students are encouraged to use MATLAB® in their future course studies. Too often engineers are associated with making assumptions in order to solve problems. While this may have been necessary fifty years ago, it need not necessarily be the case today. With computational tools we may solve problems that we would never imagine solving otherwise. Additionally, as a student, mathematical modeling with MATLAB® can be an excellent learning tool. Rather than just reading about a theory in a text, we can set-up a physical modeling, and then readily perform virtual experiments; we can change parameters and look at what the effect of each is. To encourage this, we will solve problems in the course from upper level classes, where you need not know the underlying theory, but can solve the problems using MATLAB® and can further use MATLAB® to gain insight. As mentioned, the course is designed assuming that the students in class have never programmed before. This is different from most texts that use MATLAB® , which are aimed at readers who know how to program. As a result, the order of presentation is unusual. The text starts with scalar values and works up to vectors and matrices very gradually. This approach is good for beginning programmers, because it is hard to understand composite objects until you understand basic programming semantics. But there are problems: • The MATLAB® documentation is written in terms of matrices, and so are the error messages. To mitigate this problem, the book explains the necessary vocabulary early and deciphers some of the messages that beginners find confusing. • Many of the examples in the first few chapters are not idiomatic MATLAB® . This problem is addressed in the later chapters by translating the examples into a more standard style. v The texts puts a lot of emphasis on functions, in part because they are an important mechanism for controlling program complexity, and also because they are useful for working with MATLAB® tools; we will look at fzero, fsolve, ode45, and integral in this text. However, they are additionally central to the use of many other tools students will likely encounter in their future studies; for example, fminbnd comes up in my thermodynamics course, and after completing this course I find students are readily able to use it after consulting the excellent documentation provided by MATLAB® . In teaching the course I have to be selective of the material covered. Typically, the first third of the course can be thought of as an introduction to computer programming. While we will use MATLAB® , the logic is equally applicable to other languages and programs. I find that students struggle most with this material, so we spend ample time working through examples. In last two thirds of the course we then turn and look at some of MATLAB’s® built-in functionalities. We begin with root-finding and systems of equations, and then work our way up to systems of ordinary differential equations. These are skills I wish my class had when I taught our course in reactor design. The text then moves to interpolation, which results naturally as a complement to numerically solving ordinary differential equations. We then end with numerical integration. At present, this is all that I can comfortably cover in a semester. However, recently this text has been adopted by CPB 102 Introduction to Chemical and Bio- Engineering which teaches the first few chapters. I hope that in the near future this will allow me to add additional material on optimization, curve fitting, and symbolic calculations. When a new numerical method is introduced, a brief amount of time will be spent writing and using our own function. This text is not a text on numerical methods. We will look at some basic numerical methods to help understand how MATLAB’s® built-in functions work. While this is good to help students appreciate MATLAB® , I find it invaluable to better understand the required inputs, the outputs, and what can go wrong. Links will be provided to external resources (typically Wikipedia and textbooks available in the Open Textbook Library) for the reader interested in learning more. While the text is formatted to be printed, I believe the electronic copy is much better. You will notice that hyperlinking is used throughout the text. First, the documentation provided by MATLAB® is invaluable. You will find that I link to it throughout the text. I emphasize the documentation is invaluable, and I encourage my students to consult it throughout the text. This not only improves student learning of the topics covered in the text, but it helps students become comfortable using the documentation in the future if they need to learn a new feature of MATLAB® . Second, in the text I provide copies of all of my M-files. Additionally, you will find that in the M-file caption, I link to a digital copy stored in my Google Drive account that you may download. Third, links are provided throughout to screen casts on my YouTube channel. I would encourage everyone using this text to subscribe. Initially, my screen casts will be of solutions to select examples and topics that frequently cause confusion, but I will continue to grow vi the channel and improve the quality of the screen casts. You may notice my solutions in the screen casts differ slightly from the text; in the screen casts I am genuinely solving the problems and offer commentary throughout. When applicable, the description of the YouTube video contains a link the my Google Drive account to download the M-file created in the video. At present I have not updated the text solutions to be identical to the screen casts to provide an additional example. At present, the text is only available as a PDF document. I have spent a great amount of time working on an HTML version. However, in the end, I believe the PDF is better. It allows me to readily share the text with anyone via a single email. And I believe the use of hyperlinking allows the text to come alive, similar to what the user experience with the HTML version might be. Before getting started have a look at my screen cast demonstrating the use of hyperlinking. Additionally in the spirit of free and accessible, I plan to evolve the text from exclusively using MATLAB® to also use freely available software. I will start with GNU Octave, which is what I actually use in my research group. At present, GNU Octave can be used throughout most of the text. The exception is I present the option of using nested functions, which GNU Octave does not support. This appears later in the text when solving systems of second order initial value ordinary differential equations. I delay the introduction of nested functions because they often cause confusion initially because I frequently repeat the message the functions have their own workspace. However, for the trajectory problems we solve, the use of nested functions greatly simplifies our programs as compared to attempting to use anonymous functions. If you have any suggestions or comments, please let me know. If you are interested in helping to develop the text and/or the YouTube channel, I would love to hear from you. If you encounter a problem in another course you would like to solve using MATLAB® let me know and material can be added to the text. Please also let me know if you would like to contribute the material to the text. Please also know that many excellent (and freely available) resources are available to you to help you learn MATLAB® . You should bookmark the online documentation. MathWorks® , the fine folks that develop MATLAB® , also have an “Academia” section on their website worth checking out. You will find many excellent MATLAB® tutorials available. You will also find that many other resources are available. For example, a free “Introduction to Programming with MATLAB” course is offered by Vanderbilt University on coursera. The course covers material from the first third of this course, and a series of very good YouTube videos are available. Many others exist, this is just an example. Lastly, I would like to acknowledge and thank Professor Allen B. Downey, Professor of Computer Science at Olin College, who has written many “open” textbooks. This text was inspired by and is a derivative of his textbook “Physical Modeling in MATLAB® ”, version 1.1.8, which is published under the terms of the Creative Commons Attribution-NonCommercial 3.0 Unported License. The text is additionally available in the Open Textbook Library, which I would encourage vii you to visit. Contents 1 2 3 Variables and values 1.1 A glorified calculator . . . . . . . 1.2 Math functions . . . . . . . . . . . 1.3 Documentation . . . . . . . . . . 1.4 Variables . . . . . . . . . . . . . . 1.5 Assignment statements . . . . . . 1.6 Why variables? . . . . . . . . . . . 1.7 Errors . . . . . . . . . . . . . . . . 1.8 Floating-point arithmetic . . . . . 1.9 Comments . . . . . . . . . . . . . 1.10 Examples . . . . . . . . . . . . . . 1.11 Chemical Engineering Examples 1.12 MATLAB Online . . . . . . . . . . 1.13 Glossary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 4 5 6 7 7 8 10 12 13 15 20 21 Scripts 2.1 M-files . . . . . . . . . . . . 2.2 Why scripts? . . . . . . . . 2.3 The workspace . . . . . . . 2.4 More errors . . . . . . . . . 2.5 Pre- and post-conditions . 2.6 Assignment and equality . 2.7 Updating variables . . . . 2.8 Incremental development 2.9 Unit testing . . . . . . . . . 2.10 Kinds of error . . . . . . . . 2.11 Absolute and relative error 2.12 CPB Examples . . . . . . . 2.13 Glossary . . . . . . . . . . . 2.14 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 27 27 29 29 30 34 35 36 37 38 38 44 45 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . for loops and basic plotting 3.1 for loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 3.3 plotting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix 49 49 53 58 x CONTENTS 3.4 3.5 3.6 3.7 3.8 3.9 3.10 4 5 Series . . . . . . . . . . . Generalization . . . . . . Examples . . . . . . . . . CPB Examples . . . . . . Creating multiple figures Glossary . . . . . . . . . . Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 61 63 69 73 76 77 Logic and vectors 4.1 Checking preconditions 4.2 if . . . . . . . . . . . . . 4.3 Relational operators . . . 4.4 Logical operators . . . . 4.5 exist . . . . . . . . . . . 4.6 Vectors . . . . . . . . . . . 4.7 Vector arithmetic . . . . 4.8 Everything is a matrix . . 4.9 Indices . . . . . . . . . . . 4.10 Indexing errors . . . . . . 4.11 Vectors and sequences . 4.12 Sizing vectors . . . . . . . 4.13 Creating vectors . . . . . 4.14 Plotting vectors . . . . . 4.15 Reduce . . . . . . . . . . 4.16 Apply . . . . . . . . . . . 4.17 Search . . . . . . . . . . . 4.18 Spoiling the fun . . . . . 4.19 Strings . . . . . . . . . . . 4.20 Examples . . . . . . . . . 4.21 CPB Examples . . . . . . 4.22 Glossary . . . . . . . . . . 4.23 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 81 82 84 85 86 88 89 89 91 92 93 96 97 100 102 103 104 107 108 108 118 132 133 Functions 5.1 Name Collisions . . . . . . . . . . . . . 5.2 Functions . . . . . . . . . . . . . . . . . 5.3 Documentation . . . . . . . . . . . . . 5.4 Function names . . . . . . . . . . . . . 5.5 Multiple input variables . . . . . . . . 5.6 Logical functions . . . . . . . . . . . . . 5.7 An incremental development example 5.8 Nested loops . . . . . . . . . . . . . . . 5.9 Conditions and flags . . . . . . . . . . 5.10 Encapsulation and generalization . . . 5.11 A misstep . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 135 136 139 140 143 146 149 149 151 153 154 CONTENTS 5.12 5.13 5.14 5.15 5.16 5.17 6 7 xi continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 Mechanism and leap of faith Examples . . . . . . . . . . . CPB Examples . . . . . . . . Glossary . . . . . . . . . . . . Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Functions of vectors 6.1 Functions and files . . . . . . . . . 6.2 Vectors as input variables . . . . . . 6.3 Vectors as output variables . . . . . 6.4 Multiple/optional output variables 6.5 Vectorizing your functions . . . . . 6.6 Sums and differences . . . . . . . . 6.7 Products and ratios . . . . . . . . . 6.8 Existential quantification . . . . . . 6.9 Universal quantification . . . . . . 6.10 Logical vectors . . . . . . . . . . . . 6.11 CPB Examples . . . . . . . . . . . . 6.12 Glossary . . . . . . . . . . . . . . . . 6.13 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zero-finding 7.1 Why functions? . . . . . . . . . . . . . . . . . . . . . . 7.2 Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.3 A note on notation . . . . . . . . . . . . . . . . . . . . 7.4 Nonlinear equations . . . . . . . . . . . . . . . . . . 7.5 Zero-finding . . . . . . . . . . . . . . . . . . . . . . . 7.6 fzero . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.7 What could go wrong? . . . . . . . . . . . . . . . . . . 7.8 Finding an initial guess . . . . . . . . . . . . . . . . . 7.9 More name collisions . . . . . . . . . . . . . . . . . . 7.10 Debugging in four acts . . . . . . . . . . . . . . . . . 7.11 Examples . . . . . . . . . . . . . . . . . . . . . . . . . 7.12 Functions: what we’ve done so far . . . . . . . . . . 7.13 Anonymous function . . . . . . . . . . . . . . . . . . 7.14 What could go wrong? . . . . . . . . . . . . . . . . . . 7.15 Saving anonymous functions . . . . . . . . . . . . . 7.16 Revisiting the duck . . . . . . . . . . . . . . . . . . . 7.17 Numerical methods: zero-finding . . . . . . . . . . . 7.17.1 The bisection method . . . . . . . . . . . . . 7.17.2 The secant (or linear interpolation) method 7.18 roots . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.19 Examples . . . . . . . . . . . . . . . . . . . . . . . . . 7.20 CPB Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158 159 160 169 170 . . . . . . . . . . . . . 173 173 177 178 180 183 185 187 190 191 192 197 200 201 . . . . . . . . . . . . . . . . . . . . . . 203 203 204 204 205 206 207 209 211 214 216 217 221 222 225 226 227 229 229 237 240 242 247 xii CONTENTS 8 9 7.21 Glossary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.22 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 251 Systems of Equations 8.1 Matrices . . . . . . . . . . . . . . . . . 8.2 Row and column vectors . . . . . . . 8.3 The transpose operator . . . . . . . . 8.4 Zero-finding: what we’ve done so far 8.5 System of equations . . . . . . . . . . 8.6 What can go wrong? . . . . . . . . . . 8.7 Examples . . . . . . . . . . . . . . . . 8.8 One final fsolve note . . . . . . . . . 8.9 System of linear equations . . . . . . 8.10 Gaussian elimination . . . . . . . . . 8.11 What could go wrong? . . . . . . . . . 8.12 Left division . . . . . . . . . . . . . . 8.13 Application: Raoult’s Law . . . . . . . 8.13.1 Specifying T and x 1 . . . . . 8.13.2 Specifying p and x 1 . . . . . 8.14 Application: CSTR . . . . . . . . . . . 8.14.1 CSTR Basics . . . . . . . . . . 8.14.2 A single reaction example . . 8.15 Glossary . . . . . . . . . . . . . . . . . 8.16 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 257 260 261 262 262 265 268 273 274 274 276 277 279 280 285 292 292 294 303 304 . . . . . . . . . . . . . . . . . . 313 313 315 316 316 319 320 321 323 325 331 335 349 349 351 351 352 353 354 Ordinary Differential Equations 9.1 Differential equations . . . . 9.2 Euler’s method . . . . . . . . 9.3 Another note on notation . . 9.4 ode45 . . . . . . . . . . . . . 9.5 Multiple output variables . . 9.6 Analytic or numerical? . . . 9.7 What can go wrong? . . . . . 9.8 Stiffness . . . . . . . . . . . . 9.9 Examples . . . . . . . . . . . 9.10 Euler in action! . . . . . . . . 9.11 Return of fzero . . . . . . . 9.12 Isothermal Batch Reactors . 9.12.1 Batch Reactor Basics 9.12.2 Useful Assumption . 9.12.3 Note on Units . . . . 9.12.4 Final Note . . . . . . 9.13 Glossary . . . . . . . . . . . . 9.14 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CONTENTS xiii 10 Systems of ODEs 10.1 Lotka-Voltera . . . . . . . . . . . . . . . . . . . . 10.2 What can go wrong? . . . . . . . . . . . . . . . . 10.3 Output matrices . . . . . . . . . . . . . . . . . . 10.4 Examples . . . . . . . . . . . . . . . . . . . . . . 10.5 Euler returns! . . . . . . . . . . . . . . . . . . . . 10.6 Isothermal Batch Reactors: Multiple Reactions 10.7 Glossary . . . . . . . . . . . . . . . . . . . . . . . 10.8 Exercises . . . . . . . . . . . . . . . . . . . . . . 11 Second-order systems 11.1 Nested functions . . . . . . . . . . 11.1.1 Application fzero . . . . 11.2 Newtonian motion . . . . . . . . 11.3 Freefall . . . . . . . . . . . . . . . 11.4 Air resistance . . . . . . . . . . . . 11.5 Parachute! . . . . . . . . . . . . . . 11.6 Two dimensions . . . . . . . . . . 11.6.1 Flying in vacuum . . . . . 11.6.2 Flying with air resistance 11.7 What could go wrong? . . . . . . . 11.8 ODE Events . . . . . . . . . . . . . 11.9 What could go wrong? . . . . . . . 11.10 Glossary . . . . . . . . . . . . . . . 11.11 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355 355 358 359 360 372 375 378 378 . . . . . . . . . . . . . . 383 383 387 388 388 397 405 409 409 419 427 429 440 441 441 12 Interpolation 443 12.1 Discrete and continuous maps . . . . . . . . . . . . . . . . . . . . 443 12.2 Interpolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444 12.3 Interpolating the inverse function . . . . . . . . . . . . . . . . . . 448 12.4 Creating a function . . . . . . . . . . . . . . . . . . . . . . . . . . . 451 12.5 Field mice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453 12.6 Good results come from good users . . . . . . . . . . . . . . . . . . 457 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466 12.7.1 Downloading the saturated steam tables . . . . . . . . . . 466 12.7.2 dlmread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 468 12.7.3 interp1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473 12.7.4 Plotting phase diagrams . . . . . . . . . . . . . . . . . . . . 476 12.8 Glossary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480 12.9 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481 13 Numerical Integration 483 13.1 Integration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 483 13.2 Numerical Integration . . . . . . . . . . . . . . . . . . . . . . . . . 484 xiv CONTENTS 13.2.1 integral . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.2.2 Working with Tabulated Data . . . . . . . . . . . . . . . . . 13.3 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Monte Carlo 14.1 Random number generator basics . . . . . 14.2 Back to π . . . . . . . . . . . . . . . . . . . . 14.3 Evaluating Integrals in General . . . . . . . 14.4 Simplifying and Generalizing the Algorithm 14.5 Exercises . . . . . . . . . . . . . . . . . . . . 488 490 492 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 493 495 497 502 504 508 15 An Introduction to Symbolic Calculations 15.1 Some Symbolic Variable Basics . . . . . . . . . . 15.2 Limits . . . . . . . . . . . . . . . . . . . . . . . . . 15.2.1 Basic Limits . . . . . . . . . . . . . . . . . 15.2.2 Intermediate Forms and L’Hôpital’s Rule 15.2.3 Directional Limits . . . . . . . . . . . . . 15.2.4 Limits and Derivatives . . . . . . . . . . . 15.3 Derivatives . . . . . . . . . . . . . . . . . . . . . . 15.4 Indefinite and Definite Integrals . . . . . . . . . . 15.5 More fun with solve . . . . . . . . . . . . . . . . 15.6 Factor my polynomial please . . . . . . . . . . . 15.7 Solving systems of equations . . . . . . . . . . . . 15.8 Initial Value Ordinary Differential Equations . . 15.9 Boundary Value Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509 509 512 512 514 514 515 516 517 519 521 521 522 525 . . . . . . . . . . Appendix A while loops 529 A.1 Loops: what we’ve done so far . . . . . . . . . . . . . . . . . . . . . 529 A.2 while loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 529 Chapter 1 Variables and values In Chapters 1 to 6 we begin with some MATLAB “basics.” The goal is the start by building a strong foundational knowledge. From there, the MATLAB world is your oyster. By the end of this chapter you will be able to: • Demonstrate the ability to perform basic mathematical operations in the MATLAB command window • Recall how to assign variables, order of operations, and how to search the documentation • Apply MATLAB to solve basic engineering problems t You may see different windows and/or a different window layout upon starting MATLAB. The layout may be modified using the large Layout icon on the Home menu tab. You can also click on the windows and drag them to re-arrange. t In teaching this course I have used MATLAB versions R2013a and R2018a. In going from R2013a to R2018a, the only changes I have observed are to error 1.1 A glorified calculator messages, warnings about features that will become At heart, MATLAB is a glorified calculator. When you start MATLAB you will obsolete in the future, and see a window entitled MATLAB R2018a that contains smaller windows entitled the options used by a few Current Folder , Workspace , Command History and Command Window . The Command Window of MATLAB’s built-in functions, all of which will not runs the MATLAB interpreter, which allows you to type MATLAB commands, impact this course. Please then executes them and prints the result. alert me if you discover this is not the case. A nice summary of the MATLAB release history is provided on the MATLAB Wikipedia page. If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 1 2 Chapter 1 Variables and values Figure 1.1 MATLAB R2018a GUI. t If you are familiar using a bash shell on Linux or Apple OS (i.e., the default terminal), then you will notice that those commands work here in the Command Window . For example here, pressing will recall your previous commands. If you were to keep pressing , it will keep going through your previous commands sequentially. Initially, the Command Window contains a welcome message: “New to MATLAB? See resouces for Getting Started.” Getting Started is a hyperlink, give it a click. You will find that the documentation provided by MATLAB is excellent! When learning something new, please always consult the documentation. I will try to link to appropriate pages throughout the text. The documentation pages are available in your stand-alone MATLAB on your computer, and the fine folks at MathWorks also make it freely available online. The welcome message is followed by a chevron: >> which is the MATLAB prompt; that is, this symbol prompts you to enter a command. The simplest kind of command is a mathematical expression, which is made up of operands (like numbers, for example) and operators (like the plus sign, +). If you type an expression and then press Enter (or ), MATLAB evaluates the expression and prints the result. >> 2 + 1 ans = 3 Just to be clear: in the example above, MATLAB printed »; I typed 2 + 1 and then hit Enter , and MATLAB printed ans = 3. And when I say “printed,” I really mean “displayed on the screen,” which might be confusing, but it’s the way MATLAB folk talk (or computer scientists in general). An expression can contain any number of operators and operands. You don’t have to put spaces between them; some people do and some people don’t. >> 1+2+3+4+5+6+7+8+9 1.1 A glorified calculator 3 ans = 45 Speaking of spaces, you might have noticed that MATLAB puts some space between the expression and ans =, and between ans = and the result. In my examples I will leave it out to save paper. The other arithmetic operators are pretty much what you would expect. Subtraction is denoted by a minus sign, –; multiplication by an asterisk, * (sometimes pronounced “splat”); division by a forward slash /. >> 2*3 - 4/5 ans = 5.2000 The order of operations is what you would expect from basic algebra: parentheses, exponents, multiplication and division, and addition and subtraction. In the United States the acronym “PEMDAS” or the mnemonic phrase “Please Excuse My Dear Aunt Sally” is commonly used to recall the order of operations. Exponentiation happens before multiplication and division, followed by addition and subtraction; if you want to override the order of operations, you can use parentheses. >> 2 * (3-4) / 5 ans = -0.4000 When I added the parentheses I also changed the spacing to make the grouping of operands clearer to a human reader. This is the first of many style guidelines I will recommend for making your programs easier to read. Style doesn’t change what the program does; the MATLAB interpreter doesn’t check for style. But human readers do, and the most important human who will read your code is you. And that brings us to the First Theorem of debugging: Readable code is debuggable code. It is worth spending time to make your code pretty; it will save you time debugging! The other common operator is exponentiation, which uses the ^ symbol, sometimes pronounced “carat” or “hat”. So 2 raised to the 16th power is >> 2^16 ans = 65536 Again, exponentiation happens before multiplication and division, followed by addition and subtraction; if you want to override the order of operations, you can use parentheses. t Use parentheses, do NOT use brackets. Brackets mean something different to MATLAB, and will be used later when working with vectors and matrices. t When in doubt of the order of operations, add parentheses. This includes parentheses around exponents. I sometimes do this out of habit. If I wanted to raise 2 to the power of 10+6, I better put 10+6 in parentheses. Otherwise it will be 2 raised to the power of 10, then the result of that calculation plus 6. t There are so many possible function calls you may find it difficult to remember them all. If you can remember what the command starts with, you can use + Tab + Tab (or ) to try and auto-fill in the rest. If there is more than one possibility, a list of possible commands will be shown. I often find this useful for little details and spelling (e.g., sin or sine). (Again, this is a bash shell command.) Alternatively, search the documentation. 4 Chapter 1 Variables and values 1.2 Math functions MATLAB knows how to compute pretty much every math function you’ve heard of. It knows all the trigonometric functions; here’s how you use them: Figure 1.2 If you need to find a command, use the “Search Documentation” text box in the top right corner of the MATLAB window, or search the documentation online. >> sin(1) ans = 0.8415 This command is an example of a function call. The name of the function is sin, which is the usual abbreviation for the trigonometric sine. The value in parentheses is called the argument. All the standard trig functions in MATLAB work in radians. If you wish to work in degrees, you can: 1) covert from degrees to radians with the conversion factor 180/π, or 2) you can add a “d” to the end of the function. For example, while sin works in radians, sind works in degrees. Some functions take more than one argument, in which case they are separated by commas. For example, atan2 computes the inverse tangent, which is the angle in radians between the positive x-axis and the point with the given y and x coordinates. >> atan2(1,1) ans = 0.7854 If that bit of trigonometry isn’t familiar to you, don’t worry about it. It’s just an example of a function with multiple arguments. MATLAB also provides exponential functions, like exp, which computes e raised to the given power. So exp(1) is just e. >> exp(1) ans = 2.7183 t I emphasize that the function log computes the logarithm with base e; what you likely think of as ln. To compute a logarithm with base 10, use the function log10. You might also find the Change-of-Base Formula useful. The inverse of exp is log, which computes the logarithm with base e: >> log(exp(3)) ans = 3 This example also demonstrates that function calls can be nested; that is, you can use the result from one function as an argument for another. More generally, you can use a function call as an operand in an expression. >> sqrt(sin(0.5)^2 + cos(0.5)^2) ans = 1 As you probably guessed, sqrt computes the square root. There are lots of other math functions, but this is not meant to be a reference 1.3 Documentation 5 manual. To learn about other functions, you should read the documentation. In fact, whenever you use a function for the first time, I would encourage you to read the function’s documentation page. 1.3 Documentation MATLAB comes packaged with two forms of documentation, help and doc. The help command works from the Command Window ; just type help followed by the name of a command. t The blue underlined text is a hyperlink. Give it a click. Here, if you were to click on asin, it is as if you had typed help asin. >> help sin sin Sine of argument in radians. sin(X) is the sine of the elements of X. See also asin, sind. t If MATLAB does not sug- Reference page for sin Other functions named sin Unfortunately, this documentation is not beginner-friendly. One gotcha is that in previous versions of MATLAB1 , the name of the function appears in the help page in capital letters, but if you type it like that in MATLAB, you get an error: >> SIN(1) Undefined command/function 'SIN' for input arguments of type 'double'. Did you mean: >> sin(1) Notice that MATLAB is smart and suspects the typographical error, providing you with a new command prompt with the correct function. In the current version of MATLAB, boldface is used instead of capital letters to highlight the function name in the documentation, to help prevent you from making this mistake. Another problem is that the help page uses vocabulary you don’t know yet. For example, “the elements of X” won’t make sense until we get to vectors and matrices a few chapters from now. The doc pages are usually better. If you type doc sin, a browser appears with more detailed information about the function, including examples of how to use it. The examples often use vectors and arrays, so they may not make sense 1 With MATLAB R2015a, lowercase “sin” is used in the Command Window . However, if I use MATLAB R2015a from my Linux terminal, a non-GUI version that you would likely encounter at a high performance computing center, capital letters are used for “SIN”! So even with newer versions, it appears that you still need to be careful. gest a correction for a typographical error, you may need to turn it on. From the Home menu tab, select Preferences Command Window . Then under Display , you will find a box that you can check to “Suggest corrections for mistyped functions and variables.” 6 Chapter 1 Variables and values yet, but you can get a preview of what’s coming. Whenever you encounter a new function, I would strongly encourage you to get in the habit of looking at the doc page. Remember, you can also access the documentation pages online. 1.4 Variables t Technically pi is a function, not a variable. I only know this because if we type doc pi to bring up the documentation page, we find “pi returns the floatingpoint number nearest the value of π”. Since pi is a function, it can be overridden and used as a variable name. This also means pi is not stored in our Workspace . But for now you can ignore all this and pretend it is a variable. t Note: you can’t use Greek letters in MATLAB; when translating math expressions with Greek letters, it is common to write out the name of the letter (assuming you know it). t Just like pi, i and j are technically functions. From the documentation page, “Since both i and j are functions, they can be overridden and used as a variable. This permits you to use i or j as an index in FOR loops, etc.”, which is a common practice that we will use later in the text. But as with pi, for now you can ignore this. One of the features that makes MATLAB more powerful than a calculator is the ability to give a name to a value. A named value is called a variable. MATLAB comes with a few predefined variables. For example the name pi refers to the mathematical quantity π, which is approximately >> pi ans = 3.1416 And if you do anything with complex numbers, you might find it convenient that both i and j are predefined as the square root of −1. You can use a variable name anywhere you can use a number; for example, as an operand in an expression: >> pi * 3^2 ans = 28.2743 or as an argument to a function: >> sin(pi/2) ans = 1 >> exp(i * pi) ans = -1.0000 + 0.0000i As the second example shows, many MATLAB functions work with complex numbers. This example demonstrates Euler’s identity: e i π + 1 = 0. Whenever you evaluate an expression, MATLAB assigns the result to a variable named ans. You can use ans in a subsequent calculation as shorthand for “the value of the previous expression”. >> 3^2 + 4^2 ans = 25 >> sqrt(ans) ans = 5 But keep in mind that the value of ans changes every time you evaluate an expression. 1.5 Assignment statements 7 1.5 Assignment statements You can create your own variables, and give them values, with an assignment statement. The assignment operator is the equals sign, =. >> x = 6 * 7 x = 42 This example creates a new variable named x and assigns it the value of the expression 6 * 7. MATLAB responds with the variable name and the computed value. In every assignment statement, the left side has to be a legal variable name. The right side can be any expression, including function calls. Almost any sequence of lower and upper case letters is a legal variable name. The only legal punctuation (non-letter) is the underscore, _. Numbers are fine, but the variable name must begin with a letter. Spaces are not allowed, but this is where the underscore is commonly used instead. Variable names are “case sensitive”, so x and X are different variables. t Note: The current value of all assigned variables will be shown in the Workspace window, which is typically to the right of the Command Window , unless you have modified your layout. >> fibonacci0 = 1; >> LENGTH = 10; >> first_name = 'allen' first_name = allen The first two examples demonstrate the use of the semi-colon, which suppresses the output from a command. In this case MATLAB creates the variables and assigns them values, but displays nothing. The third example demonstrates that not everything in MATLAB is a number. A sequence of characters in single quotes is a string. Although i, j and pi are predefined, you are free to reassign them. It is common to use i and j for other purposes, but it is probably not a good idea to change the value of pi! Figure 1.3 Workspace and History windows. Notice variables are listed in alphabetical and not chronological order in the Workspace window. t In general, I will reserve 1.6 Why variables? The most common reasons to use variables are: 1. To avoid recomputing a value that is used repeatedly. For example, if you are performing computations involving e, you might want to compute it once and save the result. >> e = exp(1) e = 2.7183 variable names with uppercase letters for vectors and matrices, and will use lowercase letters for scalars (i.e., everything else). t Spaces are not allowed, so I commonly use an _ in place of where I might like a space when naming a variable. You will find the same practice useful later when you save script and function files. 8 Chapter 1 Variables and values t Two notes on breaking a long expression onto multiple lines using ...: 1) It must follow after an operand (i.e., +, –, *, /). 2) In the MATLAB Command Window pressing Enter (or ) will cause MATLAB to evaluate the line and will not have the desired effect of continuing on to the next line. To do this, type Shift + Enter . 2. To make the connection between the code and the underlying mathematics more apparent. If you are computing the area of a circle, you might want to use a variable named r which corresponds to the radius: >> r = 3 r = 3 >> area = pi * r^2 area = 28.2743 That way your code resembles the familiar formula πr 2 . 3. To break a long computation into a sequence of steps. Suppose you are evaluating a big, hairy expression like this: >> ans = ((x - theta) * sqrt(2 * pi) * sigma) ^ -1 * ... exp(-1/2 * (log(x - theta) - zeta)^2 / sigma^2) You can use an ellipsis to break the expression into multiple lines. Just type ... at the end of the first line and continue on the next. And in the future if you forget about the use of an ellipsis, MATLAB conveniently has a documentation page titled: “Continue Long Statements on Multiple Lines”. (In case you haven’t gotten the hint by now, take advantage of MATLAB’s documentation!) But often it is better to break the computation into a sequence of steps and assign intermediate results to variables. >> >> >> >> >> shiftx = x - theta; denom = shiftx * sqrt(2 * pi) * sigma; temp = (log(shiftx) - zeta) / sigma; exponent = -1/2 * temp^2; ans = exp(exponent) / denom The names of the intermediate variables explain their role in the computation. shiftx is the value of x shifted by theta. It should be no surprise that exponent is the argument of exp, and denom ends up in the denominator. Choosing informative names makes the code easier to read and understand (see the First Theorem of Debugging on page 3). 1.7 Errors It’s early, but now would be a good time to start making errors. Whenever you learn a new feature, you should try to make as many errors as possible, as soon 1.7 Errors 9 as possible. When you make deliberate errors, you get to see what the error messages look like. Later, when you make accidental errors, you will know what the messages mean. A common error for beginning programmers is leaving out the * for multiplication. >> area = pi r^2 area = pi r^2 | Error: Unexpected MATLAB expression. The error message indicates that, after seeing the operand pi, MATLAB was “expecting” to see an operator, like *. Instead, it got a variable name, which is the “unexpected expression” indicated by the vertical line, | (which is called a “pipe”). Another common error is to leave out the parentheses around the arguments of a function. For example, in math notation, it is common to write something like sin π, but not in MATLAB. >> sin pi Undefined function 'sin' for input arguments of type 'char'. The problem is that when you leave out the parentheses, MATLAB treats the argument as a string (rather than as an expression). In this case the sin function generates a reasonable error message, but in other cases the results can be baffling. For example, what do you think is going on here? >> abs pi ans = 112 105 There is a reason for this “feature”, but rather than get into that now, let me suggest that you should always put parentheses around arguments. This example also demonstrates the Second Theorem of Debugging: The only thing worse than getting an error message is not getting an error message. Beginning programmers hate error messages and do everything they can to make them go away. Experienced programmers know that error messages are your friend. They can be hard to understand, and even misleading, but it is worth making some effort to understand them. Here’s another common rookie error. If you were translating the following mathematical expression into MATLAB: 1 p 2 π t Again, when in doubt, use parentheses. 10 Chapter 1 Variables and values You might be tempted to write something like this: >> 1 / 2 * sqrt(pi) But that would be wrong. So very wrong. The correct translation would be: >> 1 / (2 * sqrt(pi)) 1.8 Floating-point arithmetic t The names of these variables are misleading; floating-point numbers are sometimes, wrongly, called “real”. In mathematics, there are several kinds of numbers: integer, real, rational, irrational, imaginary, complex, etc. MATLAB only has one kind of number, called floating-point. You might have noticed that MATLAB expresses values in decimal notation. So, for example, the rational number 1/3 is represented by the floating-point value >> 1/3 ans = 0.3333 t You can also change the format by selecting the Preferences icon, then selecting Command Window . There is a drop-down list next to “Numeric format” under “Text display”. which is only approximately correct. It’s not quite as bad as it seems; MATLAB uses more digits than it shows by default. You can change the format to see the other digits. >> format long >> 1/3 ans = 0.33333333333333 Internally, MATLAB uses the IEEE double-precision floating-point format, which provides about 15 significant digits of precision (in base 10). Leading and trailing zeros don’t count as “significant” digits, so MATLAB can represent large and small numbers with the same precision. Very large and very small values are displayed in scientific notation. >> factorial(100) ans = 9.332621544394410e+157 The e in this notation is not the transcendental number known as e; it is just an abbreviation for “exponent”. So this means that 100! is approximately 9.33 × 10157 . The exact solution is a 158-digit integer, but we only know the first 16 digits. You can enter numbers using the same notation. >> speed_of_light = 3.0e8 speed_of_light = 300000000 1.8 Floating-point arithmetic 11 Although MATLAB can handle large numbers, there is a limit. The predefined variables realmax and realmin contain the largest and smallest numbers that MATLAB can handle. >> realmax ans = 1.797693134862316e+308 >> realmin ans = 2.225073858507201e-308 If the result of a computation is too big, MATLAB “rounds up” to infinity. >> factorial(170) ans = 7.257415615307994e+306 >> factorial(171) ans = Inf >> realmax*100 ans = Inf Division by zero and the natural log of zero returns Inf and -Inf, respectively. >> 1/0 ans = Inf >> log(0) ans = -Inf Allowing Inf to propagate through a computation doesn’t always do what you expect, but if you are careful with how you use it, Inf can be quite useful. For operations that are truly undefined, MATLAB returns NaN, which stands for “not a number”. >> 0/0 ans = NaN Lastly, we can change the format back to where we started: >> format short >> 1/3 ans = 0.3333 We saw that with very large and small values, MATLAB uses scientific notation. If t In MATLAB R2009a and earlier, dividing by zero and the log of zero would produce a warning message because it is usually considered undefined. A warning is like an error message, but the computation is allowed to continue. 12 Chapter 1 Variables and values you would like to use scientific notation in general, you can change the format to do so; you can augment short and long with an e: >> format short e >> 1/3 ans = 3.3333e-01 >> format long e >> 1/3 >> ans = 3.333333333333333e-01 1.9 Comments Along with the commands that make up a program, it is useful to include comments that provide additional information about the program. The percent symbol % separates the comments from the code. >> speed_of_light = 3.0e8 speed_of_light = 300000000 % meters per second The comment runs from the percent symbol to the end of the line. In this case it specifies the units of the value. In an ideal world, MATLAB would keep track of units and propagate them through the computation, but for now that burden falls on the programmer. Comments have no effect on the execution of the program. They are there for human readers. Good comments make programs more readable, but bad comments are useless or (even worse) misleading. Avoid comments that are redundant with the code: >> x = 5 % assign the value 5 to x Good comments provide additional information that is not in the code, like units in the example above, or the meaning of a variable: >> p = 0 >> v = 100 >> a = -9.8 % position from the origin in meters % velocity in meters / second % acceleration of gravity in meters / second^2 If you use longer variable names, you might not need explanatory comments, but there is a trade-off: longer code can become harder to read. Also, if you are translating from math that uses short variable names, it can be useful to make your program consistent with your math. 1.10 Examples 13 1.10 Examples In each chapter I will try to provide ample examples with worked solution. As you work through the chapter material, you should first attempt to solve the example problems without looking at the solution. Consult the solutions only if you get stuck or to check your work. In doing so, the examples will serve as excellent practice for the exercises at the end of the chapter. For additional help, most of the examples will link to a screencast on my YouTube Channel of me working through the solutions and making general comments on the Chapter material. Often, at the end of the problem statement, I will provide you with a value of the final numerical solution, ans =. You can use this value to troubleshoot/debug your work. In later chapters, note that your answer may differ depending on the numerical method used. Example 1.1 Write a MATLAB expression that evaluates the following math expression. You can assume that the variables µ, σ and x already exist. e ´ ³ x−µ 2 − σp2 p σ 2π (1.1) Remember, you can’t use Greek letters in MATLAB; when translating math expressions with Greek letters, it is common to write out the name of the letter (assuming you know it). To test your expression, use µ = 2.2, σ = 0.5, and x = 2. (ans = 0.7365) Solution: (Link to screen cast.) In my solution, I will break the calculation up into many small parts to minimize error. >> x = 2; >> mu = 2.2; >> sigma = 0.5; >> exp_num = x-mu; >> exp_den = sigma*sqrt(2); >> exponent = -(exp_num/exp_den)^(2); >> denominator = sigma*sqrt(2*pi); >> exp(exponent) / denominator ans = 0.7365 14 Chapter 1 Variables and values Example 1.2 Professor Paluch is shopping for furniture. He finds a chair that cost $125 and a sofa for $200. He decides to purchase 2 chairs and 1 sofa. Assuming he has a 20% off coupon and sales tax is 8%, what is his total cost? (ans = 388.80) Solution: (Link to screen cast.) >> cost_sofa = 200; >> cost_chair = 125; >> subtotal = 2*cost_chair+cost_sofa subtotal = 450 >> discount = (1-0.2)*subtotal discount = 360 >> total_w_tax = discount*1.08 total_w_tax = 388.8000 The total is $388.80. The answer here is fine, but what if we had calculated a dollar amount with three decimal places? Since we are dealing with money, the final answer should have two decimal places. While I know you can properly round the solution, we could also have MATLAB do this for us with the round command. The first argument will be the number we wish to round, and the second argument will be the number of decimal places to use. Here is an example: >> test = 388.8011; >> round(test,2) ans = 388.8000 The result is properly round to two decimal places. Notice that MATLAB still displays four decimal places. This is because of format short. Example 1.3 Compute: sin2 ¡π¢ 6 + cos2 ¡π¢ 6 (ans = 1) Solution: (Link to screen cast.) >> sin(pi/6)^(2) + cos(pi/6)^(2) ans = 1 1.11 Chemical Engineering Examples 15 The tricky part of this problem is the placement of the exponent. In our math classes, we typically write sin2 (x) and cos2 (x). However, MATLAB does not recognize this notation. What this means is that the (entire) quantity is squared, and this is how we write it in MATLAB. In a math class, we prefer the other notation so that we do not mis-interpret this as only the term in parentheses is squared. Also note that we have an extra step in our order of operations here. First, MATLAB evaluates the terms in parentheses, pi/6. Starting from the left, MATLAB then evaluates the function, sin, and then squares the result. It then evaluates the second term in the same fashion, and then adds the two together. If you were unsure, you can add parentheses and get the same result >> (sin(pi/6))^(2) + (cos(pi/6))^(2) ans = 1 Example 1.4 Compute the area of a trapezoid with height of 2, and bases of 4 and 7. Remember: A = 12 h (b 1 + b 2 ). (ans = 11) Solution: (Link to screen cast.) >> h = 2; >> b1 = 4; >> b2 = 7; >> area = 0.5*h*(b1+b2) area = 11 1.11 Chemical Engineering Examples Example 1.5 Over limited temperature ranges, the vapor pressure of a pure fluid is commonly correlated using an Antoine equation of the form log10 p sat = A − B T +C (1.2) where A, B , and C are constants. Equation (1.2) can also be solved for T to find the corresponding saturation temperature at a particular pressure. T= B −C A − log10 p sat (1.3) 16 Chapter 1 Variables and values For ethanol, A = 8.13484, B = 1662.48, and C = 238.131 over the range −114.1 ◦ C < T < 243.1◦ C, where T is in ◦ C and p sat is in mmHg. When using an Antoine equation, please always be certain to check units; while units are not provided for the constants, they are dependent on the units used for T and p sat a) Calculate the vapor pressure of ethanol at 380 K in units of mmHg, atm, kPa, and bar. The key to conversion is remembering atmospheric pressure: 1 atm = 760 mmHg = 101.325 kPa. And 1 bar = 1 × 105 Pa, which is almost equal to 1 atm. (2069.2 mmHg) b) Does the vapor pressure of ethanol increase or decrease with increasing T ? Find out by computing p sat at a few different temperatures. (increases) c) Calculate the normal boiling point of ethanol. That is, at what temperature does it boil at atmospheric pressure? (ans = 78.289) Solution: (Link to screen cast.) Let the fun begin! a) The provided Antoine equation uses units of ◦ C for temperature and mmHg for pressure. In this problem we are asked to compute the vapor pressure in various units at 380 K. Let’s begin by converting the temperature to ◦ C, and then using the Antoine equation as written to compute the vapor pressure in mmHg. We can then convert to other units. For convenience and to make my calculation more readable, I will store the Antoine coefficients to lowercase a, b, and c, and I will make a note of the units in the variables I use to store my temperature and pressure. >> a = 8.13484; % Antione parameters >> b = 1662.48; >> c = 238.131; >> t_K = 380; % Temperature >> t_C = 380-273.15; >> p_mmHg = 10^(a-b/(t_C+c)) % Pressure p_mmHg = 2069.2 >> p_atm = p_mmHg*(1/760) p_atm = 2.7226 >> p_kPa = p_atm*101.325 p_kPa = 275.86 >> p_bar = p_kPa*(1/100) p_bar = 2.7586 1.11 Chemical Engineering Examples 17 b) In my opinion, one of the best things about computational modeling is the ability to ask “what if” questions. To me, this becomes a very powerful educational tool. For me, this is better than memorizing facts from a book. Here, we are asked if the vapor pressure of ethanol increases or decreases with temperature. From the previous question, we already have our model set-up. Here we need only change the temperature and re-evaluate p_mmHg = 10ˆ (a-b/(t_C+c)). Remember with the up arrow key ( ), we can quickly recall previous commands, and are able to edit them too. >> t1_C = 20; >> t2_C = 40; >> t3_C = 60; >> p1_mmHg = 10^(a-b/(t1_C+c)) p1_mmHg = 49.475 >> p2_mmHg = 10^(a-b/(t2_C+c)) p2_mmHg = 143.72 >> p3_mmHg = 10^(a-b/(t3_C+c)) p3_mmHg = 361.83 We find as temperature increases, the vapor pressure increases. Does this make sense? c) Last, we are asked to solve for the temperature where the vapor pressure is equal to 1 atm or 760 mmHg. Remember, using the provided Antoine parameters, the temperature will be in ◦ C and the pressure must be in mmHg. Also, the equation uses log10 . Remember in MATLAB log is actually the natural log, ln or loge , and log10 is log10 . The constants are already stored in our session, so we are all set to go! >> p_mmHg = 760; >> t_C = b/(a-log10(p_mmHg)) - c t_C = 78.289 The normal boiling point of ethanol is 78.289 ◦ C. This is less than the normal boiling point of water of 100 ◦ C. Ethanol is therefore more volatile than water. Example 1.6 All around us molecules are moving. It’s a hot summer’s day, and you sit down to have a tall glass of ice water. How fast are water molecules moving in your glass? Let’s calculate it! The kinetic energy of a molecule may be taken to be independent of it’s potential (or configurational) energy. This allows us to calculate the average kinetic energy of a molecule as Ek = m 2 3 v = kB T 2 2 (1.4) t In this problem and likely in many of your classes, you need to know the values of several constants and conversion factors. In future chapters we will build-up to using scripts and then functions to solve a problem. Then we can look our constant up once to write a function, which we can then use over and over again. 18 Chapter 1 Variables and values where m is the mass of a molecule (which is constant), v is the molecular velocity, T is the temperature, and k B is the molecular gas constant (R = k B NAvo ) or Boltzmann’s constant. We can therefore compute the average speed of a molecule as s q speed = v2 = 3k B T m (1.5) So how fast are the molecules moving in your glass of ice water? Recall: R = 8.314 J/(mol·K), NAvo = 6.022 × 1023 mol−1 , and MWwater = 18 amu. You may assume that the (solid) ice and liquid water are in equilibrium at 0 ◦ C. Report the speed in both units of m/s and miles/hour. (1 mile = 1609.34 m) (615.22 m/s) Note, the average speed is only a function of T and m. For our problem then, since the (solid) ice and liquid water are in equilibrium at the same T , the ice and liquid water molecules are moving at exactly the same average speed! At the same temperature, do heavier or lighter molecules move faster? Do molecules move faster at higher or lower temperatures? Solution: (Link to screen cast.) How cool... no pun intended. I will begin by clearing all of my variables with the command clear variables since we are beginning a new problem. If you want to clear your workspace too, try clc. For a problem like this, where there are various variables with different units, I tend to convert everything to SI and solve. This way I am confident the units of my final answer will be correct in SI units. How do we handle amu? An amu can equivalently be written as g/mol. We can convert to kg/mol (SI units) by dividing by 1000. Then to get mass of a molecule of water, we can divide by NAvo . Notice then that the NAvo to convert from R to k B and MWwater to m conveniently cancel out of the expression. This allows us to re-write the expression as: s speed = 3RT MWwater where here MWwater is in kg/mol. Let’s begin by solving for average molecular speed of water at 0 ◦ C (or 273.15 K). >> clear variables >> r = 8.314; % gas constant in J/(mol K) >> mw = 18/1000; % molecular weight of water in kg/mol >> t = 273.15; % temperature in K >> speed = sqrt( 3*r*t/mw ) speed = 615.22 Having used SI units, this is the average speed in m/s. Now let’s convert to mph as requested. We are provided with the conversion factor to go from m to miles. To go from s to h, remember 60 s equal 1 min, and 60 min equal 1 h. 1.11 Chemical Engineering Examples 19 >> speed_mph = speed*(1/1609.34)*60*60 speed_mph = 1376.2 Wow! It is a good thing a molecule of water does not weigh too much. At the same temperature, do heavier of lighter molecules move faster? The speed is inversely proportional to mass, so we expect lighter molecules to move faster. While we could work this out analytically, let’s use MATLAB and double the molecular weight. >> mw = 32/1000; >> speed = sqrt( 3*r*t/mw ) speed = 461.41 The heavier molecules moves slower as expected. What’s the effect of temperature? Since speed is proportional to temperature, we expect the speed to increase with increasing temperature. Let’s again use MATLAB, this time to double the temperature. >> t = 273.15*2; >> speed = sqrt( 3*r*t/mw ) speed = 652.54 As the temperature increases, the speed increases. Not that in this last calculation, the molecular weight was still double that of water. Example 1.7 When I was an undergraduate student and took my first thermodynamics course (CPB 314), I recall solving a lot of homework problems that required me to perform a linear interpolation in the steam tables. Let’s therefore tackle a sample problem here. Our goal here is understanding and performing a linear interpolation, do not worry if the thermo part is new to you. You would like to know the temperature of superheated steam at 40 bar and with a molar volume of v = 0.1 m3 /kg. In the “Superheated Steam” tables in the back of your CPB 314 text, you find the following data T (◦ C) 600 650 v (m3 /kg) 0.0989 0.1049 Shoot, an entry with exactly v = 0.1 m3 /kg is missing. But we do know that it must lie somewhere between 600 and 650 ◦ C. We can estimate the temperature from the provided data using a technique called linear interpolation. We will generate a general solution which you can use in you CPB 314 class, and then apply it here for the present problem. Let’s start by setting up a table depicting the general scenario t Note that the built-in MATLAB function interp1 may be used to perform a linear interpolation as we are doing here. However, we need to discuss vectors first before we can use it. 20 Chapter 1 Variables and values X x1 x2 x3 Y y1 y2 y3 where X and Y are arbitrary properties (such as T and v), where the following two points are known: (x 1 , y 1 ) and (x 3 , y 3 ). However, what you need to know is y 2 for a specified x 2 (i.e. you need the point (x 2 , y 2 ) where x 2 is known). In linear interpolation, we assume that the three points are co-linear (i.e., they all lie on a straight line), where (x 2 , y 2 ) lies in between (x 1 , y 1 ) and (x 3 , y 3 ). This is an assumption, one you should keep in mind. What is the slope of a straight line? It is a constant. Therefore, the slope from (x 1 , y 1 ) to (x 2 , y 2 ) is equal to the slope from (x 1 , y 1 ) to (x 3 , y 3 ). Remember from your math classes that slope = m = rise/run = ∆y/∆x. Now let’s write it out using math: y2 − y1 y3 − y1 = (1.6) x2 − x1 x3 − x1 All that is left is to solve for y 2 . y2 − y1 = y3 − y1 (x 2 − x 1 ) x3 − x1 y3 − y1 (x 2 − x 1 ) + y 1 x3 − x1 You can simplify this expression as much or as little as you would like. y2 = (1.7) (1.8) Now using our expression, go back and find the value of T where v = 0.1 m3 /kg. (ans = 609.17) Solution: (Link to screen cast.) In this example you can think of v as being your x variable, and T is your y variables. As far as units are concerned in this problem, if we use the same units as in the table we are interpolating, our result will have the same units. Let’s do it! >> >> >> >> >> >> t2 v1 v2 v3 t1 t3 t2 = = 0.0989; = 0.1; = 0.1049; = 600; = 650; = (t3-t1)/(v3-v1)*(v2-v1)+t1 609.17 We estimate the temperature to be 609.17 ◦ C. 1.12 MATLAB Online Before wrapping-up our first introductory chapter, I would like to just briefly mention the existance of MATLAB Online and MATLAB Drive. At Miami University, our current licence includes access to MATLAB Online, which allows you 1.13 Glossary to use MATLAB through your web browser, with no downloads or installation necessary. This gives you the ability to use MATLAB on a tablet, smart phone, or Chromebook. Or if you find yourself using a computer that you do now own, such as a loaner from the repair center, on which you can not install MATLAB, then you can still use MATLAB using MATLAB Online. MATLAB Drive, as the name suggests, is a cloud based storage system for your MATLAB files. In order to use MATLAB Online and Drive, you first need to create a MathWorks account. As indicated on the main registration page, to access your organization’s MATLAB license, you must use your work or university email address. The screen cast Introduction to MATLAB Online from Chapter 1: Variables and values provides a quick demonstration of the use of MATLAB Online. 1.13 Glossary interpreter: The program that reads and executes MATLAB code. command: A line of MATLAB code executed by the interpreter. prompt: The symbol the interpreter prints to indicate that it is waiting for you to type a command. operator: One of the symbols, like * and +, that represent mathematical operations. operand: A number or variable that appears in an expression along with operators. expression: A sequence of operands and operators that specifies a mathematical computation and yields a value. value: The numerical result of a computation. evaluate: To compute the value of an expression. order of operations: The rules that specify which operations in an expression are performed first. function: A named computation; for example log10 is the name of a function that computes logarithms in base 10. call: To cause a function to execute and compute a result. function call: A kind of command that executes a function. argument: An expression that appears in a function call to specify the value the function operates on. nested function call: An expression that uses the result from one function call as an argument for another. 21 22 Chapter 1 Variables and values variable: A named value. assignment statement: A command that creates a new variable (if necessary) and gives it a value. string: A value that consists of a sequence of characters (as opposed to a number). floating-point: The kind of number MATLAB works with. All floating-point numbers can be represented with about 16 significant decimal digits (unlike mathematical integers and reals). scientific notation: A format for typing and displaying large and small numbers; e.g. 3.0e8, which represents 3.0 × 108 or 300,000,000. comment: Part of a program that provides additional information about the program, but does not affect its execution. Chapter 2 Scripts In Chapter 2 we continue to build our foundational knowledge of MATLAB with the introduction of scripts. By the end of this chapter you will be able to: • Demonstrate the ability to write and execute MATLAB scripts • Explain how MATLAB interprets the contents of a script • Construct a script to solve basic engineering problems If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. Figure 2.1 The New Script button. You could also find “New Script” in the drop-down list from the New button. t Be sure to have a look at the 2.1 M-files MATLAB documentation. So far we have typed all of our programs “at the prompt,” which is fine if you are not writing more than a few lines. Beyond that, you will want to store your program in a script and then execute the script. A script is a file that contains MATLAB code. These files are also called “Mfiles” because they use the extension .m, which is short for MATLAB. You can create and edit scripts with any text editor or word processor, but the simplest way is by clicking the New Script button at the far left of the top menu under on the Home tab. An Editor window appears running a text editor specially designed for MATLAB. 23 t There are also many other important reasons to write scripts, even “short” scripts. If there is a calculation you know you will need to perform many times, a script can save you a great deal of time. Turning in a script for a homework solution or lab report ensures that others can reproduce your work. It can also serve as a lab notebook, documenting exactly what you did. 24 Chapter 2 Scripts Figure 2.3 The large Save icon at the top left of the top menu. Directly below you will also find a drop-down menu with options to “Save”, “Save As”, “Save All” (if you are working on more than one script at the same time), and “Save Copy As”. Figure 2.2 MATLAB R2018a desktop with an Editor window open. t If you are using MATLAB Online, have a look back at Section 1.12 and watch the screen cast Introduction to MATLAB Online from Chapter 1: Variables and values which includes an example of downloading myscript. t Just as with functions, Tab + (or ) works when typing the name of script files too. If you can remember just the first few letters, it will try to auto-fill the rest. If there is more than one possibility, a list of possible commands will appear. If you get an error, this is a good check if you mis-spelled the name or are in the wrong directory. + Tab t In MATLAB and in general, a script is nothing more than a series of commands one could just as well enter one after the other in a command prompt (or here the Command Window). Type the following code in the editor x = 5 and then press the large Save (outdated) floppy disk icon at the top left of the menu, or select the smaller Save icon in the smaller top right corner of your MATLAB desktop. You can also use your standard keyboard shortcuts, which for Windows would be Ctrl + S . Either way, a dialog box appears where you can choose the file name and the directory where it should go. Change the name to myscript.m and leave the directory unchanged. By default, MATLAB will store your script in your current folder. When you run the script from the Command Window , MATLAB will look in your current folder for the file, and it will also look in the search path, which is the list of directories MATLAB searches for scripts. You can see the list of directories in the search path by clicking on the “Set Path” icon on the Home tab. 1 Go back to the Command Window and type myscript (without the extension) at the prompt. MATLAB executes your script and displays the result. >> myscript x = 5 When you run a script, MATLAB executes the commands in the M-File, one after another, exactly as if you had typed them at the prompt. If something goes wrong and MATLAB can’t find your script, you will get an error message like: 1 If you are curious, information about adding and removing folders to/from the search path may be found in the MATLAB documentation for “Change Folders on the Search Path.” You can also add folders to the search path interactively using the “Set Path” icon on the Home tab. You may find this useful when you become a more advanced MATLAB user. 2.1 M-files 25 >> myscript Undefined function or variable 'myscript'. In this case you can either need to save your script again in your current directory or in a directory that is on the search path, or you need to modify the search path to include the directory where you keep your scripts. Changing your current folder is easily accomplished by using the interactive path navigator found immediately above your windows, but below the menu icons. You can also use the Current Folder window to interactively navigate within your current folder. When you are first getting started writing scripts, this discussion of current folders and paths can be a little confusing. And from my experience teaching this course, a very common (and frustrating) mistake made by students is saving files in a directory other than the current folder. If the reading was unclear, have a look at my “Chapter 2: Current folder and paths” screen cast. The file name can be anything you want, but you should try to choose something meaningful and memorable. You should be very careful to choose a name that is not already in use; if you do, you might accidentally replace one of MATLAB’s functions with your own. Finally, the name of the file cannot contain spaces. If you create a file named my script.m, MATLAB doesn’t complain until you try to run it: t We saw previously on >> my script Undefined function or variable 'my'. The problem is that it is looking for a script named my. The problem is even worse if the first word of the file name is a function that exists. Just for fun, create a script named abs val.m and run it. Remember, if you would like the effect of a space in your file name, use an underscore (_) between abs and val. Before looking at an example, in previous semesters I have found that students like to use the “Run” button to execute their scripts. Simply clicking the run button can also be used to run functions without any inputs. It is equivalent to typing the name of the M-file in the Command Window . The Run button is located on the Editor tab. In general, simply clicking the Run button will work for scripts and functions with no inputs. I tend to avoid using the Run button. This way when we deal later with functions, I am certain to assign the output to a variable other than “ans”, and I can make sure I pass any variables that need to be passed. But more on that later. For now, if you are curious, have a look at my Chapter 2: Run Button screen cast. page 5 that when we typed SIN, MATLAB suggested it was a possible typo and asked if we meant sin. If a script (or function) with a similar name is present in your path, MATLAB may make a suggestion. Figure 2.4 The large “Run” button on the Home tab. Simply clicking the Run button is equivalent to typing the name of the M-file in the Command Window . 26 Chapter 2 Scripts Example 2.1 t Here and in the future, I The Fibonacci sequence, denoted F , is described by the equations F 1 = 1, F 2 = 1, and for i ≥ 3, F i = F i −1 + F i −2 . The elements of this sequence occur naturally in many plants, particularly those with petals or scales arranged in the form of a logarithmic spiral. will provide you with a copy of the example script file that I provide in the text. The name of the file may be found following the Listing heading. I will also link to an electronic copy of the file that you can download from my Google Drive. The following expression computes the n th Fibonacci number: 1 Fn = p 5 "à p !n à p !n # 1+ 5 1− 5 − 2 2 (2.1) Translate this expression into MATLAB and store your code in a file named fibonacci1. At the prompt, set the value of n to 10 and then run your script. The last line of your script should assign the value of F n to ans. (ans = 55.0000) t Note that on the Home tab, under Preferences MATLAB Editor/Debugger , you will find options available for “Automatic file changes”. If you check the box “Save changes upon clicking away from a file”, your script (or function) being edited will automatically be saved if you click away from the file. This feature was activated by default when I started MATLAB R2015a and R2018a for the very first time. Solution: (Link to screen cast with accompanying M-file.) Let’s start by writing the script. I will provide you with two examples. First, since the equation is not very long, I will write everything on a single line. Second, I will break up the calculation into smaller, more manageable parts. The later approach is often preferred to minimize the occurrence of errors due to syntax and also to make the code more readable and easier to debug. Also, note that I use ans = here only to make the script more readable; it makes it clear what the intended answer is. If we were to omit this, ans = would still be printed to screen when we run the script; remember ans is the default variable MATLAB uses to store a result to when an alternative name is not provided. Listing 2.1 fibonacci1_long.m 1 ans = 1/sqrt(5) * ( ((1+sqrt(5))/2)^n - ((1-sqrt(5))/2)^n ) Listing 2.2 fibonacci1.m 1 2 3 4 5 s5 = sqrt(5); t1 = (1 + s5) / 2; t2 = (1 - s5) / 2; dfib = t1^n - t2^n; ans = dfib / s5 Save the script to your current path. Then to compute the n th Fibonacci number: >> n = 10; >> fibonacci1_long ans = 55.0000 >> fibonacci1 ans = 55.0000 2.2 Why scripts? 27 2.2 Why scripts? The most common reasons to use scripts are: • When you are writing more than a couple of lines of code, it might take a few tries to get everything right. Putting your code in a script makes it easier to edit than typing it at the prompt. t Remember, you can change the arrangement of your windows using the Layout icon in the top menu bar. On the other hand, it can be a pain to switch back and forth between the Command Window and the Editor . Try to arrange your windows so you can see the Editor and the Command Window at the same time, and use the mouse to switch between them. • If you choose good names for your scripts, you will be able to remember which script does what, and you might be able to reuse a script from one project to the next. Similarly, don’t “reinvent the wheel” every time you solve a problem; if you have script for a similar problem, use it as your starting point and modify it as necessary. We will revisit this idea later in this chapter on page 35 as the first step of incremental development. • If you run a script repeatedly, it is faster to type the name of the script than to retype the code! Unfortunately, the great power of scripts comes with great responsibility, which is that you have to make sure that the code you are running is the code you think you are running. First, whenever you edit your script, you have to save it before you run it. If you forget to save it, you will be running the old version. Also, whenever you start a new script, start with something simple, like x=5, that produces a visible effect. Then run your script and confirm that you get what you expect. MATLAB comes with a lot of predefined functions. It is easy to write a script that has the same name as a MATLAB function, and if you are not careful, you might find yourself running the MATLAB function instead of your script. This will also make sure you are saving in the search path. Either way, if the code you are running is not the code you are looking at, you will find debugging a frustrating exercise! And that brings us to the Third Theorem of Debugging: t So start by making sure your You must always be 100% sure that the code you are running is the code you think you are running. 2.3 The workspace The variables you create are stored in the workspace, which is a set of variables and their values. The who command prints the names of the variables in the workspace. script actually runs. Then if problems arise, you can focus on the the code itself. 28 Chapter 2 Scripts >> >> >> >> x=5; y=7; z=9; who Your variables are: x y z The clear command removes variables. >> clear y >> who Figure 2.5 If you prefer a graphical environment, don’t forget about the Workspace window. To clear a variable, you can highlight it by clicking, then hit the Delete key on your keyboard, or right-click with your mouse then select “Delete”. If you right-click, you will also find other useful commands that should be self-explanatory. Your variables are: x z You can clear more than one variable at a time by listing the variables sequentially. >> y = 7; >> clear x y >> who Your variables are: z If you wish to clear all of the variables, you could list all of the variables sequentially, or for this specific case you can use the command clear variables. >> >> >> >> x=5; y=7; clear variables who To display the value of a variable, you can use the disp function. >> z=9; >> disp(z) 9 But it’s easier to just type the variable name. >> z z = 9 2.4 More errors (Strictly speaking, the name of a variable is an expression, so evaluating it should assign a value to ans, but MATLAB seems to handle this as a special case.) On the topic, if you wish to clear the command window, use clc. 2.4 More errors Again, when you try something new, you should make a few mistakes on purpose so you’ll recognize them later. The most common error with scripts is to run a script without creating the necessary variables. For example, fibonacci1 requires you to assign a value to n. If you don’t: >> fibonacci1 Undefined function or variable 'n'. Error in fibonacci1 (line 9) dfib = t1^n - t2^n; The details of this message might be different for you, depending on what’s in your script. But the general idea is that n is undefined. Notice that MATLAB tells you what line of your program the error is in, and displays the line. This information can be helpful, but beware! MATLAB is telling you where the error was discovered, not where the error is. The reported line is where n is first used. And since it is not defined, MATLAB stops at this point. In this case, the error is not in the script at all; it is, in a sense, in the workspace. Which brings us to the Fourth Theorem of Debugging: Error messages tell you where the problem was discovered, not where it was caused. The object of the game is to find the cause and fix it—not just to make the error message go away. 2.5 Pre- and post-conditions Every script should contain a comment that explains what it does, and what the requirements are for the workspace. For example, I might put something like this at the beginning of fibonacci1 (Listing 2.2): 29 30 Chapter 2 Scripts fibonacci1.m 1 2 3 4 5 6 % % Computing the nth Fibonacci number using equation 2.1 % Precondition: you must assign a value to n before % running this script. % Postcondition: the result is stored in ans. % A precondition is something that must be true, when the script starts, in order for it to work correctly. A postcondition is something that will be true when the script completes. If there is a comment at the beginning of a script, MATLAB assumes it is the documentation for the script, so if you type help fibonacci1, you get the contents of the comment (without the percent signs). Figure 2.6 The Help window that opens following the command doc fibonacci1. >> help fibonacci1 Computing the nth Fibonacci number using equation 2.1 Precondition: you must assign a value to n before running this script. Postcondition: the result is stored in ans. That way, scripts that you write behave just like predefined scripts. You can even use the doc command to see your comment in the Help window. 2.6 Assignment and equality In mathematics the equals sign means that the two sides of the equation have the same value. In MATLAB an assignment statement looks like a mathematical equality, but it’s not. One difference is that the sides of an assignment statement are not interchangeable. The right side can be any legal expression, but the left side has to be a variable, which is called the target of the assignment. So this is legal: >> y = 1; >> x = y+1 x = 2 But this is not: >> y+1 = x y+1 = x | Error: The expression to the left of the equals sign is not a valid target for an assignment. 2.6 Assignment and equality In this case the error message is pretty helpful, as long as you know what a “target” is. Another difference is that an assignment statement is only temporary, in the following sense. When you assign x = y+1, you get the current value of y. If y changes later, x does not get updated. A third difference is that a mathematical equality is a statement that may or may not be true. For example, y = y + 1 is a statement that happens to be false for all real values of y. In MATLAB, y = y+1 is a sensible and useful assignment statement. It reads the current value of y, adds one, and replaces the old value with the new value. >> y = 1; >> y = y+1 y = 2 When you read MATLAB code, you might find it helpful to pronounce the equals sign “gets” rather than “equals.” So x = y+1 is pronounced “x gets the value of y plus one.” To test your understanding of assignment statements, try this exercise: 31 32 Chapter 2 Scripts Example 2.2 Write a few lines of code that swap the values of x and y. Put your code in a script called swap and test it. Solution: (Link to screen cast with accompanying M-file.) Let’s start by writing the script. Listing 2.3 swap.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 % % Swap the current value of x with the current value of y % Precondition: you must assign a value to x and y before running % this script. % Postcondition: the initial value of y is stored as the new value % of x. % the initial value of x is stored as the new value % of y. % % Storing a copy of the intial value of y y0 = y; % Now re-assigning y = x x = y0 % And now clearing (or deleting) the new variable y0 that we created clear y0; Next define initial values to y and x then run our script. >> y = 2; >> x = 4; >> swap y = 4 x = 2 2.6 Assignment and equality 33 Example 2.3 Imagine that you are the owner of a car rental company with two locations, Albany and Boston. Some of your customers do “one-way rentals,” picking up a car in Albany and returning it in Boston, or the other way around. Over time, you have observed that each week 5% of the cars in Albany are dropped off in Boston, and 3% of the cars in Boston get dropped off in Albany. At the beginning of the year, there are 150 cars at each location. Write a script called car_update that updates the number of cars in each location from one week to the next. The precondition is that the variables a and b contain the number of cars in each location at the beginning of the week. The postcondition is that a and b have been modified to reflect the number of cars that moved. To test your program, initialize a and b at the prompt and then execute the script. The script should display the updated values of a and b, but not any intermediate variables. Note: cars are countable things, so a and b should always be integer values. You might want to use the round function to compute the number of cars that move during each week. If you execute your script repeatedly, you can simulate the passage of time from week to week. What do you think will happen to the number of cars? Will all the cars end up in one place? Will the number of cars reach an equilibrium, or will it oscillate from week to week? In the next chapter we will see how to “automatically” execute your script repeatedly, and how to plot the values of a and b versus time. (After 1 week: a = 147 and b = 153) Solution: (Link to screen cast with accompanying M-file.) Listing 2.4 car_update.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 % % % % % % % % % Update the number of cars in Albany and Boston from one weeek to the next. Precondition: you must assign the number of cars in Albany and Boston at the start of the week to a and b, respectively. Postcondition: a and b have been modified to reflect the number of cars that moved during the week. % First we calculate the net number of cars gained/lost in Boston % 5% or the cars in Albany (a) are dropped off in Boston (b) % 3% of the cars in Boston (b) are dropped off in Albany (a) atob = round(0.05*a) - round(0.03*b); % Now updating the number of cars in each location % (conservation of cars) b = b + atob a = a - atob 34 Chapter 2 Scripts Let’s simulation the passage of a few weeks here. On your own you can try more. >> b = 150; >> a = 150; >> car_update % Week 1 b = 153 a = 147 >> car_update % Week 2 b = 155 a = 145 >> car_update % Week 3 b = 157 a = 143 2.7 Updating variables In Example 2.3, you might have been tempted to write something like: a = a - 0.05*a + 0.03*b b = b + 0.05*a - 0.03*b But that would be wrong, so very wrong. Why? The problem is that the first line changes the value of a, so when the second line runs, it gets the old value of b and the new value of a. As a result, the change in a is not always the same as the change in b, which violates the principle of Conversation of Cars! One solution is to use temporary variables anew and bnew: anew = a - 0.05*a + 0.03*b bnew = b + 0.05*a - 0.03*b a = anew b = bnew This has the effect of updating the variables “simultaneously;” that is, it reads both old values before writing either new value. The following is an alternative solution that has the added advantage of simplifying the computation: atob = 0.05*a - 0.03*b a = a - atob b = b + atob 2.8 Incremental development It is easy to look at this code and confirm that it obeys “Conversation of Cars”. Even if the value of atob is wrong, at least the total number of cars is right. And that brings us to the Sixth Theorem of Debugging: The best way to avoid a bug is to make it impossible. In this case, removing redundancy also eliminates the opportunity for a bug. 2.8 Incremental development When you start writing scripts that are more than a few lines, you might find yourself spending more and more time debugging. The more code you write before you start debugging, the harder it is to find the problem. Incremental development is a way of programming that tries to minimize the pain of debugging. The fundamental steps are: 1. Always start with a working program. If you have an example from a book or a program you wrote that is similar to what you are working on, start with that. Otherwise, start with something you know is correct, like x=5. Run the program and confirm that you are running the program you think you are running. This step is important, because in most environments there are lots of little things that can trip you up when you start a new project. Get them out of the way so you can focus on programming. 2. Make one small, testable change at a time. A “testable” change is one that displays something on the screen (or has some other effect) that you can check. Ideally, you should know what the correct answer is, or be able to check it by performing another computation. 3. Run the program and see if the change worked. If so, go back to Step 2. If not, you will have to do some debugging, but if the change you made was small, it shouldn’t take long to find the problem. When this process works, you will find that your changes usually work the first time, or the problem is obvious. That’s a good thing, and it brings us to the Fifth Theorem of Debugging: The best kind of debugging is the kind you don’t have to do. In practice, there are two problems with incremental development: • Sometimes you have to write extra code to generate visible output that you can check. This extra code is called scaffolding because you use it to build the program and then remove it when you are done. But the time you save on debugging is almost always worth the time you spend on scaffolding. 35 36 Chapter 2 Scripts • When you are getting started, it is usually not obvious how to choose the steps that get from x=5 to the program you are trying to write. If you find yourself writing more than a few lines of code before you start testing, and you are spending a lot of time debugging, you should try incremental development. 2.9 Unit testing In large software projects, unit testing is the process of testing software components in isolation before putting them together. The programs we have seen so far are not big enough to need unit testing, but the same principle applies when you are working with a new function or a new language feature for the first time. You should test it in isolation before you put it into your program. For example, suppose you know that x is the sine of some angle and you want to find the angle. You find the MATLAB function asin, and you are pretty sure it computes the inverse sine function. Pretty sure is not good enough; you want to be very sure. Since we know sin 0 = 0, we could try >> asin(0) ans = 0 which is correct. Also, we know that the sine of 90 degrees is 1, so if we try asin(1), we expect the answer to be 90, right? >> asin(1) ans = 1.5708 t Note, we could alternatively use asind for degrees. Oops! We forgot that the trig functions in MATLAB work in radians, not degrees. So the correct answer is π/2, which we can confirm by dividing through by pi: >> asin(1) / pi ans = 0.5000 With this kind of unit testing, you are not really checking for errors in MATLAB, you are checking your understanding. If you make an error because you are confused about how MATLAB works, it might take a long time to find, because when you look at the code, it looks right. Which brings us to the Seventh Theorem of Debugging: The worst bugs aren’t in your code; they are in your head. This example is a strategy I use a lot; working interactively on the command line to test my MATLAB understanding before implementing in my program. 2.10 Kinds of error While this is one example of unit testing, you might also think of a large programming project as a block flow diagram encountered in your mass and energy balance course; work on each unit operation (or block) separately, one at a time, then combine them to model the entire process. 2.10 Kinds of error There are four kinds of error we will encounter: Syntax error: You have written a MATLAB command that cannot execute because it violates one of the rules of syntax. For example, you can’t have two operands in a row without an operator, so pi r^2 contains a syntax error. When MATLAB finds a syntax error, it prints an error message and stops running your program. Runtime error: Your program starts running, but something goes wrong along the way. For example, if you try to access a variable that doesn’t exist, that’s a runtime error. When MATLAB detects the problem, it prints an error message and stops. Logical error: Your program runs without generating any error messages, but it doesn’t do the right thing. The problem in Section 2.7, where we changed the value of a before reading the old value, is a logical error. Numerical error: Most computations in MATLAB are only approximately right. Most of the time the errors are small enough that we don’t care, but in some cases the roundoff errors are a problem. Syntax errors are usually the easiest. Sometimes the error messages are confusing, but MATLAB can usually tell you where the error is, at least roughly. Run time errors are harder because, as I mentioned before, MATLAB can tell you where it detected the problem, but not what caused it. Logical errors are hard because MATLAB can’t help at all. Only you know what the program is supposed to do, so only you can check it. From MATLAB’s point of view, there’s nothing wrong with the program; the bug is in your head! Numerical errors can be tricky because it’s not clear whether the problem is your fault. For most simple computations, MATLAB produces the floatingpoint value that is closest to the exact solution, which means that the first 15 significant digits should be correct. But some computations are ill-conditioned, which means that even if your program is correct, the roundoff errors accumulate and the number of correct digits can be smaller. Sometimes MATLAB can warn you that this is happening, but not always! Precision (the number of digits in the answer) does not imply accuracy (the number of digits that are right). Numerical errors is a topic that will come up repeatedly in this course. 37 38 Chapter 2 Scripts 2.11 Absolute and relative error There are two ways of thinking about numerical errors, called absolute and relative. An absolute error is just the difference between the correct value and the approximation. We usually write the magnitude of the error, ignoring its sign, because it doesn’t matter whether the approximation is too high or too low. p For example, we might want to estimate 9! using the formula 18π(9/e)9 . The exact answer is 9 · 8 · 7 · 6 · 5 · 4 · 3 · 2 · 1 = 362, 880. The approximation is 359, 536.87. The absolute error is 3,343.13. At first glance, that sounds like a lot—we’re off by three thousand—but it is worth taking into account the size of the thing we are estimating. For example, $3,000 matters a lot if we are talking about my annual salary, but not at all if we are talking about the national debt. A natural way to handle this problem is to use relative error, which is the error expressed as a fraction (or percentage) of the exact value. In this case, we would divide the error by 362,880, yielding 0.00921, which is just less than 1%. For many purposes, being off by 1% is good enough. 2.12 CPB Examples Example 2.4 Let’s revisit Example 1.5 on example 1.5. Write two scripts to calculate the vapor pressure (eq. (1.2)) and saturation temperature (eq. (1.3)) of a pure fluid using Antoine’s equation. Since you anticipate using Antoine’s equation to model a wide range of fluids, as a precondition require that the variables a, b and c contain your Antoine parameters, and that variable tsat or psat contain your temperature or pressure, depending on if you would like to compute p sat or T . Remember that the Antoine parameters are dependent on the units of p sat and T . I would therefore be sure to include this information as a comment at the top of your script so that a user can find this information using help. Tell the user what the precondition units of tsat and psat should be. They don’t necessarily need to agree with the Antoine equation; you can perform the unit conversion at the beginning of the script file. Use units that you believe will be most useful. Same is true for the postcondition value of psat or tsat, which ever you are calculating. You can perform the Antoine calculations in ◦ C and mmHg, then convert to your preferred units. Again, just include this information as a comment at the start of your script file. (For problem a, you might consider outputting the pressure in a range of units.) Next, let’s use our scripts to solve again Example 1.5 a), b) and c). For ethanol, A = 8.13484, B = 1662.48, and C = 238.131 over the range −114.1 ◦ C < T < 243.1◦ C, where T is in ◦ C and p sat is in mmHg. a) Calculate the vapor pressure of ethanol at 380 K in units of mmHg, atm, kPa, and bar. The key to conversion is remembering atmospheric pressure: 1 atm 2.12 CPB Examples 39 = 760 mmHg = 101.325 kPa. And 1 bar = 1 × 105 Pa, which is almost equal to 1 atm. (Note, be sure to have your script perform the temperature conversion.) (2069.2 mmHg) b) Does the vapor pressure of ethanol increase or decrease with increasing T ? Find out by computing p sat at a few different temperatures. (increases) c) Calculate the normal boiling point of ethanol. That is, at what temperature does it boil at atmospheric pressure? (ans = 78.289) Solution: (Link to screen cast with accompanying M-files.) For this problem, our solutions are identical to the Solution of Example 1.5. The only difference here is that we are asked to write scripts rather than perform all of the calculations from the command line. a) Let’s start by writing the script. I will keep the script general and ask the user to provide Antoine parameters. This way we can apply the script to other systems. You could just as well include the parameters in the script, both are correct. Listing 2.5 Example_2_4a.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 % % % % % % % % % % % Calculating the vapor pressure of ethanol using Antione's equation Dr. Paluch, Example 2.4a Pre-condition: A, B, and C (for Antione's equation which uses mmHg and oC) stored to variables a, b, and c, and the temperature in K stored to variable t_K. Post-condition: Psat in units of mmHg (p_mmHg), atm (p_atm), kPa (p_kPa), and bar (p_bar), and temperature in degrees C (t_C) % First, let's convert T from oC to K t_C = t_K-273.15; % Calcutating log_p, with p in mmHg log_p_mmHg = a-b/(t_C+c); % p in mmHg p_mmHg = 10^log_p_mmHg % p in atm p_atm = p_mmHg/760 % p in kPa p_kPa = p_atm*101.325 % p in bar p_bar = p_kPa/100 >> >> >> >> >> t_K = 380; a = 8.13484; b = 1662.48; c = 238.131; Example_2_4a 40 Chapter 2 Scripts p_mmHg = 2069.2 p_atm = 2.7226 p_kPa = 275.86 p_bar = 2.7586 This is in perfect agreement with the Solution of Example 1.5. b) To see if the vapor pressure increases with increasing temperature, we need just specify a new temperature then re-run the script. Our Antoine parameters were already defined in the last problem. First let’s decreases the temperature and then let’s increase it. >> t_K = 380-80; >> Example_2_4a p_mmHg = 72.591 p_atm = 0.095514 p_kPa = 9.6780 p_bar = 0.096780 >> t_K = 380 + 40; >> Example_2_4a p_mmHg = 6553.9 p_atm = 8.6235 p_kPa = 873.78 p_bar = 8.7378 Once again, we find that as the temperature increases, the vapor pressure increases. c) Similar to part (a), let’s start by writing the script, where again we will keep the script as general as possible to facilitate extension to other fluids. Listing 2.6 Example_2_4c.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 % At a given pressure, calculate the correspnoding boiling point % (or saturation temperature) using Antione's equation. % % Dr. Paluch, Example 2.4c % % Pre-condition: A, B, and C (for Antione's equation which uses mmHg and % oC) stored to variables a, b, and c, and the pressure % in units of atm stored to p_atm. % Post-condition: The saturation temperature in units of oC (t_C) % % Convert the inputed pressure from atm to mmHg p_mmHg = p_atm*760; % Calculating the saturation temperature t_C = b/(a-log10(p_mmHg))-c The Antoine parameters were already defined in our workspace in part (a), so here we need just specify the pressure for which we desire to know the saturation temperature, in this case 1 atm. Take note of the units used; I use units of 2.12 CPB Examples atm for pressure for the input pressure, and then convert to mmHg in the script. >> p_atm = 1; >> Example_2_4c t_C = 78.289 In this exercise, the instructions stated: “Since you anticipate using Antoine’s equation to model a wide range of fluids, as a precondition require that the variables a, b and c contain your Antoine parameters, and that variable tsat or psat contain your temperature or pressure.” However, know that this is not always the best practice. I tell you to do so here as an academic exercise and to become familiar the interactions of scripts with your workspace. But if I think of a submission for homework, lab or project, I would create a script where at least the Antoine parameters are hard coded, and I might update the name of the file to contain the name of the species whose Antoine parameters are provided. Then if someone wanted to look at another fluid, they could easily save a copy of the file and update the parameters, assuming you have clear documentation. And if for your homework you were looking at only a specific temperature or pressure, I would hard code that too. Example 2.5 Revisit Example 1.6 on example 1.6 and write a script to calculate the average speed of a molecule at a given temperature using eq. (1.5). As a precondition, require that the molecular weight of the molecule of interest and the temperature are stored to variables. Please be sure to communicate the expected variable names and their units at the start of your script as a comment, so the user can see them with the help command. Please also communicate the units of the (postcondition) computed speed. How fast are the molecules moving in a glass of ice water? The molecular weight of water is MWwater = 18 amu, and you may assume that the temperature is 0 ◦ C. What if your glass also contained liquid ethanol molecules at the same temperature. Would the ethanol molecules or water molecules be moving faster? By how much? The molecular weight of ethanol is MWethanol = 46 amu. Solution: (Link to screen cast and accompanying M-file.) As in the last Example, our solutions are identical to the Solution of Example 1.6. The only difference here is that we are asked to write scripts rather than perform all of the calculations from the command line. I will start by writing the script. The script will be general and ask the user to specify temperature in ◦ C and the molecular weight in amu. This way we can apply the script to other systems. You could just as well include the parameters in the script and then update them for each system, which would be just as easy and would actually be a better way of documenting your solution... either is correct. Note that as compared to the Solution of Example 1.6, I have not simplified the expression. This will be a good check of our previous solution. 41 42 Chapter 2 Scripts Listing 2.7 Example_2_5.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 % At a given temperature, calculate the average speed of a molecule % using the Maxwell-Boltzmann distribution. % % Dr. Paluch, Example 2.5 % % Pre-condition: The temperature in oC stored to t_C, and the molecular % weight of the molecule of interest in amu stored to mw. % Post-condition: The average speed in units of m/s stored to speed_mps % % Convert the temperature from oC to K t_K = t_C+273.15; % Avogadro's number navo = 6.022e23; % Gas constant in J/mol K r = 8.314; % Boltzmann's constant, kB, in J/K kB = r/navo; % Calculating the mass of a molecule in kg mass_kg = mw/navo/1000; % Calculating the speed in m/s speed_mps = sqrt(3*kB*t_K/mass_kg) Start by applying to water at 0 ◦ C. >> mw = 18; >> t_C = 0; >> Example_2_5 speed_mps = 615.22 This is in perfect agreement with the Solution of Example 1.6. Next, we are asked to compare the speed to the speed of ethanol molecules at the same temperature. I will start by saving our previous result for water to a new variable. Then I will re-run the script with the molecular weight of ethanol. Finally, we will be able to compare to results to see how much faster water is. >> speed_water = speed_mps; >> mw = 46; >> Example_2_5 speed_mps = 384.85 >> water_to_ethanol = speed_water/speed_mps water_to_ethanol = 1.5986 We find that the average speed of water is greater than water, as expected since water has the smaller mass. At 0 ◦ C, we find that the average speed of water is 1.5986 times larger than ethanol. 2.12 CPB Examples 43 Example 2.6 Many of you are currently taking engineering thermodynamics or will take it next semester. Let’s therefore revisit Example 1.7 on example 1.7 and write a script to perform linear interpolation. Hopefully you will find the resulting script to be useful in your class. In your script, please be sure to communicate the required precondition input. To be absolutely clear, I might suggest including a table like that used to derive the general expression in Example 1.7. Test your code on Example 1.7: You would like to know the temperature of superheated steam at 40 bar and with a molar volume of v = 0.1 m3 /kg. In the “Superheated Steam” tables in the back of your engineering thermodynamics text, you find the following data T (◦ C) 600 650 v (m3 /kg) 0.0989 0.1049 An entry with exactly v = 0.1 m3 /kg is missing. However, we know that it must lie somewhere between 600 and 650 ◦ C. Estimate the temperature from the provided data using linear interpolation. Solution: (Link to screen cast and accompanying M-file.) We are asked to perform liner interpolation. It will be kept general, using x and y as variables. We are then asked to use it to interpolate in the superheated steam tables, exactly as in the Solution to Example 1.7. Let’s start by writing the script: Listing 2.8 Example_2_6.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 % % % % % % % % % % % % % % % Perform linear interpolation on data of the form X | Y ----x1 | y1 x2 | y2 x3 | y3 were we know all values of y2, which we wish to estimate. Note that x3>x2>x1. Dr. Paluch, Example 2.6 Pre-condition: x1, x2, x3, y1 and y3 Post-condition: y2 y2 = (y3-y1)/(x3-x1)*(x2-x1)+y1 For our problem, v corresponds to x, and T corresponds to y. 44 Chapter 2 Scripts >> >> >> >> >> >> y2 x1 = 0.0989; x2 = 0.1; x3 = 0.1049; y1 = 600; y3 = 650; Example_2_6 = 609.17 2.13 Glossary M-file: A file that contains a MATLAB program. script: An M-file that contains a sequence of MATLAB commands. search path: The list of directories where MATLAB looks for M-files. workspace: A set of variables and their values. precondition: Something that must be true when the script starts, in order for it to work correctly. postcondition: Something that will be true when the script completes. target: The variable on the left side of an assignment statement. incremental development: A way of programming by making a series of small, testable changes. scaffolding: Code you write to help you program or debug, but which is not part of the finished program. unit testing: A process of testing software by testing each component in isolation. absolute error: The difference between an approximation and an exact answer. relative error: The difference between an approximation and an exact answer, expressed as a fraction or percentage of the exact answer. 2.14 Exercises 45 2.14 Exercises Exercise 2.1 “Yaws’ Critical Property Data for Chemical Engineers and Chemists”2 recommends the following equation to compute the surface tension of saturated organic compounds (i.e., organic compounds at vapor/liquid equilibrium): σ = A + BT +C T 2 + DT 3 + E T 4 + F T 5 (2.2) where σ is the surface tension in units of N/m, T is the temperature in K, and A, B , C , D, E , and F are regressed coefficients. The surface tension is the force per unit length needed to separate the molecules at the vapor/liquid interface. It can equivalently be related to the vapor/liquid free energy barrier per unit area: σ= 1F 2A (2.3) At the critical point, the two phases become one, and the vapor/liquid free energy barrier and hence surface tension go to zero. This is a famous property that Guggenheim exploited to estimate the critical temperature of compounds. He would measure the surface tension at a range of accessible temperatures, and then extrapolate to where the surface tension went to zero. Cool! compound propane ethanol acetone A × 102 4.9138 0.8673 5.7452 B × 104 −0.9349 6.6647 0.4625 C × 106 −0.7665 −4.7511 −1.4587 D × 108 0.4318 1.3783 0.5045 E × 1011 −1.1075 −1.9592 −0.8216 F × 1014 1.1416 1.1142 0.5357 Tmin [K ] 86 250 180 Tmax [K ] 369.82 513.9 508.10 a) Write a script to compute surface tension using the expression suggested by Yaws’. b) Using your script, compute the surface tension of propane, ethanol, and acetone at a range of temperatures. Does surface tension increase or decrease with increasing temperature? Does the surface tension appear to approach zero at the critical point? (Tmax corresponds to the critical temperature.) c) Propane is a linear alkane consisting of three methyl groups. We can picture ethanol as propane where one of the terminal methyl groups has been replaced with a hydroxyl group. Comparing the surface tension of propane and ethanol can therefore give us an idea of the difference in intermolecular interaction strength of a methyl and hydroxyl group. Compute the surface tension of propane and ethanol at 300 K, and compare. In which system are the intermolecular interactions strongest? Next compare to acetone. What is the effect of the carbonyl group. 2 Yaws, Carl L. (2012; 2013; 2014). Yaws’ Critical Property Data for Chemical Engineers and Chemists. Knovel. Retrieved from https://app.knovel.com/hotlink/toc/id:kpYCPDCECD/ yaws-critical-property/yaws-critical-property 46 Chapter 2 Scripts Exercise 2.2 “Yaws’ Critical Property Data for Chemical Engineers and Chemists” recommends use of a modified Watson equation to calculate the enthalpy of vaporization as a function of temperature: ¶ µ T n ∆H vap = A 1 − B (2.4) where A, B and n are regressed coefficients, T is the temperature in K, and ∆H vap is the enthalpy of vaporization in units of kJ/mol. ∆H vap corresponds to the change in enthalpy in going from a liquid to vapor, where the two phases are at equilibrium (same temperature and pressure). Like surface tension, ∆H vap can be related to intermolecular interactions. If we assume the vapor phase is an ideal gas (a very good approximation at atmospheric pressure), then: ∆U coh = ∆H vap − RT (2.5) coh where ∆U is the cohesive energy, or the change in internal energy upon vaporizing a fluid, and R is the molar gas constant. In going from a liquid to an ideal gas, where the molecules do not interact, ∆U coh is a direct measure of the intermolecular interactions in the liquid phase. In comparing two fluids at the same temperature, the RT term is constant and we can just as well compare values of ∆H vap . compound ethanol ethane propane n-butane n-pentane n-hexane n-heptane A 60.8036 21.3420 26.8896 33.0198 39.8543 45.61 49.73 B 516.25 305.42 369.82 425.18 469.65 507.43 540.26 n 0.380 0.403 0.365 0.377 0.398 0.401 0.386 Tmin [K ] 300.00 90.35 85.44 134.86 143.42 177.84 182.56 Tmax [K ] 516.25 305.42 369.82 425.18 469.65 507.43 540.26 a) Write a script to compute ∆H vap . b) Compute ∆H vap for propane and ethanol at a range of temperatures. Does ∆H vap increase or decrease with increasing temperature? (Note that ∆H vap also goes to zero at the critical point.) c) Compute ∆H vap of propane and ethanol at 300 K. In which system are intermolecular interactions strongest? d) We mentioned that ∆H vap can be related to intermolecular interactions. This leads to an interesting observation when looking at ∆H vap for a homologous series at a specific temperature, namely that the rate of increase is constant with increasing carbon number. So if we were to look at the series of homologous linear alkanes (i.e., ethane, propane, n-butane, ...) and compute ∆H vap , we would find that the increase in going from ethane to propane is the same as the increase in going from propane to n-butane and so on. This constant rate of increase is equivalent to the contribution of a –CH2 – methyl unit. Cool! Compute ∆H vap for ethane, propane, n-butane, n-pentane, n-hexane and n-heptane at 300 K. Is the rate of increase (approximately) constant? Using the values of ethane and propane, could you predict the value of n-butane? How about the value of npentane? How good are the predictions? 2.14 Exercises 47 Exercise 2.3 “Yaws’ Handbook of Properties of Aqueous Systems”3 recommends the following equation to compute the solubility of organic gases in water: log10 S = A + B +C log10 T T (2.6) where S is the solubility in water in parts per million by weight (ppm), T is the temperature in K, and A, B , and C are regressed coefficients. Let s be the solubility in water in parts per million by moles. We can convert from S to s using the molecular weight of the solute (M s ) and the molecular weight of water (M w ) as: s= S/M s ¡ ¢ S/M s + 1 × 106 − S /M w (2.7) The molecular weight of water is 18.015 g/mol, and the molecular weight of the solutes are provided in the table below. compound methane ethane propane Ms 16.043 30.070 44.097 A 41.2703870202563 −67.5454935533925 −116.868391911852 B −1055.8685134604 4033.20796737051 6267.69431176702 C −14.6882884263499 22.5339742887131 39.4739999999982 Tmin [K ] 273.15 275.15 283.15 Tmax [K ] 360.95 323.15 360.95 a) Write a script to compute the solubility using the expression suggested by Yaws’ in units of ppm by weight and by mole (S and s). b) Using your script, compute the solubility of methane, ethane, and propane at a range of temperatures. Does the solubility of a gas in water increase or decrease with increasing temperature? c) Compare the solubility in ppm by mole (s) of methane, ethane, and propane at a common temperature of 298.15 K. Does the solubility increase or decrease with increasing alkyl chain length? Put differently, does the solubility increase or decrease in going from methane to ethane to propane? Exercise 2.4 In your transport phenomenon course (fluid mechanics), you likely solved many problems that required you to read values of friction constants from a Moody chart, or to use analytic expressions for the friction factor of limited range. Recently, Díaz-Damacillo and Plascencia published an article in AIChE Journal titled: “A New Six Parameter Model to Estimate the Friction Factor.”4 In that work, the authors propose a new analytic expression containing six parameters that is capable of estimating the friction factor for flow in pipes at all conditions (i.e., Reynold’s numbers and relative roughness). The proposed expression takes the form: f = 64 λ1 λ2 ³ ´+ ³ + Re 1 + exp τ1 −Re 1 + exp τ2 −Re · 100 600 ² D ´ (2.8) where f is the friction factor, Re is the Reynold’s number, defined as: 3 Yaws, Carl L. (2012). Yaws’ Handbook of Properties of Aqueous Systems. Knovel. Retrieved from https://app.knovel.com/hotlink/toc/id:kpYHPAS006/yaws-handbook-properties/ yaws-handbook-properties 4 L. Díaz-Damacillo and G. Plascencia, AIChE J. 2019, 65, 1144–1148. DOI: 10.1002/aic.16535 48 Chapter 2 Scripts Re = ρV D µ where ρ is the density of the fluid, V is the fluid flow velocity, and D is the diameter of the pipe. The term ² is the pipe roughness, and the term ²/D is dimensionless and commonly referred to as the relative roughness. In addition to Re and ²/D (two parameters), the other six parameters are λ1 , λ2 , τ1 , and τ2 . The parameter λ1 is the residual stress contribution from the laminar to turbulant transition to the friction factor, λ2 is the residual stress contribution from the pipe roughness to the friction factor, τ1 is the value of Re at which occurs the first transition in the friction factor, and τ2 is the value of Re at which the second transition occurs. The values of λ1 and τ1 are constant and equal to: λ1 = 0.02 τ1 = 3000 and λ2 and τ2 are given by the following expressions: !2 ¯ ¯ ¯ ¢ ¯ ² 1 ¯ · 3.7065 D ¯ à ¯ 1 ¯ λ2 = ¯λ1 − ¡ ¯ −2 log 10 0.77505 10.984 τ2 = ¡ ¢2 − ¡ ² ¢ + 7953.8 ² D D As you try to keep track of units, remember that Re and ²/D are dimensionless. a) Write a script that computes f for known values of Re and ²/D. To test your code, for Re = 1 × 105 with ²/D = 1/30 I get f = 0.0601. b) Using your script, does f increase or decrease when Re increases? c) Using your script, does f increase or decrease when ²/D increases? Chapter 3 for loops and basic plotting In Chapter 3 we continue to build our foundational knowledge of MATLAB with the introduction of loops and plotting. By the end of this chapter you will be able to: • Define a for loop and explain how it works • Explain how to construct a for loop to automate repetitive calculations • Demonstrate the ability to construct basic plots • Apply the use of for loops and plotting to solve basic engineering problems If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 3.1 for loops A loop is a part of a program that executes repeatedly; a for loop is the kind of loop that uses the for statement. The simplest use of a for loop is to execute one or more lines a fixed number of times. For example, in Example 2.3 (page 33) we wrote a script named car_update (Listing 2.4) that simulates one week in the life of a rental car company. To simulate an entire year, we have to run it 52 times: for i=1:52 car_update end t Let’s interpret the for loop another way. For the case 1 ≤ i ≤ 52, execute the command(s) between the for and end statements. Initially, when we first encounter the for statement, i is assigned a value of 1 (or whatever integer you set the lower-bound to be). Then we execute car_update and reach end. Here, the computer asks if i=52 (or whatever integer you set the upper-bound of the list to be). If i=52 we exit and move on in the code. However, if i<52, we go back to for, increment the value of i by one (i = i + 1), then execute car_update, and then ask again if i=52. This repeats until i=52 when the end statement is reached. t We previously saw that The first line looks like an assignment statement, and it is like an assignment statement, except that it runs more than once. The first time it runs, it creates the variable i and assigns it the value 1. The second time, i gets the value 2, and so on, up to 52. 49 adding a semi-colon (;) at the end of a line suppresses MATLAB from printing the result to screen. A semicolon is not necessary here for the for and end lines. Chapter 3 for loops and basic plotting 50 The colon operator, :, specifies a range of integers. In the spirit of unit testing, you can create a range at the prompt: >> 1:5 ans = 1 t When writing a for loop from command line, you do not need to press Shift + Enter to get to the next line. MATLAB recognizes that the loop is “open”, and will not evaluate until you press Enter after the end statement. Remember MATLAB skips whitespace, so I add spaces to make my command more readable. 2 3 4 5 The variable you use in the for statement is called the loop variable. It is a common convention to use the names i, j and k as loop variables. The statements inside the loop are called the body. By convention, they are indented to show that they are inside the loop, but the indentation does not actually affect the execution of the program. The end of the loop is officially marked by the end statement. Let’s look at a simple example that we can run from the command line that displays the value of the loop variable: >> for i=1:5 i end i i i i i = = = = = 1 2 3 4 5 Does the result make sense? This simple example allows us to test our understanding of the flow of calculations in a for loop, and is a perfect example of using the Command Window for unit testing and debugging. What is the value of i after the loop is complete? Let’s unit test from the command line to find out: >> i i = 5 The current value of i is the last value assigned before exiting the loop. The great thing about computers is they do exactly what you tell them to. The bad thing is they do exactly what you tell them to, even if it is wrong. So take time to fully understand the flow of calculations through a for loop. Please try to get in the habit of unit testing from the command line. It is an invaluable tool to help you check your understanding of MATLAB. As this example shows, you can run a for loop from the command line, but it’s much more common to put it in a script. If this discussion has been unclear, please watch the available screen cast. When learning about a new feature of MATLAB, it is always a great idea to look 3.1 for loops 51 at the MATLAB documentation pages. Here, try doc for. For this specific case, the documentation will use features we haven’t yet discussed in this class, such as vectors and matrices. But with time it will. You will also get an idea of some more advanced capabilities that we will take advantage of later, once we have honed our skills a little more, such as break and continue. The documentation is great too because it will link to related commands and keywords. Example 3.1 Create a script named car_loop that uses a for loop to run car_update (Listing 2.4 on page 2.4) 52 times. Remember that before you run car_update, you have to assign values to a and b. For this exercise, start with the values a = 150 and b = 150. Also, make sure that both car_update and car_loop are in your current path so that they can “see” each other. If everything goes smoothly, your script will display a long stream of numbers on the screen. But it is probably too long to fit, and even if it fits, it would be hard to interpret. A graph would be much better! (Running, I get a final answer of b = 184 and a = 116.) Solution: (Link to scree cast with accompanying M-files.) The script is a straightforward for loop. We want to run the script car_update 52 times to simulation the passage of cars over a period of 1 year or 52 weeks. Listing 3.1 car_loop.m 1 2 3 4 5 6 7 8 9 10 11 % % Update the number of cars in Albany and Boston over the % course of 52 weeks. % Precondition: you must assign the number of cars in Albany and Boston % at the start of the week to a and b, respectively. % Postcondition: a and b have been modified to reflect the number of % cars that moved after 1 year (52 weeks). % for i=1:52 car_update end In the interest of space I will not display the output here, but the script would be run as: >> b = 150; >> a = 150; >> car_loop Note that since car_loop references the script car_update, it is important that both scripts are in the same directory, and in your current path. Running the script, I get a final answer of b = 184 and a = 116. In the next section, we will see how the results may be plotted. Chapter 3 for loops and basic plotting 52 In the interest of checking our understanding, in Example 2.3 (page 33), we simulated the passage of three weeks. Let’s write a script car_loop_short to reproduce the results. Listing 3.2 car_loop_short.m 1 2 3 4 5 6 7 8 9 10 11 % % Update the number of cars in Albany and Boston over the % course of 52 weeks. % Precondition: you must assign the number of cars in Albany and Boston % at the start of the week to a and b, respectively. % Postcondition: a and b have been modified to reflect the number of % cars that moved after 3 weeks. % for i=1:3 car_update end >> b = 150; >> a = 150; >> car_loop_short b = 153 a = 147 b = 155 a = 145 b = 157 a = 143 Nice! This will be rather primitive and not as pretty as MATLAB is capable, but we could use disp to indicate the week to make the output more readable by a user. Listing 3.3 car_loop_short2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 % % Update the number of cars in Albany and Boston over the % course of 52 weeks. % Precondition: you must assign the number of cars in Albany and Boston % at the start of the week to a and b, respectively. % Postcondition: a and b have been modified to reflect the number of % cars that moved after 3 weeks. % for i=1:3 disp('Week: ') disp(i) car_update end >> b = 150; >> a = 150; >> car_loop_short2 3.2 plotting Week: 1 b = 153 a = 147 Week: 2 b = 155 a = 145 Week: 3 b = 157 a = 143 Keeping the end user in mind when writing code is always a good idea. You might also imagine a need to come back and run your code a year from now. Additionally, remember that I have eliminated the extra lines MATLAB produces in its output, so the actual result is actually less pretty than I have shown. 3.2 plotting plot is a versatile function for plotting points and lines on a two-dimensional graph. Unfortunately, it is so versatile that it can be hard to use (and hard to read the documentation!). We will start simple and work our way up. To plot a single point, type >> plot(1, 2) A Figure Window should appear with a graph and a single, blue dot at x position 1 and y position 2. To make the dot more visible, you can specify a different shape: >> plot(1, 2, 'o') The letter in single quotes is a string that specifies how the point should be plotted. You can also specify the color: >> plot(1, 2, 'ro') r stands for red; the other colors include green, blue, cyan, magenta, yellow and black. Other shapes include +, *, x, s (for square), d (for diamond), and ^ (for a triangle). When you use plot this way, it can only plot one point at a time. If you run plot again, it clears the figure before making the new plot. The hold command 53 Chapter 3 for loops and basic plotting 54 lets you override this behavior. hold on tells MATLAB not to clear the figure when it makes a new plot; hold off returns to the default behavior. Try this: >> hold on >> plot(1, 1, 'o') >> plot(2, 2, 'o') When you first execute hold on, a blank Figure Window should appear. Next, the point (1,1) will be plotted as a circle, and then the point (2,2) will be plotted as a circle; by default, since no color was specified, the first point (or data set) will be colored blue, and the second will be colored red. MATLAB scales the plot automatically; in this example the points are plotted in the corners. You can manually fix the x- and y-axis range using the commands xlim and ylim, respectively. Keeping Figure Window open, try this: >> hold on >> xlim([-1,3]) >> ylim([0,4]) t We will learn later that the argument provided to xlim and ylim is actually a (row) vector of length 2. What happened? We just changed the x-axis range to go from –1 to 3, and the y-axis range to go from 0 to 4. If you close the Figure Window you will need to start over again; you will need to again execute hold on to plot more than one point (or data set). Alternatively, to begin working with new data, you can keep the Figure Window open and just clear the data using clf; since the Figure Window never closed, there is no need to re-execute hold on. In the next example, we will additionally look at adding a figure title, axis labels, and a legend. As I stated before with for, you should get in the habit of having a look at the MATLAB documentation pages whenever we learn about a new MATLAB feature. Here, try doc plot. You will see a range of examples and learn about other features, such as creating sub-plots. If this discussion has been unclear, please watch the available screen cast. Example 3.2 Modify car_loop (Listing 3.1) so that each time through the loop it plots the value of a versus the value of i (your loop variable). Once you get that working, modify it so it plots the values of a with red circles and the values of b with blue diamonds. One more thing: if you use hold on to prevent MATLAB from clearing the figure, you might want to clear the figure yourself, from time to time, with the command clf. 3.2 plotting 55 Solution: (Link to screen cast with accompanying M-file.) So we are asked to update car_loop (Listing 3.1) so that it now plots the results. Listing 3.4 car_loop2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 % % % % % % % % Update the number of cars in Albany and Boston over the course of 52 weeks and plot the results. Precondition: you must assign the number of cars in Albany and Boston at the start of the week to a and b, respectively. Postcondition: a and b have been modified to reflect the number of cars that moved after 1 year (52 weeks). % I will start by clearing the graph in case anyting is already printed. % Note that if a figure window is not already open, a blank figure % window will appear. clf % Execute hold on to allow plotting more than one point. % Note, I said previously if a figure was already open, clf would % clear the existing figure. If we had previously executed hold on, % we would not need to do so again. However, when in doubt, % exectute hold on. If hold was already on, it will stay on. hold on for i=1:52 car_update plot(i, a, 'ro') plot(i, b, 'bd') end In the interest of space I again will not display the output from car_update here; we could instead go back to car_update and suppress printing a and b. The updated script would be run as: >> b = 150; >> a = 150; >> car_loop2 The resulting figure is: Chapter 3 for loops and basic plotting 56 190 180 170 160 150 140 130 120 110 0 10 20 30 40 50 60 Figure 3.1 The plot generated by MATLAB upon running the script car_loop2. Let’s format the image so that it is presentation ready. Keep the Figure Window open and execute the commands the follow from the command line. Alternatively, you can add the commands to the end of car_loop2.m and re-run then script (which in general would be preferred). First, let’s add labels to the x- and y-axis. This is achieved with the command xlabel and ylabel, respectively, where we will pass a string which is the axis label. >> xlabel('Week') >> ylabel('Number of Cars') Since we are simulating the passage of 1 year, or 52 weeks, lets change the x-axis range to go from 0 to 52: >> xlim([0, 52]) Next, let’s add a title. We add a title using the title command, where again we pass a string which is the plot title. >> title('Example 3.2') To finish up, let’s add a legend to distinguish between the cars in Albany and Boston. Each loop iteration, first we plot the number of cars in Albany (a) and then Boston (b). To add a legend we use the command legend. To legend we will pass a series of strings, with each string separated by a comma, in the order of the data sets plotted. Note, there are many subtleties to adding legends to plots with 3.2 plotting 57 multiple data sets, so do not be discouraged if it does not work the first time. >> legend('Albany', 'Boston') The final version of our figure may be found below: 190 Albany Boston 180 Number of Cars 170 160 150 140 130 120 110 0 10 20 30 40 50 Week Figure 3.2 Our formatted plot from car_loop2. Notice that by default MATLAB places the legend in the top right of the figure, which covers some of our data. This location is not ideal. You could move it by clicking on the legend and dragging it to the desired location. In general, I prefer to do everything from the command line. When I use MATLAB at a high performance computing center, I do not have access to a graphical interface. Working exclusively from command line also facilitates moving to a MATLAB alternative such as GNU Octave. You can move the location of the legend from command line by passing an optional argument to legend, but I will not do so here to keep from overwhelming you. (If interested, give help legend or doc legend a try. In fact, every time you learn a new function, checking out MATLAB’s documentation is always a good idea.) Lastly, it would be nice to save a copy of your pretty figure. From the Figure Save As... . You can choose the appropriate image format, and save it to the desired location; your current path is the default location. If you save your figure as a type “fig” (or a FIG-file to use the MATLAB lingo), it can be re-opened later in MATLAB and edited. It may be a good idea to save a copy as a FIG-file and a second copy as an image file you can share and include in your lab reports. From the command line, saving as FIG-file is accomplished using the savefig command, and saving as an image file can be accomplished using the print command. To save the image as FIG-file and a png with name “example_32” use: Window, you can use File >> savefig('example_32.fig') Chapter 3 for loops and basic plotting 58 >> print('-dpng', 'example_32') Other formats are available, but again, in the interest of not overwhelming you, we will stop here. (And again, have a look at MATLAB’s documentation if interested.) 3.3 Sequences In mathematics a sequence is a set of numbers that corresponds to the positive integers. The numbers in the sequence are called elements. In math notation, the elements are denoted with subscripts, so the first element of the series A is A 1 , followed by A 2 , and so on. for loops are a natural way to compute the elements of a sequence. As an example, in a geometric sequence, each element is a constant multiple of the previous element. As a more specific example, let’s look at the sequence with A 1 = 1 and the ratio A i +1 = A i /2, for all i . In other words, each element is half as big as the one before it. The following loop computes the first 10 elements of A: a = 1 for i=2:10 a = a/2 end Each time through the loop, we find the next value of a by dividing the previous value by 2. Notice that the loop range starts at 2 because the initial value of a corresponds to A 1 , so the first time through the loop we are computing A 2 . Each time through the loop, we replace the previous element with the next, so at the end, a contains the 10th element. The other elements are displayed on the screen, but they are not saved in a variable. Later, we will see how to save all of the elements of a sequence in a vector. This loop computes the sequence recurrently, which means that each element depends on the previous one. For this sequence it is also possible to compute the i th element directly, as a function of i , without using the previous ¡ ¢i −1 element. In math notation, A i = A 1 12 . Example 3.3 Write a script named sequence that uses a loop to compute elements of A directly. Solution: (Link to screen cast and accompanying M-file.) Let me actually write a script named sequence that uses a loop to compute the elements of A both directly and recurrently. This will allow me to make sure my code is correct. For the recurrent calculations, I will use variable b instead of a. 3.4 Series 59 Listing 3.5 sequence.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 % % % % % % % % Calculating the value of Ai+1 = Ai/2 where A1 = 1 both directly and recurrently. The recurrent calculation will not be printed, but used to check the correctness of the direct calculations. Precondition: none Postcondition: at the conclusion of the loop a will contain the value of A10 % a1 for use in the direct calculations a1 = 1; % initial value of b for the recurrent calculations b = 1; % for i=2:10 b = b/2; a = a1*0.5^(i-1) % Checking that both compute the same value check = b - a end 3.4 Series In mathematics, a series is the sum of the elements of a sequence. It’s a terrible name, because in common English, “sequence” and “series” mean pretty much the same thing, but in math, a sequence is a set of numbers, and a series is an expression (a sum) that has a single value. In math notation, a series is often P written using the summation symbol . For example, the sum of the first 10 elements of A is 10 X Ai i =1 where A i = A 1 series: ¡ 1 ¢i −1 2 . A for loop is a natural way to compute the value of this a1 = 1; total = 0; for i=1:10 a = a1 * 0.5^(i-1); total = total + a; end ans = total Chapter 3 for loops and basic plotting 60 a1 is the first element of the sequence. Each time through the loop a is the i th element. Note we can start with i=1 because we are computing the elements directly, and the first time through the loop, i=1 resulting in a=a1. Had we been t When using a for loop to compute a sum, the accumulator will almost always start with a value of 0. This way when we add our first term to the sum, the sum is equal to the first term. Likewise, when we later use for loops to compute continuous products, we will start with a value of 1 calculating recurrently, we would need to specify a starting point, as we will see in the next Example. The way we are using total is sometimes called an accumulator; that is, a variable that accumulates a result a little bit at a time. Before the loop we initialize it to 0. Each time through the loop we add in the i th element. At the end of the loop, total contains the sum of the elements. Since that’s the value we were looking for, we assign it to ans. Example 3.4 In this section, we wrote a for loop to compute the sum of the sequence (or series), where each element was computed directly. Write a script named series_direct to perform the calculation. Next, write a script named series_recurrent to compute the same sum with the elements computed recurrently. You will have to be careful about where you start and stop the loop. (ans = 1.9980) Solution: (Link to screen cast and accompanying M-files.) We will create two scripts. First, let’s create a script series_direct that calculates the terms of the sequence directly. Then, let’s create a script series_recurrent that calculates the terms of the sequence recurrently. This will allow us to test the second case for correctness. 3.5 Generalization 61 Listing 3.6 series_direct.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 % % % % % % % Calculating the value of the series of Ai from 1 to 10 with each element of the sequence computed directly. Precondition: none Postcondition: at the conclusion of the loop total and ans will contain the value of the sum a1 = 1; total = 0; for i=1:10 a = a1 * 0.5^(i-1); total = total + a; end ans = total Listing 3.7 series_recurrent.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 % % % % % % % Calculating the value of the series of Ai from 1 to 10 with each element of the sequence computed recurrently. Precondition: none Postcondition: at the conclusion of the loop total and ans will contain the value of the sum a = 1; % Start with total = a1 total = a; % Then we begin the loop with a2 for i=2:10 a = a/2; total = total + a; end ans = total When executed, both scripts properly compute ans = 1.9980. Note that in series_direct, we could start with i=2, but then total needs to begin with (or be initialized with) total=a1. 3.5 Generalization As written, the previous example always adds up the first 10 elements of the sequence, but we might be curious to know what happens to total as we increase the number of terms in the series. If you have studied geometric series, you might know that this series converges on 2; that is, as the number of terms goes to infinity, the sum approaches 2 asymptotically. Chapter 3 for loops and basic plotting 62 To see if that’s true for our program, series_direct, we could replace the constant, 10, with a variable named n: a1 = 1; total = 0; for i=1:n a = a1 * 0.5^(i-1); total = total + a; end ans = total Now the script can compute any number of terms, with the precondition that you have to set n before you execute the script. Here’s how you could run it with different values of n, where I use format long to display more decimal places: t We haven’t previously seen multiple commands on a single line like we have here. Multiple commands separated by semi-colon can be placed on a single line rather than a separate line for each command. In series_direct the final sum in printed to screen; this therefore must end a line. >> format long >> n=10; series_direct total = 1.99804687500000 >> n=20; series_direct total = 1.99999809265137 >> n=30; series_direct total = 1.99999999813735 >> n=40; series_direct total = 1.99999999999818 It sure looks like it’s converging on 2. Replacing a constant with a variable is called generalization. Instead of computing a fixed, specific number of terms, the new script is more general; it can compute any number of terms. This is an important idea we will come back to when we talk about functions. 3.6 Examples 63 3.6 Examples Practice makes perfect... Example 3.5 Write a general script that uses a for loop to compute the factorial of an arbitrary integer n. Test you script by computing the factorial of a number and comparing to the result of the MATLAB function factorial. The recall factorial of 3 is: 3! = 3 · 2 · 1 or equivalently 3! = 1 · 2 · 3 I write this second expression as a hint as to how you might set-up a for loop. In general, n! = n · (n − 1) · (n − 2) · ... · 1 or n! = Πni=1 i where Π is the symbol for a continuous product. (A continuous product is similar to a sum, only we multiple consecutive elements rather than add them.) Solution: (Link to screen cast and accompanying M-file.) Listing 3.8 Example_3_5.m 1 2 3 4 5 6 7 8 9 10 % Calculating the facotrial of a number n % % Pre-condition: n, the number you wish to compute the factorial of % Post-condition: ans and n_fact will contain the value of the factorial % of n n_fact = 1; for i = 2:n n_fact = n_fact*i; end ans = n_fact Note that I could start my loop with either i=1 or i=2 and obtain the same result. When computing sums with a for loop, we typically initialize the counter variable with a value of zero. A number plus zero is equal to that number. With continuous products, we typically initialize the counter variable with a value of one. A number times one is equal to that number. Chapter 3 for loops and basic plotting 64 Example 3.6 During class, we found that sin(pi) was not exactly equal to zero as we expected. The reason for this is pi is not exact but is rather a numerical approximation. It is valuable to know how MATLAB actually computes sine, and how the numerical error is thus propagated through the calculation. The calculation of sine is not a standard operator. The MATLAB function is actually performing a Taylor series expansion about the specified point: sin (x) = ∞ X i =0 (−1)i x 2i +1 (2i + 1)! (3.1) MATLAB can not carry out the summation to ∞, so it must truncate the series. This introduces additional numerical error in the calculation, although the number of terms used is large enough that you likely do not notice. This is in addition to the numerical error of approximating pi, which is carried throughout the sum. Write a script to compute sin(pi) using a Taylor series expansion, and compare to the MATLAB function sin. Run your script multiple times to see how the result changes with the number of terms in the series. Note that eq. (3.1) requires the calculation of the factorial of 2i + 1. You can add a second, “nested” loop to your script to compute the factorial, just be sure to use a unique loop variable. Solution: (Link to screen cast and accompanying M-file.) Listing 3.9 Example_3_6.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 % Calculating sin(x) using a Taylor series expansion % % Pre-condition: x in radians, where x is what you would like to compute % the sine of, and n, the number of terms in the Taylor % series expansion % Post-condition: The value of sin(x) using n terms in the series will % be stored to taylor_ans, and the result using MATLAB's % built in function will be stored to ans_matlab. I % will also plot the value of sin(x) vs the terms in the % sum. % % Start by clearing our figure clf % Don't clear my graph when I add a new point hold on % My Taylor series accumulator taylor_ans = 0; for i=0:n % We will use 2*i+1 twice, so let's precompute dum = 2*i+1; % Computing the factorial of 2*i+1 % Note that when i = 0, it won't perform the for loop. This is okay % since the factorial of 1 is 1. n_fact = 1; 3.6 Examples for j=2:dum n_fact = n_fact*j; end % Calculating the ith Taylor term taylor_term = (-1)^(i)*x^(dum)/n_fact; % Updating my Talylor sum taylor_ans = taylor_ans+taylor_term; plot(i,taylor_ans,'ro') end % Labeling the plot axis xlabel('n') ylabel('sin(x)') taylor_ans ans_matlab = sin(x) 1.6 1.5 1.4 1.3 sin(x) 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 65 1.2 1.1 1 0.9 0 5 10 15 20 n Figure 3.3 A plot of the estimate of sin(x) as a function of terms, n, in the Taylor series, where here x = π/2.. Chapter 3 for loops and basic plotting 66 Example 3.7 We have already seen the Fibonacci sequence, F , which is defined recurrently as F i = F i −1 + F i −2 In order to get started, you have to specify the first two elements, but once you have those, you can compute the rest. The most common Fibonacci sequence starts with F 1 = 1 and F 2 = 1. Write a script that uses a for loop to compute the first 10 elements of this Fibonacci sequence. As a postcondition, your script should assign the 10th element to ans. Now generalize your script so that it computes the n th element for any value of n, with the precondition that you have to set n before you run the script. To keep things simple for now, you can assume that n is greater than 2. Hint: you will have to use two variables to keep track of the previous two elements of the sequence. You might want to call them prev1 and prev2. Initially, prev1 = F1 and prev2 = F2 . At the end of the loop, you will have to update prev1 and prev2; think carefully about the order of the updates! (ans = 55) Solution: (Link to screen cast and accompanying M-file.) Listing 3.10 Example_3_7.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 % Computing the nth Fibonacci number. For now, we will assume % that n is greater than 2. Later in Chapter 4, we will find % that we can use an if statement to check this condition. % % Pre-condition: n, the value of the desired Fibonacci number (n>2) % Post-condition: ans and Fi contain the value of the nth Fibonacci number % F1 = 1; % F(i-2) F2 = 1; % F(i-1) for i=3:n Fi = F2 + F1; % now thinking about what I need for my next time through the % loop (for i+1) prev_1 = Fi; % current i will be i-1 prev_2 = F2; % current i-1 will be i-2 F1 = prev_2; F2 = prev_1; end ans = Fi Example 3.8 Write a script that loops i through a range from 1 to 20, uses the script from Example 3.7 to compute Fibonacci numbers, and plots F i for each i with a series of red circles. 3.6 Examples 67 Solution: (Link to screen cast and accompanying M-file.) Listing 3.11 Example_3_8.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 % Plotting the first 20 Fibonacci numbers % % Pre-condition: none % Post-condition: plot of the first 20 Fibonacci numbers % Clear figure clf; % Don't clear graph with each new point hold on F1 = 1; % F(i-2) F2 = 1; % F(i-1) plot(1,F1,'ro') plot(2,F2,'ro') for i=3:20 Fi = F2 + F1; % now thinking about what I need for my next time throuh the % loop (for i+1) prev_1 = Fi; % current i will be i-1 prev_2 = F2; % current i-1 will be i-2 F1 = prev_2; F2 = prev_1; plot(i,Fi,'ro') end xlabel('i') ylabel('F_i') Chapter 3 for loops and basic plotting 68 7000 6000 5000 Fi 4000 3000 2000 1000 0 0 5 10 15 20 i Figure 3.4 A plot of the Fibonacci sequence versus index (i ). 3.7 CPB Examples 69 3.7 CPB Examples Example 3.9 Let’s build-upon the script you wrote for Example 2.4 on page 38. Recall you wrote a script to calculate the vapor pressure (p sat ) of ethanol for a given T , where, A = 8.13484, B = 1662.48, and C = 238.131, and is valid over the range −114.1 ◦ C < T < 243.1◦ C, where T is in ◦ C and p sat is in mmHg. Here, let’s create a plot of our results. A common way to represent the pT plane is using a Clapeyron plot. In a Clapeyron plot, we plot log10 p sat or ln p sat versus 1/T . Since it is inverse T , T should be in absolute units (e.g., K). Create a Clapeyron plot for ethanol over the range 0 to 100 ◦ C, where p sat is in units of mmHg and T is in units of K. Represent each point with a circle. Label your axis and give your plot a title. Solution: (Link to screen cast and accompanying M-file.) Over small temperature ranges, ln p sat or log p sat vs 1/T is linear. This is why it is common to plot p sat vs T is this way. This type of plot is know as a Clapeyron plot, named after the thermodynamicist that first suggested the linear dependence. Linearizing data is advantageous as it facilitates interpolating and extrapolating the results. For this case there is also a physical benefit, but you will have to wait for your thermodynamics class for that discussion. Listing 3.12 clapeyron_plot.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 % % % % % % % % Plotting the vapor pressure of ethanol using Antione's equation from 0 to 100 oC Pre-condition: A, B, and C (for Antione's equation which uses mmHg and oC) stored to variables a, b, and c. Post-condition: Plot of log10 p vs 1/T where T is in units of K and p is in units of mmHg % Start by clearing our figure clf % Don't erase the graph when we add a new data set hold on % We need to calculate log10 p over the range 0 to 100 C. % We will loop from 0 to 100 C in increments of 1 C. % Each interation we will plot the results, where % the temperature in converted to K before calculating % the inverse. for t_C = 0:100 % Calcutating log_p_mmHg, p in units of mmHg log_p_mmHg = a-b/(t_C+c); % Calculating 1/t with t in units of K tinv = 1/(t_C+273.15); % Plotting plot(tinv,log_p_mmHg,'ro') end Chapter 3 for loops and basic plotting 70 % labeling the axis xlabel('1/T [1/K]') ylabel('log10 p/mmHg') % plot title title('Clapeyron plot for ethanol for 0 to 100 C') % Last, let's get in the habit of saving and printing our graphs savefig('ethanol_clapeyron_plot.fig') print('-dpng', 'ethanol_clapeyron_plot') >> >> >> >> a = 8.13484; b = 1662.48; c = 238.131; clapeyron_plot 3.5 Clapeyron plot for ethanol for 0 to 100 C 3 log10 p/mmHg 28 29 30 31 32 33 34 35 36 37 38 2.5 2 1.5 1 2.6 2.8 3 3.2 1/T [1/K] 3.4 3.6 3.8 ×10 -3 Figure 3.5 The plot generated by MATLAB upon running the script clapeyron_plot. Note, here I require a, b, and c as pre-conditions, despite indicating throughout the script that it is for ethanol. It would in general be better to assign values to a, b, and c within the script. This would give me a record of exactly what was done, in addition to being inline with our ethanol specific script. It would then be easy to update the script for a new compound and just rename the file. However, I did not do this to give us practice specifying pre-conditions. 3.7 CPB Examples 71 Example 3.10 Let’s build upon the script you wrote for Example 2.5 on page 41. Recall you wrote a script to calculate the average speed of a molecule at a specified temperature. You used your script to compare the average speed of water and ethanol at 0 ◦ C. Create a plot of the average speed of water and ethanol over the range 0 to 100 ◦ C. Use a different symbol to represent water and ethanol. Label your axis, give your plot a title, and add a legend. Adding a legend can be tricky. Things will become much easier when we learn about vectors, but we are not there yet. Every time you plot a new point, MATLAB treats this as a new set of data. The legend labels need to be provided in the same order as the data sets that have been plotted. So if you plot a point for water first and then ethanol, you are all set. But if you plot all of the water data and then all of the ethanol data, it will not work as expected. Why, because the first two sets of data correspond to the first two water points. Give it a try both ways and you will see what I mean. Solution: (Link to screen cast and accompanying M-file.) Maxwell-Boltzmann fun! Listing 3.13 maxwell_plot.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 % % % % % % % % Plotting the average speed of water and ethanol molecules over the range 0 to 100 oC Pre-condition: None. I will hardcode everything since we are asked for a specific temperature range and the two species are specified. Post-condition: Plot of average molecular speed versus temperature % Start by clearing our figure clf % Don't erase the graph when we add a new data set hold on % gas constant in J/(mol K) r = 8.314; % Molecular weight of water and ethanol in kg/mol mw_water = 18/1000; mw_ethanol = 46/1000; % Looping over the range 0 to 100 C and calculating the % average molecular speed. Note our Maxwell-Boltzmann % equation uses K for temperature, so we will perform % the conversion each iteration. for t_C = 0:100 % Temperature in K t_K = t_C + 273.15; % Speed of water in m/s speed_water = sqrt( 3*r*t_K/mw_water ); speed_ethanol = sqrt( 3*r*t_K/mw_ethanol ); % Plotting Chapter 3 for loops and basic plotting 72 end % When plotting, we will use the temperature in C, since % that is what the problem statement asked for. % Water then ethanol plot(t_C,speed_water,'ro') plot(t_C,speed_ethanol,'bx') % labeling the axis xlabel('T [C]') ylabel('speed [m/s]') % plot title title('Average speed from Maxwell-Boltzmann') % legend. % Data is plotted water then ethanol legend('water','ethanol') % Last, let's get in the habit of saving and printing our graphs savefig('maxwell_plot.fig') print('-dpng', 'maxwell_plot') >> maxwell_plot Average speed from Maxwell-Boltzmann 750 water ethanol 700 650 600 speed [m/s] 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 550 500 450 400 350 0 20 40 60 80 100 T [C] Figure 3.6 The plot generated by MATLAB upon running the script maxwell_plot. 3.8 Creating multiple figures 3.8 Creating multiple figures A question that has come up repeatedly in the past is how to create multiple figures. This is best demonstrated using Example 3.10 where we are plotting two sets of data. So what exactly do I mean by creating multiple figures? Imagine I wish to create a separate plot for water and ethanol, and I wish to compare them. We could do this by opening a figure window and first generating the plot for water. We can then save and print the plot. Then we would clear the figure, generate the plot for ethanol, and then save and print again. We could then compare the two printed plots. By default MATLAB only uses a single figure window, which is why we are forced to compare the printed files. What would be great is if we could create multiple figure windows. This can be accomplished using the figure command. If we type figure(1), it will open a new figure window named Figure 1. This can be followed with the hold on command, and we can then go ahead and plot our data for water. We can open up a second, new figure window using the command figure(2). This new figure window will be named Figure 2. This is a new figure window, so you will need to re-type hold on, and then you can plot your ethanol data. To switch back and forth between the figures, you need just type figure(1) or figure(2), depending on which you are interested in manipulating, followed by the commands of interest. Let’s have a look at how we can update Listing 3.12 to accomplish this. Listing 3.14 maxwell_multi_plot.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 % % % % % % % % Plotting the average speed of water and ethanol molecules over the range 0 to 100 oC Pre-condition: None. I will hardcode everything since we are asked for a specific temperature range and the two species are specified. Post-condition: Plot of average molecular speed versus temperature % Start by creating two figure windows. We will plot water in % Figure 1, and ethanol in Figure 2. We will need to initiate with % hold on in both. figure(1) hold on figure(2) hold on % gas constant in J/(mol K) r = 8.314; % Molecular weight of water and ethanol in kg/mol mw_water = 18/1000; mw_ethanol = 46/1000; % Looping over the range 0 to 100 C and calculating the % average molecular speed. Note our Maxwell-Boltzmann 73 Chapter 3 for loops and basic plotting 74 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 % equation uses K for temperature, so we will perform % the conversion each iteration. for t_C = 0:100 % Temperature in K t_K = t_C + 273.15; % Speed of water in m/s speed_water = sqrt( 3*r*t_K/mw_water ); speed_ethanol = sqrt( 3*r*t_K/mw_ethanol ); % Plotting % When plotting, we will use the temperature in C, since % that is what the problem statement asked for. % Water then ethanol. % As mentioned earlier, we will plot water in Figure 1 and % ethanol in Figure 2 figure(1) plot(t_C,speed_water,'ro') figure(2) plot(t_C,speed_ethanol,'bx') end % labeling the axis % We will repeat for each figure figure(1) xlabel('T [C]') ylabel('speed [m/s]') figure(2) xlabel('T [C]') ylabel('speed [m/s]') % plot title figure(1) title('Average speed of water from Maxwell-Boltzmann') figure(2) title('Average speed of ethanol from Maxwell-Boltzmann') % Last, let's get in the habit of saving and printing our graphs figure(1) savefig('water_maxwell_plot.fig') print('-dpng', 'water_maxwell_plot') figure(2) savefig('ethanol_maxwell_plot.fig') print('-dpng', 'ethanol_maxwell_plot') >> maxwell_multi_plot 3.8 Creating multiple figures 75 Average speed of water from Maxwell-Boltzmann 720 700 speed [m/s] 680 660 640 620 600 0 20 40 60 80 100 T [C] Figure 3.7 The first figure for water generated by MATLAB upon running the script maxwell_multi_plot. Average speed of ethanol from Maxwell-Boltzmann 450 440 speed [m/s] 430 420 410 400 390 380 0 20 40 60 80 100 T [C] Figure 3.8 The second figure for ethanol generated by MATLAB upon running the script maxwell_multi_plot. Additionally, I would encourage you to have a look at help figure and doc figure. You may also be interested in MATLAB’s subplot, which can create multiple plots within the same figure window. This can be useful to facilitate Chapter 3 for loops and basic plotting 76 comparing plots that have a common axis. It is a little more involved so we will look at it in examples later. But if you are curious, have a look at MATLAB’s documentation. 3.9 Glossary loop: A part of a program that runs repeatedly. loop variable: A variable, defined in a for statement, that gets assigned a different value each time through the loop. range: The set of values assigned to the loop variable, often specified with the colon operator; for example 1:5. body: The statements inside the for loop that are run repeatedly. sequence: In mathematics, a set of numbers that correspond to the positive integers. element: A member of the set of numbers in a sequence. recurrently: A way of computing the next element of a sequence based on previous elements. directly: A way of computing an element in a sequence without using previous elements. series: The sum of the elements in a sequence. accumulator: A variable that is used to accumulate a result a little bit at a time. generalization: A way to make a program more versatile, for example by replacing a specific value with a variable that can have any value. 3.10 Exercises 77 3.10 Exercises Exercise 3.1 Let’s build-upon the script you wrote for Exercise 2.1 on page 45. Recall you wrote a script to calculate the surface tension (σ) at saturation for ethanol, propane, and acetone at a given temperature (T ). We looked at a range of temperatures to understand if σ increased or decreased with increasing temperature, and we checked to see that σ approached a value of zero at the critical point. Here, let’s build off of this. The script we wrote for Exercise 2.1 works such that if we specify T , it returns the value of σ. This script works and is bug free, so in the spirit of unit testing and incremental development, let’s keep it relatively untouched and build-off of it. a) Create a new script that loops over the entire temperature range of applicability, computes σ, and then plots σ versus T . Plot the results for both ethanol, propane, and acetone in the same figure. Be sure to use a different symbol and color for ethanol, propane, and acetone, add a title, axis labels, and a legend. (While not required for homework, try and create three separate figures too.) Do your observations agree with your findings in Exercise 2.1? b) It can be difficult to compare ethanol, propane, and acetone in the same figure because their temperature ranges are so different. One strategy to facilitate comparison is to plot with respect to the reduced temperature (Tr ): Tr = T Tc (3.2) where Tc is the critical temperature. Create a copy of your script and update to plot σ versus Tr , then re-run. Note that plotting in reduced units is also a common practice as the properties commonly exhibit (near) universal behavior. This is the basis of corresponding states theory. In case there is an issue looking back at Exercise 2.1, the expression for σ along with the parameters are re-produced below. 1 The value of Tmax is Tc . σ = A + BT +C T 2 + DT 3 + E T 4 + F T 5 Where σ is the surface tension in units of N/m, T is the temperature in K, and A, B , C , D, E , and F are regressed coefficients. compound propane ethanol acetone A × 102 4.9138 0.8673 5.7452 1 Yaws, Carl L.. B × 104 −0.9349 6.6647 0.4625 C × 106 −0.7665 −4.7511 −1.4587 D × 108 0.4318 1.3783 0.5045 E × 1011 −1.1075 −1.9592 −0.8216 F × 1014 1.1416 1.1142 0.5357 Tmin [K ] 86 250 180 Tmax [K ] 369.82 513.9 508.10 (2012; 2013; 2014). Yaws’ Critical Property Data for Chemical Engineers and Chemists. Knovel. Retrieved from https://app.knovel.com/hotlink/toc/id:kpYCPDCECD/ yaws-critical-property/yaws-critical-property Chapter 3 for loops and basic plotting 78 Exercise 3.2 Let’s build upon Exercise 2.2. “Yaws’ Critical Property Data for Chemical Engineers and Chemists” recommends use of a modified Watson equation to calculate the enthalpy of vaporization as a function of temperature: ¶ µ T n ∆H vap = A 1 − B (3.3) where A, B and n are regressed coefficients, T is the temperature in K, and ∆H vap is the enthalpy of vaporization in units of kJ/mol. Below I have expanded the table of parameters from Exercise 2.2 for the homologous series of linear alkanes. (Specifically, I have eliminated ethanol and added n-octane.) compound ethane propane n-butane n-pentane n-hexane n-heptane n-octane A 21.3420 26.8896 33.0198 39.8543 45.61 49.73 59.0771 B 305.42 369.82 425.18 469.65 507.43 540.26 568.83 n 0.403 0.365 0.377 0.398 0.401 0.386 0.439 Tmin [K ] 90.35 85.44 134.86 143.42 177.84 182.56 216.38 Tmax [K ] 305.42 369.82 425.18 469.65 507.43 540.26 568.83 Write a script to compute ∆H vap for each compound at 300 K, and create a plot of ∆H versus carbon number. Add a title, label your axis, and print the resulting plot to file. Does ∆H vap appear to increase linearly with the carbon number? vap Exercise 3.3 Let’s build upon Exercise 2.3. “Yaws’ Handbook of Properties of Aqueous Systems”2 recommends the following equation to compute the solubility of organic gases in water: log10 S = A + B +C log10 T T (3.4) where S is the solubility in water in parts per million by weight (ppm), T is the temperature in K, and A, B , and C are regressed coefficients. compound methane ethane propane Ms 16.043 30.070 44.097 A 41.2703870202563 −67.5454935533925 −116.868391911852 B −1055.8685134604 4033.20796737051 6267.69431176702 C −14.6882884263499 22.5339742887131 39.4739999999982 Tmin [K ] 273.15 275.15 283.15 Tmax [K ] 360.95 323.15 360.95 Write a script to compute log10 S for methane over the entire temperature range of applicability (Tmin to Tmax ), and plot log10 S versus temperature. Add a title and axis labels. Also, have your script save and print your figure. 2 Yaws, Carl L. (2012). Yaws’ Handbook of Properties of Aqueous Systems. Knovel. Retrieved from https://app.knovel.com/hotlink/toc/id:kpYHPAS006/yaws-handbook-properties/ yaws-handbook-properties 3.10 Exercises Exercise 3.4 Let’s build upon Exercise 2.4. Build upon your script to plot f versus Re for a given value of ²/D. That is, make your own Moody chart. Cool! For the purpose of this question, write a script where you loop over Re values over the range of 1 × 103 to 1.1 × 103 , for each Re compute f for the case of ²/D = 0.1 and ²/D = 0.001, and plot. Plot the results for ²/D = 0.1 and ²/D = 0.001 on a separate plots, and be sure to add add a title and axis labels. Note that the results will not be too interesting. To make a quality Moody diagram we need more data points over a wider range of Re values, and we should use a log-scale. No worries, we will build up to this in future chapters. 79 Chapter 4 Logic and vectors In Chapter 4 we continue to build our foundational knowledge of MATLAB with the introduction of logical statements and vectors. By the end of this chapter you will be able to: • Demonstrate the construction of logical blocks • Recall how to dynamically create vectors in the context of for loops • Be able to pre-define a static vector and assign values to its elements • Apply the use of logical blocks and vectors to solve basic engineering problems If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 4.1 Checking preconditions Some of the loops in the previous chapter don’t work if the value of n isn’t set correctly before the loop runs. For example, this loop computes the sum of the first n elements of a geometric sequence: a1 = 1; total = 0; for i=1:n a = a1 * 0.5^(i-1); total = total + a; end ans = total It works for any positive value of n, but what if n is negative? In that case, you get: 81 t Without knowing it, we actually had this case in Exercise 3.6 in your nested loop to compute the factorial of 2*i+1. The inner (nested) loop should be for k=2:(2*i+1), where the outer loop should be for i=0:n, where n is the highest term in the sum. In the first pass, when i=0, the inner loop will be for k=2:1. MATLAB therefor did not perform the loop. It worked out correctly, because we initialize our factorial variable with a value of 1, and 1! is 1. 82 Chapter 4 Logic and vectors total = 0 Why? Because the expression 1:-1 means “all the numbers from 1 to –1, counting up by 1.” It’s not immediately obvious what that should mean, but MATLAB’s interpretation is that there aren’t any numbers that fit that description, so the result is >> 1:-1 ans = Empty matrix: 1-by-0 If the matrix is empty, you might expect it to be “0-by-0,” but there you have it. In any case, if you loop over an empty range, the loop never runs at all, which is why in this example the value of total is zero for any negative value of n. If you are sure that you will never make a mistake, and that the preconditions of your functions will always be satisfied, then you don’t have to check. But for the rest of us, it is dangerous to write a script, like this one, that quietly produces the wrong answer (or at least a meaningless answer) if the input value is negative. A better alternative is to use an if statement. 4.2 if The if statement allows you to check for certain conditions and execute statements if the conditions are met. In the previous example, we could write: t Remember, MATLAB’s documentation is your friend. Try help if and doc if. You can also do the same for else and elseif. t While I will not use them in the current version of the text, know that MATLAB does have an error function that can be used to throw an error and display a message, and also a warning function that can be used to display warning messages. if n<0 ans = NaN end The syntax is similar to a for loop. The first line specifies the condition we are interested in; in this case we are asking if n is negative. If it is, MATLAB executes the body of the statement, which is the indented sequence of statements between the if and the end. MATLAB doesn’t require you to indent the body of an if statement, but it makes your code more readable, so you should do it, and don’t make me tell you again. In this example, the “right” thing to do if n is negative is to set ans = NaN, which is a standard way to indicate that the result is undefined (not a number). If the condition is not satisfied, the statements in the body are not executed; put differently, if the condition is not met, no action is taken and the program proceeds. Sometimes there are alternative statements to execute when the condition is false. In that case you can extend the if statement with an else clause. The complete version of the previous example might look like this: 4.2 if if n<0 ans = NaN else a1 = 1; total = 0; for i=1:n a = a1 * 0.5^(i-1); total = total + a; end ans = total end Statements like if and for that contain other statements are called compound statements. All compound statements end with, well, end. Take note of my use of indentation. It is meant to make the code more readable. In this example, one of the statements in the else clause is a for loop. Putting one compound statement inside another is legal and common, and sometimes called nesting. To be completely clear as to what MATLAB is doing here, let’s walk through the flow of calculations. First, MATLAB checks whether n is less than 0. If it is, the if statement is true, and MATLAB sets ans = NaN and proceeds directly to the end statement nicely lined up with if and else. If n is equal to or greater than 0, the if statement is false, and MATLAB therefore proceeds to the else statement, and performs the calculations between else and the final end. Getting back to the use of indentation: if, else, and end make up a compound statement, and are all therefore aligned. We start with the if statement, and if it is false, we jump to else. They are aligned to make this clear. The other lines are indented to show they are part of this compound statement. With the if statement, we “open” our (outer) compound statement. With the for, we open a second, nested (inner) compound statement. We must “close” the for loop (or the inner statement) before we close the (outer) if compound statement. Therefore the first end statement ends (or closes) the for loop since it must be closed first. To make this clear, we line them up. I hope my long-winded discussion in the last two paragraphs has not confused you more. As we begin to look at examples and you begin practicing, it will all come together. Lastly, remember the else statement essentially means for all other cases. You can add additional cases beside that in the original if statement. This would be accomplished with an elseif statement, where you can have as many elseif statements as you would like. An example of this might be: 83 84 Chapter 4 Logic and vectors t Look carefully at the use of elseif here. If MATLAB gets to the elseif statement, it is because n ≥ 0. If statement 2 is performed, it is because n < 1 and the elseif statement was true. Putting it all together, statement 2 is performed if and only if 0 ≤ n < 1. If n is an integer, it must be 0. if n < 0 statement 1 elseif n < 1 statement 2 else statement 3 end If n is less than 0, execute statement 1 and proceed to end. If n is not less than 0, proceed to the elseif statement. Here we check if n is less than 1. If it is, execute statement 2 and then proceed to end. If n is not less than 1, proceed to else and evaluate statement 3. Please note that elseif is a single word; it is tempting to add a space, but that would be wrong and is a common error I have seen students make in the past. 4.3 Relational operators t I often forget if 1 represents “true” or “false”. This is a perfect case of using the command line to check your understanding (i.e., unit testing). Try something simple like 5<2 and/or 5>2. What is the result? t You must use caution when checking for equivalence with “==”. Remember, we have seen that MATLAB computes sin(pi)=1.2246e-16. Due to numerical limitations, it is not exactly 0. Instead of using if x==0, you might try instead if x > n-tiny && x < n+tiny, where “tiny” is a small number which you deem close enough to 0 for your application, and “n” is the value you are looking for. There will certainly be times when we will use “==”, such as when we are only dealing only integers. The operators that compare values, like < and > are called relational operators because they test the relationship between two values. The result of a relational operator is one of the logical values: either 1, which represents “true,” or 0, which represents “false.” Relational operators often appear in if statements, but you can also evaluate them at the prompt: >> x = 5; >> x < 10 ans = 1 You can assign a logical value to a variable: >> flag = x > 10 flag = 0 A variable that contains a logical value is often called a flag because it flags the status of some condition. The other relational operators are <= and >=, which are self-explanatory, ==, for “equal,” and ~=, for “not equal.” (In some logic notations, the tilde is the symbol for “not.”) Don’t forget that == is the operator that tests equality, and = is the assignment operator. If you try to use = in an if statement, you get a syntax error: 4.4 Logical operators >> if x=5 if x=5 | Error: The expression to the left of the equals sign is not a valid target for an assignment. Did you mean: >> x = 5 MATLAB thinks you are making an assignment to a variable named if x, and suggests using just x since spaces are not allowed in variable names. 4.4 Logical operators To test if a number falls in an interval, you might be tempted to write something like 0 < x < 10, but that would be wrong, so very wrong. Unfortunately, in many cases, you will get the right answer for the wrong reason. For example: >> x = 5; >> 0 < x < 10 ans = 1 % right for the wrong reason But don’t be fooled! >> x = 17 >> 0 < x < 10 ans = 1 % just plain wrong The problem is that MATLAB is evaluating the operators from left to right, so first it checks if 0<x. It is, so the result is 1. Then it compares the logical value 1 (not the value of x) to 10. Since 1<10, the result is true, even though x is not in the interval. For beginning programmers, this is an evil, evil bug! One way around this problem is to use a nested if statement to check the two conditions separately: ans = 0 if 0<x if x<10 ans = 1 end end or you can be creative with an elseif statement: 85 86 Chapter 4 Logic and vectors ans = 0 if 0>=x % I will provide no statement which means % do nothing if this is true, proceed to end. elseif x<10 % We only get to this elseif statement if x>0, % for which the first if statement is false. % If we find x<10, this is the same as % 0<x<10 ans = 1 end But it is more concise to use the AND operator, &&, to combine the conditions. >> x = 5; >> 0<x && x<10 ans = 1 >> x = 17; >> 0<x && x<10 ans = 0 The result of AND is true if both of the operands are true. The OR operator, ||, is true if either or both of the operands are true. Put differently, the OR operator is true is at least one of operands is true. You can have as many AND and/or OR operators you would like on a single line. Comparisons will be made from left to right, unless parentheses are added. 4.5 exist Let’s return to section 4.1 Checking preconditions. Our discussion so far has been limited to checking whether or not a variable (a pre-condition) is properly defined. For example, if I am solving a problem using the Antoine equation, is the specified temperature within the range of applicability. It may also be desirable to check to see if a variable (required as a pre-condition) is defined at all. Let’s P look at a basic example. I wish to compute ni=1 (i + 1)2 , where n is the number of terms to include in the sum; n is our required pre-condition. To accomplish this, I write the following script 4.5 exist 87 Listing 4.1 sum_test.m 1 2 3 4 5 6 7 8 9 10 % Script to compute the sum of (i+1)^2 from 1 to n % Precondition: n, the number of terms in the sum % Post-condition: total, contains the value of the sum total = 0 for i = 1:n total = total+(i+1)^(2); end total Let’s give it a try. >> n = 5; >> sum_test total = 0 total = 90 Next, let’s clear the variable n and see what would happen if we forget to define it. >> clear n; >> sum_test total = 0 Undefined function or variable 'n'. Error in sum_test (line 6) for i = 1:n The script returns the value of total = 0, which is the value we initialize it to, and then also an error message indicating that n is undefined along with the line on which the error was encountered. This is a good error message. However, if it is missed by the user for one reason or another, they have total = 0 which may be used in subsequent calculations. You may therefore wish to check to see if n has been defined. If it has, run the code as normal, if not, return an answer such as total = NaN and possibly an error message of your own. This can be accomplished with the exist function. There are two ways to call it, I prefer the natural function form so that I remember it is a function. When used to check for the existence of a variable, it can be thought of as a logical function, returning a value of 1 for true, the variable exists, and a value of 0 for false, the variable does not exist. It takes the basic form exist(’name’,’var’), where name is the name of the variable you are checking exists, and var indicates that we are checking for the existence of a variable. You can check for the existence of things besides variables, but you can read the documentation for that. Let’s try and integrate exist into our script. 88 Chapter 4 Logic and vectors Listing 4.2 sum_test_b.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 % Script to compute the sum of (i+1)^2 from 1 to n % Precondition: n, the number of terms in the sum % Post-condition: total, contains the value of the sum if exist('n','var') total = 0; for i = 1:n total = total+(i+1)^(2); end total else % n does not exist disp('The pre-condition n is not defined') total = NaN end Let’s give it a try >> sum_test_b The pre-condition n is not defined total = NaN Remember, another issue with scripts is they can read variables from and save variables to your workspace. What would be unfortunate is if total was a variable already defined in your workspace that you did not want to overwrite. Exist could be used to check if total is already defined. If it is, then you could save the current value to a new variable, or maybe you print a warning, or maybe you prevent the script from running all together. The choice is up to you. The last note I will make, if you need to check for the existence of more than one variable, then you can combine exist with the AND operator, &&. 4.6 Vectors The values we have seen so far are all single numbers, which are called scalars to contrast them with vectors and matrices, which are collections of numbers. In this chapter we will focus on the basics, in the context of loops. Later we will revisit the topic in greater detail. A vector in MATLAB is similar to a sequence in mathematics; it is a set of numbers that correspond to positive integers. What we called a “range” in the previous chapter was actually a vector. In general, anything you can do with a scalar, you can also do with a vector. You can assign a vector value to a variable: 4.7 Vector arithmetic >> X = 1:5 X = 1 2 3 89 4 5 Variables that contain vectors are often capital letters. That’s just a convention; MATLAB doesn’t require it, but for beginning programmers it is a useful way to remember what is a scalar and what is a vector. Just as with sequences, the numbers that make up the vector are called elements. 4.7 Vector arithmetic You can perform arithmetic with vectors, too. If you add a scalar to a vector, MATLAB increments each element of the vector: >> Y = X+5 Y = 6 7 8 9 10 The result is a new vector; the original value of X is not changed. If you add two vectors, MATLAB adds the corresponding elements of each vector and creates a new vector that contains the sums: >> Z = X+Y Z = 7 9 11 13 15 But adding vectors only works if the operands are the same size. Otherwise: >> W = 1:3 W = 1 2 3 >> X+W Error using + Matrix dimensions must agree. The error message in this case is confusing, because we are thinking of these values as vectors, not matrices. The problem is a slight mismatch between math vocabulary and MATLAB vocabulary. 4.8 Everything is a matrix In math (specifically in linear algebra) a vector is a one-dimensional sequence of values and a matrix is two-dimensional (and, if you want to think of it that way, a scalar is zero-dimensional). In MATLAB, everything is a matrix. t This gives us a deeper understanding of how a for loop works. MATLAB keeps track of the loop iteration number. Each iteration the loop index variable is set equal to that corresponding element of the specified vector. 90 Chapter 4 Logic and vectors You can see this if you use the whos command to display the variables in the workspace. whos is similar to who except that it also displays the size and type of each variable. First I’ll make one of each kind of value: >> scalar = 5 scalar = 5 >> vector = 1:5 vector = 1 2 3 4 5 >> matrix = ones(2,3) matrix = 1 1 1 1 1 1 t To get a vector of 1’s, try ones(1,3). MATLAB can also created a new matrix of 0’s, Try zeros(2,3). Both are useful for creating a new matrix of a given dimension and initializing it. What is the result if you try ones(3)? A 3 by 3 “square” matrix. Have a look at the documentation with the help command. ones is a function that builds a new matrix with the given number of rows (2) and columns (3), and sets all the elements to 1. Now let’s see what we’ve got. >> whos Name scalar vector matrix Size Bytes 1x1 1x5 2x3 8 40 32 Class Attributes double double double According to MATLAB, everything is a double array: “double” is another name for double-precision floating-point numbers, and “array” is another name for a matrix. The only difference is the size, which is specified by the number of rows and columns. The thing we called scalar is, according to MATLAB, a matrix with one row and one column. Our vector is really a matrix with one row and 5 columns. And, of course, matrix is a matrix, with 2 rows and 3 columns. The point of all this is that you can think of your values as scalars, vectors, and matrices, and I think you should, as long as you remember that MATLAB thinks everything is a matrix. Here’s another example where the error message only makes sense if you know what is happening under the hood: >> X = 1:5 X = 1 2 3 4 5 >> Y = 1:5 Y = 1 2 3 4 5 4.9 Indices 91 >> Z = X*Y Error using * Inner matrix dimensions must agree. First of all, * is the MATLAB function that performs matrix multiplication. The reason the “inner matrix dimensions must agree” is the way matrix multiplication is defined in linear algebra. Any scalar (a 1-by-1 matrix) may multiply anything. Otherwise, the number of columns in X has to equal the number of rows in Y (those are the inner dimensions). If you don’t know linear algebra, this doesn’t make much sense. When you saw X*Y you probably expected it to multiply each of the elements of X by the corresponding element of Y and put the results into a new vector. That operation is called elementwise multiplication, and the operator that performs it is .*: >> X .* Y ans = 1 4 9 16 25 We’ll get back to the elementwise operators later; you can forget about them for now. and elementwise exponentiation (.ˆ). 4.9 Indices You can select elements of a vector with parentheses: >> Y = 6:10 Y = 6 7 8 9 10 >> Y(1) ans = 6 >> Y(5) ans = 10 This means that the first element of Y is 6 and the fifth element is 10. The number in parentheses is called the index because it indicates which element of the vector you want. The index can be any kind of expression. >> i = 1; >> Y(i+1) ans = 7 t As we will see, we also have elementwise division (./) t So far we have only used the ones command to create a matrix, which is not too interesting. But to select an element of a matrix, we would have something like this: Y(3,4). The first number is the row index, and the second number is the column index. 92 Chapter 4 Logic and vectors Loops and vectors go together like the storm and rain. For example, this loop displays the elements of Y. for i=1:5 Y(i) end t length can also be used with a matrix to find the number of elements in a dimension (along a row or column). You can also ask for the length of a subset of a vector or matrix dimension. But more on this later... or have a look at the documentation if you are eager. And if you are really eager, when working with matrices the related function size can be very useful. Each time through the loop we use a different value of i as an index into Y. A limitation of this example is that we had to know the number of elements in Y. We can make it more general by using the length function, which returns the number of elements in a vector: for i=1:length(Y) Y(i) end There. Now that will work for a vector of any length. 4.10 Indexing errors An index can be any kind of expression, but the value of the expression has to be a positive integer, and it has to be less than or equal to the length of the vector. If it’s zero or negative, you get this: >> Y(0) Subscript indices must either be real positive integers or logicals. “Subscript indices” is MATLAB’s longfangled way to say “indices.” “Real positive integers” means that complex numbers are out. And you can forget about “logicals” for now; we will certainly come back to logicals later. If the index is too big, you get this: >> Y(6) Index exceeds matrix dimensions. There’s the “m” word again, but other than that, this message is pretty clear. Finally, don’t forget that the index has to be an integer: >> Y(1.5) Subscript indices must either be real positive integers or logicals. 4.11 Vectors and sequences 93 4.11 Vectors and sequences Vectors and sequences go together like ice cream and apple pie. For example, another way to evaluate the Fibonacci sequence is by storing successive values in a vector. Again, the definition of the Fibonacci sequence is F 1 = 1, F 2 = 1, and F i = F i −1 + F i −2 for i ≥ 3. In MATLAB, that looks like: Listing 4.3 fibonacci_seq_script.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 % Computing the Fibonacci sequence out to n % Precondition: n, the number of terms in the sequence % Postcondition: F, a vector for the Fibonacci sequence % where each element corresponds to the % term in the sequence. The final element % is stored to ans. % Initializing the sequence F(1) = 1; F(2) = 1; % Computing elements 3 to n for i=3:n F(i) = F(i-1) + F(i-2); end % Assigning the value of the nth element to ans ans = F(n) Notice that I am using a capital letter for the vector F and lower-case letters for the scalars i and n. At the end, the script extracts the final element of F and stores it in ans, since the result of this script is supposed to be the n th Fibonacci number, not the whole sequence. If you had any trouble with Example 3.7, you have to appreciate the simplicity of this version. The MATLAB syntax is similar to the math notation, which makes it easier to check correctness. The only drawbacks are • You have to be careful with the range of the loop. In this version, the loop runs from 3 to n, and each time we assign a value to the ith element. It would also work to “shift” the index over by two, running the loop from 1 to n-2: t You will notice that within the Editor window that the nice folks at MATLAB caution you about assigning a value to the variable ans. Why? Because when you perform a MATLAB operation and do not assign the result to a variable, MATLAB will assign the result to ans. So MATLAB is just reminding you so that you do not accidentally overwrite it. 94 Chapter 4 Logic and vectors Listing 4.4 fibonacci_seq_shift_script.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 % Computing the Fibonacci sequence out to n % Precondition: n, the number of terms in the sequence % Postcondition: F, a vector for the Fibonacci sequence % where each element corresponds to the % term in the sequence. The final element % is stored to ans. % Initializing the sequence F(1) = 1; F(2) = 1; % Computing elements 3 to n for i=1:n-2 F(i+2) = F(i+1) + F(i); end % Assigning the value of the nth element to ans ans = F(n) Either version is fine, but you have to choose one approach and be consistent. If you combine elements of both, you will get confused. I prefer the version that has F(i) on the left side of the assignment, so that each time through the loop it assigns the ith element. • If you really only want the nth Fibonacci number, then storing the whole sequence wastes some storage space. But if wasting space makes your code easier to write and debug, that’s probably okay. Example 4.1 Write a loop that computes the first n elements of the geometric sequence A i +1 = A i /2 with A 1 = 1. Notice that the math notation puts A i +1 on the left side of the equality. When you translate to MATLAB, you may want to shift the index. Note: We are only asked to compute the terms of the series and not the sum as we did in Example 3.4. 4.11 Vectors and sequences Solution: (Link to screen cast and accompanying M-file.) I will provide two examples. First, I will shift the index as suggested. To me, this makes more sense logically. Second, I will compute the terms using the notation as written. Listing 4.5 geometic_series_elements.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 % % Compute the first % Precondition: n, % Postcondition: at % we % n elements of the geometric seris A_i = A_{i-1}/2 the number of terms you wish to compute the conclusion of the loop, the n elements will stored to the vector A % The first element is known and equal to 1 A(1) = 1; % Computing elements 2 to n for i=2:n % I shifted the index by 1 and start my loop at 2. A(i) = A(i-1)/2; end % Displaying all n elements. If n is large, you may wish to remove this. A Listing 4.6 geometic_series_elements_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 % % Compute the first % Precondition: the % Postcondition: at % we % n elements of the geometric seris A_{i+1} = A_i/2 value of n, how many terms you wish to compute the conclusion of the loop, the n elements will stored to the vector A % The first element is known and equal to 1 A(1) = 1; % Computing elements 2 to n for i=1:n-1 % Notice here my loop only goes up to n-1 since I am computing % the term i+1. A(i+1) = A(i)/2; end % Displaying all n elements. If n is large, you may wish to remove this. A 95 96 Chapter 4 Logic and vectors 4.12 Sizing vectors Notice in the examples of Section 4.11 and in Example 4.1 we did not need to pre-define the size of the final vector. Our vectors were “dynamic”, their size changed every iteration. Let’s use the command line to take a look at what is going on: >> T(1) = 1 T = 1 >> T(2) = 12 T = 1 12 >> T(3) = 5 T = 1 12 5 We start with a vector of length 1. Then each time we reference a subsequent element, the length increases by 1. What would happen if we skipped ahead and assign a value to element 8? >> T(8) = 1 T = 1 12 5 0 0 0 0 1 The length of the vector is increased from 3 to 8, and the value of elements 4 to 7, which we “skipped”, are assigned a value of 0. Likewise, if we were to move next to the element in the 8th row and 12th column, all of the elements that we “skipped” will be assigned a value of zero: >> T(8,12) = 1 T = 1 12 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 Having dynamic vectors and matrices is convenient and may simplify your code. However, you may find that the MATLAB editor suggests that you pre-allocate your vectors and matrices when possible. The issue is that every time you re-size your vector or matrix, MATLAB needs to reassign memory. This can be a time consuming operation. For the purpose of this class, you could safely ignore the suggestion without any implications. However, if you know the final size of your 4.13 Creating vectors 97 matrix or length of your vector, a better practice would be to initialize (or create) the matrix or vector to be the final size using zeros or ones. This way memory never needs to be reassigned. For example, if we knew the final length of vector G would be 10, we might initially define G as: >> G = zeros(1,10) G = 0 0 0 0 0 0 0 0 0 0 For this class, you will not notice a change in efficiency. However, if you know the final size of your matrix or length of your vector, then initializing your matrix or vector to be the final size is the better practice. If you are interested in high performance computing, then getting in the habit of sizing your vectors and matrices up-front is a good idea. For these applications any time savings can have a huge impact. 4.13 Creating vectors Before moving on, I would like to pause, summarize, and go through some examples of creating vectors. Some commands we will use in this chapter, while some (most) will find use later on. But we will see them here, then get in more depth later. We first saw in the context of for loops that we could use the colon operator. The command begin:end will create a vector that goes from begin to end in increments of 1. The values of begin and end can be anything. But remember that the increment will be 1. Let’s consider a few examples and see why I emphasize this point. >> 1:5 ans = 1 >> 1.1:5.8 ans = 1.1000 2 3 2.1000 4 5 3.1000 4.1000 5.1000 >> 5:1 ans = 1x0 empty double row vector Beside the colon operator, we have the extended colon operator. The extended colon operator takes the form begin:increment:end, where here we can specify an increment value (beside the default value of 1). t Row and column vectors will be a topic of conversation later. For now, we will only consider row vectors. 98 Chapter 4 Logic and vectors >> 1:2:6 ans = 1 3 >> 1:1.5:6 ans = 1.0000 >> 6:-1:1 ans = 6 5 2.5000 5 4 4.0000 3 2 5.5000 1 We see that we can go backwards now too. We have seen the ones and zeros command, where here we specify we have only a single row to create a vector. >> ones(1,5) ans = 1 1 1 1 1 >> zeros(1,5) ans = 0 0 0 0 0 We have defined vectors dynamically: >> T(1) = 5 T = 5 >> T(2) = 3 T = 5 3 >> T(3) = 10 T = 5 3 10 If our goal was to create a vector with data that we know, we can accomplish this in a single command using brackets, with the elements separated either by a space or a comma. 4.13 Creating vectors >> R = [5 3 0] R = 5 3 99 0 >> S = [5, 3, 0] S = 5 3 0 For the stubborn Excel user reluctant to give MATLAB a try, you could likewise accomplish this graphically using the New Variable button on the Home tab. You Figure 4.1 The New Variable and can likewise edit an existing variable using the Open Variable button, or just douOpen Variable buttons located on ble click the variable name in the Workspace . To create a new variable, simply the Home tab. click New Variable , then type into a spreadsheet-like environment. To give the variable a name, click on the tab that currently says “unnamed”, and type in a variable name of your choice. This is best demonstrated in the following screen cast. Another useful command is linspace. I often find students using the function in class without ever mentioning it. The basic command is linspace(begin,end) which will create a vector of 100 equally spaced points between begin and end. If you want to use more or less than 100 points, you can use linspace(begin,end,n) where n is the number of points. This can be very useful for the examples where we have looped over a range of temperatures. Here the endpoints need not be integers. If we were to create a vector of temperatures using linspace and assigned it to variable T, we could then loop as for i=1:length(T), and each iteration point to the i th element of T. >> X = linspace(2.5, 12, 6) X = 2.5000 4.4000 6.3000 8.2000 10.1000 12.0000 And very last, we will end with the command rand. The basic form of the command is rand(1,n), which will create a vector of length n of uniformly distributed pseudorandom numbers between 0 an 1. Technically the numbers are pseudorandom as they are generated using a definite mathematical procedure, but for our purposes you can think of them as random. It takes the same form as ones and zeros, where the first argument is actually the number of rows. This will get much more attention later. >> Y=rand(1,3) Y = 0.8147 0.9058 0.1270 Again, we will not use most of these commands for some time yet. However, there is always a curiosity when we begin to discuss vectors, so here it is. When 100 Chapter 4 Logic and vectors we use the command in the text, we will give it a more proper introduction. But hopefully this satisfies your curiosity. Now back to our originally scheduled programming. 4.14 Plotting vectors Plotting and vectors go together like the moon and June, whatever that means. If you call plot with a single vector as an argument, MATLAB plots the indices on the x-axis and the elements on the y-axis. To plot the Fibonacci numbers we computed in Section 4.11: >> n = 10; >> fibonacci_seq_script ans = 55 >> plot(F) >> xlabel('n') >> ylabel('F_n') 60 50 Fn 40 30 20 10 0 1 2 3 4 5 6 7 8 9 10 n Figure 4.2 A plot of the first ten elements of the Fibonacci sequence. This display is often useful for debugging, especially if your vectors are big enough that displaying the elements on the screen is unwieldy. If you call plot with two vectors as arguments, MATLAB plots the second one as a function of the first; that is, it treats the first vector as a sequence of x values and the second as corresponding y value and plots a sequence of (x, y) points. 4.14 Plotting vectors >> >> >> >> >> 101 X = 1:5; Y = 6:10; plot(X, Y) xlabel('X') ylabel('Y') 10 9.5 9 Y 8.5 8 7.5 7 6.5 6 1 1.5 2 2.5 3 3.5 4 4.5 5 X Figure 4.3 An example of plotting two vectors, one against the other. By default, MATLAB draws a blue line, but you can override that setting with the same kind of string we saw in Section 3.2. For example, the string ’ro-’ tells MATLAB to plot a red circle at each data point; the hyphen means the points should be connected with a solid line. Rather than a solid line, we can obtain a dashed line with ’--’ and a dotted line with ’:’. In this example, I stuck with the convention of naming the first argument X (since it is plotted on the x-axis) and the second Y. There is nothing special about these names; you could just as well plot X as a function of Y. MATLAB always treats the first vector as the “independent” variable, and the second as the “dependent” variable (if those terms are familiar to you). I should also point out that despite plotting multiple data points, five in this case, I did not need to use the hold on command. The reason is that MATLAB treats this a single set. If we wanted to add any additional points to the plot using a subsequent plot command, then we would need to use hold on. Of course I could have just used hold on from the start and nothing would have changed. t When overriding the default settings and specify the line and/or symbol type and the color, MATLAB does not care about the order. Try for example ’ro-’, ’r-o’, and ’-or’. You should find they all yield the same result. 102 Chapter 4 Logic and vectors 4.15 Reduce A frequent use of loops is to run through the elements of an array and add them up, or multiply them together, or compute the sum of their squares, etc. This kind of operation is called reduce, because it reduces a vector with multiple elements down to a single scalar. For example, this loop adds up the elements of a vector named X (which we assume has been defined). total = 0 for i=1:length(X) total = total + X(i) end ans = total The use of total as an accumulator is similar to what we saw in Section 3.4. Again, we use the length function to find the upper bound of the range, so this loop will work regardless of the length of X. Each time through the loop, we add in the ith element of X, so at the end of the loop total contains the sum of the elements. This is additionally an excellent example to practice with because MATLAB has a built-in function sum that does the same thing. You could use it to check the script for correctness as sum(X). Example 4.2 Write a similar loop that multiplies all the elements of a vector together. You might want to call the accumulator total_prod, and you might want to think about the initial value you give it before the loop. Solution: (Link to screen cast and accompanying M-file.) Careful here. Do not initialize your accumulator variable to a value of zero. Anything times zero is zero. This is fine when performing a summation, but not a continuous product. Initialize it to a value of one. 4.16 Apply 103 Listing 4.7 vector_continuous_product.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 % Multiply all of the elements of a vector X together % % Precondition: X, vector containing the sequence of elements % Postcondition: total_prod, the result of multiplying all of the elements % of the vector X together % % Initializing the accumulator variable total_prod = 1; % Computing the continuous product for i=1:length(X) total_prod = total_prod*X(i); end % Displaying the final result total_prod The calculation corresponds to computing a continuous product. Like computing the sum, the continuous product is a common operation. And as you might expect, MATLAB has a built-in function for computing the continuous product that you can use to check the correctness of your code. The function is prod, and you would use it here as prod(X). 4.16 Apply Another common use of a loop is to run through the elements of a vector, perform some operation on the elements, and create a new vector with the results. This kind of operation is called apply, because you apply the operation to each element in the vector. For example, the following loop computes a vector Y that contains the squares of the elements of X (assuming, again, that X is already defined). for i=1:length(X) Y(i) = X(i)^2 end Example 4.3 Write a loop that computes a vector Y that contains the sines of the elements of X. To test your loop, write a script that (a) Uses linspace to assign to X a vector with 100 elements running from 0 to 2π (X = linspace(0,2*pi)). t Remember that X(i) refers to a single element of the vector X. We can therefore use ˆ and do not need .ˆ, although both would give the same result. t Later on when we discuss elementwise operations, you will find we could just instead do Y = X.ˆ2 and get the same result. Further, functions like sin operate elementwise. So in the next example, we could replace the entire loop with Y=sin(X) and get the same result. 104 Chapter 4 Logic and vectors (b) Uses your loop to store the sines in Y. (c) Plots the elements of Y as a function of the elements of X. Solution: (Link to screen cast and accompanying M-file.) Let’s put all of the parts in a single script. For more information on how to use linspace, have a look at MATLAB’s documentation (doc or help). Listing 4.8 sine_plot.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 % Plot sine over the range 0 to 2*pi % % Precondition: None % Postcondition: Vector X will contain 100 elements from 0 to 2*pi, % vector Y will contain the corresponding value of sine, % and a plot of X vs Y will be produced % % Vector X containing angles from 0 to 2*pi X = linspace(0,2*pi); % Pre-size our vector Y that will contain the sine of X Y = zeros(1,length(X)); for i=1:length(X) Y(i) = sin(X(i)); end % Plotting clf; % clear figure, just in case plot(X,Y) xlabel('\theta [radians]') ylabel('sin(\theta)') xlim([0,2*pi]) % Finally, saving and printing the figure savefig('sine_plot.fig') print('-depsc','sine_plot.eps') 4.17 Search Yet another use of loops is to search the elements of a vector and return the index of the value you are looking for (or the first value that has a particular property). For example, if a vector contains the computed altitude of a falling object, you might want to know the index where the object touches down (assuming that the ground is at altitude 0). To create some fake data, we’ll use an extended version of the colon operator: 4.17 Search 105 X = 10:-1:-10 The values in this range run from 10 to –10, with a step size of -1. The step size is the interval between elements of the range. The following loop finds the index of the element 0 in X: for i=1:length(X) if X(i) == 0 ans = i end end One funny thing about this loop is that it keeps going after it finds what it is looking for. That might be what you want; if the target value appears more than once, this loop provides the index of the last one. But if you want the index of the first one (or you know that there is only one), you can save some unnecessary looping by using the break statement. for i=1:length(X) if X(i) == 0 ans = i break end end break does pretty much what it sounds like. It ends the loop and proceeds immediately to the next statement after the loop (in this case, there isn’t one, so the script ends). This example demonstrates the basic idea of a search, but it also demonstrates a dangerous use of the if statement. Remember that floating-point values are often only approximately right. That means that if you look for a perfect match, you might not find it. For example, try this: X = linspace(1,2) for i=1:length(X) Y(i) = sin(X(i)) end plot(X, Y) You can see in the plot that the value of sin (x) goes through 0.9 in this range, but if you search for the index where Y(i) == 0.9, you will come up empty. t Try writing a for loop with the extended version of the colon operator. This gives you the ability to write a for loop that increments by a value other than 1 each iteration. 106 Chapter 4 Logic and vectors for i=1:length(Y) if Y(i) == 0.9 ans = i break end end The condition is never true, so the body of the if statement is never executed. Even though the plot shows a continuous line, don’t forget that X and Y are sequences of discrete (and usually approximate) values. As a rule, you should (almost) never use the == operator to compare floating-point values. There are a number of ways to get around this limitation; we will get to them later. Example 4.4 Write a loop that finds the index of the first negative number in a vector and stores it in ans. If there are no negative numbers, it should set ans to –1 (which is not a legal index, so it is a good way to indicate the special case). t Instead of assigning –1 to ans if there are no negative numbers, 0 would be another excellent option. Not only is 0 not a valid element index, but in the context of logicals, 0 means false (or not true). Solution: (Link to screen cast with accompanying M-file.) Let’s start by creating an array of fake data. >> X = 6:-1:-4; The first negative number is –1 and it is the 8th element of the vector X. Listing 4.9 negative_element.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 % Find the index of the first negative number in vector X % % Precondition: vector X % Postcondition: ans will contain the index of the first negative number % of vector X % Initialize ans with value of -1, so if there is no negative number % it will return this value. ans = -1; for i=1:length(X) if X(i) < 0 ans = i break end end >> negative_element ans = 8 4.18 Spoiling the fun 4.18 Spoiling the fun Experienced MATLAB programmers would never write the kind of loops in this chapter, because MATLAB provides simpler and faster ways to perform many reduce, filter and search operations. For example, the sum function computes the sum of the elements in a vector and prod computes the product. Many apply operations can be done with elementwise operators. The following statement is more concise than the loop in Section 4.16 Y = X .^ 2 Also, most built-in MATLAB functions work with vectors: X = linspace(0, 2*pi) Y = sin(X) plot(X, Y) Finally, the find function can perform search operations, but understanding it requires a couple of concepts we haven’t got to, so for now you are better off on your own. We will take a look at it later. I started with simple loops because I wanted to demonstrate the basic concepts and give you a chance to practice. At some point you will probably have to write a loop for which there is no MATLAB shortcut, and you have to work your way up from somewhere. If you understand loops and you are comfortable with the shortcuts, feel free to use them! Otherwise, you can always write out the loop. Note, if you are interested in high performance computing applications, vector operations with MATLAB are significantly more efficient than the use of for loops. Example 4.5 Write an expression that computes the sum of the squares of the elements of a vector. Solution: Assume we have a vector X. Based on what you just read you might try: >> X = 1:10; >> X2 = X.^2; >> sum_square = sum(X2) sum_square = 385 But if we search for “sum squared,” you will find that MATLAB can do even better. >> sum_square = sumsqr(X) sum_square = 385 107 108 Chapter 4 Logic and vectors 4.19 Strings The chapter is titled “Logic and vectors”. Now that we have discussed them both, I would like to take a minute to discuss strings. In you code you may find it desirable to compare strings in a logical statement. You can do this, but you need to be very careful. When you create a string, MATLAB saves the string as a vector, with the index going 1 to the number of letters, from left to right. >> test = 'Hello'; >> whos whos Name Size test 1x5 Bytes 10 Class char Attributes test is a vector of length 5. >> test test = Hello >> test(1) ans = H >> test(4) ans = l Later we will discuss the use of applying logical operators on vectors. But two cautionary notes. When comparing strings: 1) MATLAB will compare each element for equivalence and return a vector of 1’s and 0’s, 2) MATLAB is case sensitive. You will notice that I tend to avoid logical comparisons with strings, but there are always students in class that immediately want to use strings in logical comparisons, so I mention this here. 4.20 Examples Example 4.6 The ratio of consecutive Fibonacci numbers, F n+1 /F n , converges to a constant value as n increases. Write a script that computes a vector with the first n elements of a Fibonacci sequence (assuming that the variable n is defined), and then computes a new vector that contains the ratios of consecutive Fibonacci numbers. Plot this vector to see if it seems to converge. What value does it converge on? (Running, the vector seems to converge on a value of 1.6180.) 4.20 Examples 109 Solution: (Link to screen cast with accompanying M-file.) Listing 4.10 fib_ratio.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 % % % % % % % % % % % Computing the nth Fibonacci number and the ratio of consecutive numbers Pre-condition: n, the number of terms in the Fibonacci series Post-condition: Vector F will contain the Fibonacci number of each each element index, and F_ratio will contain the ratio of consecutive Fibonacci numbers F(i+1)/F(i), beginning with i = 2. We will also plot F_ratio to look for convergence. % Since we know the final size of F and F_ratio, let's presize the vectors F = zeros(1,n); F_ratio = zeros(1,n); % Initializing F(1) = 1; F(2) = 1; F_ratio(1) = 0; F_ratio(2) = F(2)/F(1); for i=3:n F(i) = F(i-1)+F(i-2); F_ratio(i) = F(i)/F(i-1); end % Clear our figure clf % Plot the Fibonacci ratio plot(F_ratio,'ro-') ylabel('Fibonacci ratio') xlabel('i') % Saving and printing the figure savefig('fib_ratio.fig') print('-depsc','fib_ratio.eps') 110 Chapter 4 Logic and vectors Example 4.7 A certain famous system of differential equations can be approximated by a system of difference equations that looks like this: x i +1 = y i +1 = z i +1 = ¡ ¢ xi + σ y i − xi d t £ ¤ y i + x i (r − z i ) − y i d t ¡ ¢ z i + x i y i − bz i d t (4.1) (4.2) (4.3) • Write a script that computes the first 10 elements of the sequences X , Y and Z and stores them in vectors named X, Y and Z. Use the initial values X 1 = 1, Y1 = 2 and Z1 = 3, with values σ = 10, b = 8/3 and r = 28, and with time step d t = 0.01. • Read the documentation for plot3 and comet3 and plot the results in 3 dimensions. • Once the code is working, use semi-colons to suppress the output and then run the program with sequence length 100, 1000 and 10000. • Run the program again with different starting conditions. What effect does it have on the result? • Run the program with different values for σ, b and r and see if you can get a sense of how each variable affects the system. 4.20 Examples 111 Solution: Listing 4.11 diffe_sequence.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 % % % % % % % Solving the certain famous system of differential equations using the equations and conditions provided in the problem. Pre-condition: None. I will use provided parameters/settings Post-condition: 3-dimensional plots of the results as a function of time-step. The numerical results will be stored to the vectors X, Y, and Z. % Clearing the variables just in case, since X, Y, and Z, are popular % choices, or in case you just ran the script with a different number of % time-steps. clear X Y Z; % The number of timesteps n = 10000; % X Y Z Since we know the final size of vectors X, Y and Z, let's pre-size them = zeros(1,n); = zeros(1,n); = zeros(1,n); % Initial values X(1) = 1; Y(1) = 2; Z(1) = 3; % Parameters sigma = 10; b = 8/3; r = 28; % Time-step dt = 0.01; for i = 1:(n-1) X(i+1) = X(i)+sigma*(Y(i)-X(i))*dt; Y(i+1) = Y(i)+(X(i)*(r-Z(i))-Y(i))*dt; Z(i+1) = Z(i)+(X(i)*Y(i)-b*Z(i))*dt; end % For the first part of the problem we are asked to dispaly the values % when n <= 10 if n <= 10 disp(X) disp(Y) disp(Z) end % Only generate 1 of the plots. Uncomment 1 and comment the other % to switch %plot3(X,Y,Z) comet3(X,Y,Z) 112 Chapter 4 Logic and vectors Example 4.8 The logistic map is often cited as an example of how complex, chaotic behavior can arise from simple non-linear dynamical equations. It was popularized in a seminal 1976 paper by the biologist Robert May. 1 It has been used to model the biomass of a species in the presence of limiting factors such as food supply and disease. In this case, there are two processes at work: (1) A reproductive process increases the biomass of the species in proportion to the current population. (2) A starvation process causes the biomass to decrease at a rate proportional to the carrying capacity of the environment less the current population. Mathematically this can be written as X i +1 = r X i (1 − X i ) where X i is a number between zero and one that represents the biomass at year i , and r is a positive number that represents a combined rate for reproduction and starvation. • Write a script named logmap that computes the first 50 elements of X with r=3.9 and X1=0.5, where r is the parameter of the logistic map and X1 is the initial population. • Plot the results for a range of values of r from 2.4 to 4.0. How does the behavior of the system change as you vary r . • One way to characterize the effect of r is to make a plot with r on the x-axis and biomass on the y axis, and to show, for each value of r , the values of biomass that occur in steady state. See if you can figure out how to generate this plot. 1 This was adopted from the “Logistic map” Wikipedia page, which you can see for additional details. 4.20 Examples 113 Solution: Listing 4.12 logmap_1.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 % % % % % % % % The logistic map. Note that I will save my files as logmap_*, where * will be 1, 2, or 3 for the first, second, and third bullet respectively. Pre-condition: None. I will hard-code everything Post-condition: Vector X will have the first 50 elements of the series. That is, it will contain the population for years 1 to 50, corresponding to the index number. % Clearing the variable just in case since X is a % also a good idea in case you previously ran the % value of n. I'm not sure if I have mentioned it % clear is another one of those commands where we % supress output... there isn't any to supress. clear X popular choice. This is script with a different before, but note that do not need a ; to n = 50; % Since we know the final size of X, let's pre-size the vector X = zeros(1,n); % Initial population X(1) = 0.5; % Combined rate for reproduction and starvation r = 3.9; for i=1:(n-1) X(i+1)=r*X(i)*(1-X(i)); end % Plotting the result plot(X) xlabel('i') xlim([1,n]) ylabel('X') % Saving and printing the figure savefig('logmap_1.fig') print('-depsc','logmap_1.eps') 114 Chapter 4 Logic and vectors Listing 4.13 logmap_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 % % % % % % % % % % The logistic map. Note that I will save my files as logmap_*, where * will be 1, 2, or 3 for the first, second, and third bullet respectively. Pre-condition: None. I will hard-code everything Post-condition: Vector X will have the first 50 elements of the series. That is, it will contain the population for years 1 to 50, corresponding to the index number. In this case, it will be the last value of r we consider in the vector. More imporant will be our plot of X for values of r % Clearing the variable just in case since X is a popular choice. This is % also a good idea in case you previously ran the script with a different % value of n. clear X % Initial population X(1) = 0.5; % % % % R Combined rate for reproduction and starvation For part b, let's make r a vector of values from 2.4 to 4 in increments of 0.2 using the extended colon operator. Note I will use R instead of r because now it is a vector. = 2.4:0.2:4.0; n = 50; % Clear our figure clf % Don't erase points each time we call plot hold on % Looping over all values of r, then all years of interest. % For each value of r, we will plot the result before moving on to the next % value. Notice that in my plot command I do not specify the color. This % way MATLAB will automatically change the color for each new set. for j = 1:length(R) for i=1:(n-1) X(i+1)=R(j)*X(i)*(1-X(i)); end plot(X) end % Now let's make the plot look pretty and save it too. xlabel('i') xlim([1,n]) ylabel('X') % Saving and printing the figure savefig('logmap_2.fig') print('-depsc','logmap_2.eps') 4.20 Examples 115 Note that when plotting the result for multiple values of r , it might be nice to add a legend. I did not do so here as the plot was already very busy. However, doing so is straightforward and I will demonstrate it for you as an example. First, this is greatly facilitated by the command num2str which takes an argument of a number, and converts it to a string. Second, we update the plot command to name each data set as it is plotted. This is accomplished with the addition of ’DisplayName’ followed by the string you would like to use to label the data set. Third, since we have already attached names to the data sets, we need just tell MATLAB to display the legend with the command legend. Have a look at my updated code below. Also, this is a very nice example of how to efficiently add legends to plots with multiple data sets, that is particularly useful with for loops. Know that I had never did this before this example. How do you think I figured it out? If you said I looked in the MATLAB documentation, then you are correct. Have a look at the documentation page appropriately titled: “Add Legend to Graph.” It contains a section with an example to “Create Simple Legend,” which is what we discussed previously, followed by a a section with an example to “Specify Labels Using DisplayName.” While not used here, I will also point out that for a long legend like ours, you could look at the section “Legend with Multiple Columns” to learn how to to create a legend with multiple columns. So the moral of the story is, if there is anything you ever wished you could do with MATLAB, search the documentation. Listing 4.14 logmap_2b.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 % % % % % % % % % % % % % % The logistic map. Note that I will save my files as logmap_*, where * will be 1, 2, or 3 for the first, second, and third bullet respectively. This version has been added to demonstrate the use of num2str to add a legend. Pre-condition: None. I will hard-code everything Post-condition: Vector X will have the first 50 elements of the series. That is, it will contain the population for years 1 to 50, corresponding to the index number. In this case, it will be the last value of r we consider in the vector. More imporant will be our plot of X for values of r % Clearing the variable just in case since X is a popular choice. This is % also a good idea in case you previously ran the script with a different % value of n. clear X % Initial population X(1) = 0.5; % % % % R Combined rate for reproduction and starvation For part b, let's make r a vector of values from 2.4 to 4 in increments of 0.2 using the extended colon operator. Note I will use R instead of r because now it is a vector. = 2.4:0.2:4.0; n = 50; 116 Chapter 4 Logic and vectors 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 % Clear our figure clf % Don't erase points each time we call plot hold on % Looping over all values of r, then all years of interest. % For each value of r, we will plot the result before moving on to the next % value. Notice that in my plot command I do not specify the color. This % way MATLAB will automatically change the color for each new set. % % Notice my updated plot call. In this updated version we are naming the % data set. Or you can essentially think of it as we are attaching the % legend label to the set as we plot it. for j = 1:length(R) for i=1:(n-1) X(i+1)=R(j)*X(i)*(1-X(i)); end plot(X,'DisplayName',num2str(R(j))) end % Now let's make the plot look pretty and save it too. xlabel('i') xlim([1,n]) ylabel('X') % We alreay attached the legend names to each data set. Now we just need to % tell MATLAB to display the legend. legend % Saving and printing the figure savefig('logmap_2b.fig') print('-depsc','logmap_2b.eps') 4.20 Examples 117 Listing 4.15 logmap_3.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 % % % % % % The logistic map. Note that I will save my files as logmap_*, where * will be 1, 2, or 3 for the first, second, and third bullet respectively. Pre-condition: None. I will hard-code everything Post-condition: Plot of steady state X versus r % Clearing the variable just in case since X is a popular choice clear X; % Initial population X(1) = 0.5; % Combined rate for reproduction and starvation % From part b, we found that for r = 4, the steady state population was 0. % Then decreasing in increments of 0.1, the result is chaotic until we get to % 2.9. Then the population quickly converges to a steady state value. R = 0:0.01:2.99; n = 50000; % clear our figure clf % don't erase points each time we call plot hold on % Looping over all values of r, then all years of interest for j = 1:length(R) for i=1:(n-1) X(i+1)=R(j)*X(i)*(1-X(i)); end plot(R(j),X(n),'ro') end % Now let's make the plot look pretty and save it too. xlabel('i') ylabel('X') % I will set the x-axis range to go from the value of the first to last % element of R. The first element is R(1). But what about the last. Well, % the current value of j is that of the last loop iteration, which was % equal to length(R). So we could use j or length(R) for the index. Or as % we will see in the future, we could use end. xlim([R(1),R(j)]) % Saving and printing the figure savefig('logmap_3.fig') print('-depsc','logmap_3.eps') 118 Chapter 4 Logic and vectors 4.21 CPB Examples Example 4.9 We have worked with the Antoine equation for ethanol in several problems now. Here we will continue the fun. Let’s revisit Example 1.5, 2.4, and 3.9. When working with the Antoine equation, it is important to take note of the specified temperature range over which use of the expression is recommended. While sometimes you have no choice but to extrapolate your Antoine equation beyond this range, it certainly is not a best practice. a) Update your script from Example 2.4 part a) to compute vapor pressure to check that the specified temperature is within the range of applicability. If the temperature is outside the range, how might you communicate this to the user? b) Update your script from Example 2.4 part c) to compute saturation temperature to check that the specified pressure is within the range of applicability. If the pressure is outside the range, how might you communicate this to the user? c) Modify your script from Example 3.9 so that in the loop it stores the value of T and log10 p sat to a vector. Then once the loop is complete, create your Clapeyron plot using your vectors. Compare to your graph from Example 3.9. Note that when plotting vectors, MATLAB plots all of the points at once. For this case you therefore no longer need hold on. Represent your data as a line with a circle for each data point. Label your axis and give your plot a title. Solution: For this exercise we are updating solutions to previous problems with some of the new tricks we have learned in this chapter. Namely in (a) and (b) we will add logical checks so that the calculation is only performed under certain conditions; here the logical check will be to use the Antoine equation only if it is within the range of applicability. If the use of Antoine equation is not appropriate, we will display a message and also store NaN to the postcondition (solution) variables. We use NaN because it will be propagated through subsequent calculations, so that the end result is NaN. In (a) we are updating Listing 2.5 and in (b) we are updating Listing 2.6. In (c) we will use a vector to store the results of each loop iteration, and will then apply plot to vectors. Remember that vector indices start at 1. So if were were to loop over temperature in degrees C, if the loop index starts at 0 we will need to shift up by 1 to refer to the appropriate vector index. In (c) we are updating Listing 3.12. 4.21 CPB Examples 119 Listing 4.16 Example_4_9a.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 % % % % % % % % % % % % % Calculating the vapor pressure of ethanol using Antione's equation Dr. Paluch, Example 4.9a Pre-condition: A, B, and C (for Antione's equation which uses mmHg and oC) stored to variables a, b, and c, and the temperature in K stored to variable t_K. Please also store Tmin and Tmax, the minimum and maximum temperature, in K, to variables t_K_min and t_K_max. Post-condition: Psat in units of mmHg (p_mmHg), atm (p_atm), kPa (p_kPa), and bar (psat_bar), and temperature in degrees C (t_C) % Check that we are in the range of applicability if t_K >= t_K_min && t_K <= t_K_max % First, let's convert T from oC to K t_C = t_K-273.15 % Calcutating logPsat, with Psat in mmHg log_p_mmHg = a-b/(t_C+c); % Psat in mmHg p_mmHg = 10^log_p_mmHg % Psat in atm p_atm = p_mmHg/760 % Psat in kPa p_kPa = p_atm*101.325 % Psat in bar p_bar = p_kPa/100 % Else, use is no appropriate else disp('Your input temperature is outside the range of use.') p_mmHg = NaN; p_atm = NaN; p_kPa = NaN; p_bar = NaN; end 120 Chapter 4 Logic and vectors Listing 4.17 Example_4_9b.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 % At a given pressure, calculate the correspnoding boiling point % (or saturation temperature) using Antione's equation. % % Dr. Paluch, Example 4.9b % % Pre-condition: A, B, and C (for Antione's equation which uses mmHg and % oC) stored to variables a, b, and c, and the pressure % in units of atm stored to p_atm. Please also store Tmin and % Tmax, the minimum and maximum temperature, in K, to % t_K_min and t_K_max % Post-condition: The saturation temperature in units of oC (t_C) and K % (t_K) % % Convert the inputed pressure from atm to mmHg p_mmHg = p_atm*760; % Calculating the saturation temperature t_C = b/(a-log10(p_mmHg))-c; t_K = t_C + 273.15; % Check that we are within the range of applicability if t_K >= t_K_min && t_K <= t_K_max % Note display is similar to disp, only it displays the variabale name too. display(t_C) display(t_K) % Else, use is no appropriate else disp('Your input pressure is outside the range of use.') t_C = NaN; t_K = NaN; end 4.21 CPB Examples 121 Listing 4.18 Example_4_9c.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 % % % % % % % % % % Plotting the vapor pressure of ethanol using Antione's equation from 0 to 100 oC Dr. Paluch, Example 4.9c Pre-condition: A, B, and C (for Antione's equation which uses mmHg and oC) stored to variables a, b, and c. Post-condition: Plot of log10 psat vs 1/T where T is in units of K and psat is in units of mmHg % Start by clearing our figure, just in case clf % Clear/delete our vectors in case we previously ran with a different, % larger temperature range clear log_p_mmHg tinv t_C % We need to calculated log psat over the range 0 to 100 C for t_C = 0:100 % Calcutating logPsat, with Psat in mmHg % Remember loop indices start at 1, so we need to shift up by 1 log_p_mmHg(t_C+1) = a-b/(t_C+c); % Calculating 1/T with T in units of K tinv(t_C+1) = 1/(t_C+273.15); end % plotting plot(tinv,log_p_mmHg,'-ro') % labeling the axis xlabel('1/T [1/K]') ylabel('log10 p/mmHg') % plot title title('Clapeyron plot for ethanol for 0 to 100 C') % Saving and printing to file savefig('Exercise_4_9c.fig') print('-depsc','Exercise_4_9c.eps') 122 Chapter 4 Logic and vectors While in (c) we “shift up by 1”, this may fail if the temperature range were to change and would certainly fail if we decided to increment the temperature by a value other than 1. Next, let’s consider two alternatives. First, the use of a counter variable is a robust and general solution to overcome these limitations. Here’s what (c) might look like using a counter variable (c): Listing 4.19 Example_4_9c_counter.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 % % % % % % % % % % % Plotting the vapor pressure of ethanol using Antione's equation from 0 to 100 oC. In this version of the code we will use a counter variable. Dr. Paluch, Example 4.9c Pre-condition: A, B, and C (for Antione's equation which uses mmHg and oC) stored to variables a, b, and c. Post-condition: Plot of log10 psat vs 1/T where T is in units of K and psat is in units of mmHg % Start by clearing our figure, just in case clf % Clear/delete our vectors in case we previously ran with a different, % larger temperature range clear log_p_mmHg tinv t_C % Initialize our counter variable n = 0; % We need to calculated log psat over the range 0 to 100 C for t_C = 0:100 % Increment the counter variable by 1 each loop iteration n = n+1; % Calcutating logPsat, with Psat in mmHg log_p_mmHg(n) = a-b/(t_C+c); % Calculating 1/T with T in units of K tinv(n) = 1/(t_C+273.15); end % plotting plot(tinv,log_p_mmHg,'-ro') % labeling the axis xlabel('1/T [1/K]') ylabel('log10 p/mmHg') % plot title title('Clapeyron plot for ethanol for 0 to 100 C') % Saving and printing to file savefig('Example_4_9c_counter.fig') print('-depsc','Example_4_9c_counter.eps') 4.21 CPB Examples 123 By counter variable, I just mean using a variable to keep track of the loop iteration number. But we can do even better. Let’s start by creating a vector of our temperatures, then we can just loop over the vector indices. This will also allow us to pre-size all of the vectors used. Listing 4.20 Example_4_9c_vector.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 % % % % % % % % % % % Plotting the vapor pressure of ethanol using Antione's equation from 0 to 100 oC. In this version of the code we will created a vector of temperatures, then loop over the vector indices. Dr. Paluch, Example 4.9c Pre-condition: A, B, and C (for Antione's equation which uses mmHg and oC) stored to variables a, b, and c. Post-condition: Plot of log10 psat vs 1/T where T is in units of K and psat is in units of mmHg % Start by clearing our figure, just in case clf % Clear/delete our vectors in case we previously ran with a different, % larger temperature range clear log_p_mmHg tinv t_C % Creating our vector of temperatures in C. We can use the standard colon % operator, or we could use linspace. Let's use the colon operator to be % consistent with our previous solutions. t_C = 0:100; % Pre-size the over vectors used log_p_mmHg = zeros(1,length(t_C)); tinv = zeros(1,length(t_C)); % Looping over our temperatures for i=1:length(t_C) % Calcutating logPsat, with Psat in mmHg log_p_mmHg(i) = a-b/(t_C(i)+c); % Calculating 1/T with T in units of K tinv(i) = 1/(t_C(i)+273.15); end % plotting plot(tinv,log_p_mmHg,'-ro') % labeling the axis xlabel('1/T [1/K]') ylabel('log10 p/mmHg') % plot title title('Clapeyron plot for ethanol for 0 to 100 C') 124 Chapter 4 Logic and vectors 47 % Saving and printing to file 48 savefig('Example_4_9c_vector.fig') 49 print('-depsc','Example_4_9c_vector.eps') And in the spirit of “Spoiling the fun” Listing 4.21 Example_4_9c_vector_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 % % % % % % % % % % % Plotting the vapor pressure of ethanol using Antione's equation from 0 to 100 oC. In this version of the code we will created a vector of temperatures, and then spoil all of the fun with vector operations. Dr. Paluch, Example 4.9c Pre-condition: A, B, and C (for Antione's equation which uses mmHg and oC) stored to variables a, b, and c. Post-condition: Plot of log10 psat vs 1/T where T is in units of K and psat is in units of mmHg % Start by clearing our figure, just in case clf % Clear/delete our vectors in case we previously ran with a different, % larger temperature range clear log_p_mmHg tinv t_C % Creating our vector of temperatures in C. We can use the standard colon % operator, or we could use linspace. Let's use the colon operator to be % consistent with our previous solutions. Notice in this version of the % script I will update the variable names to be consistent with the % convention of using capital letters to indicate vectors. T_C = 0:100; % Calculating logPsat, with Psat in mmHg using elementwise operations Log_p_mmHg = a-b./(T_C+c); % Calculating 1/T with T in units of K Tinv = 1./(T_C+273.15); % plotting plot(Tinv,Log_p_mmHg,'-ro') % labeling the axis xlabel('1/T [1/K]') ylabel('log10 p/mmHg') % plot title title('Clapeyron plot for ethanol for 0 to 100 C') % Saving and printing to file savefig('Example_4_9c_vector_2.fig') print('-depsc','Example_4_9c_vector_2.eps') 4.21 CPB Examples 125 Example 4.10 We have now worked with the Maxwell-Boltzmann equation in every chapter now: Example 1.6,2.5,and 3.10. In Example 3.10 you wrote a script to calculate the average speed of water and ethanol over the range 0 to 100 ◦ C and plotted the results. Here, repeat the calculations but now store the results to vectors. Then create a plot of your results. Use a different symbol and/or line to represent water and ethanol. Label your axis, give your plot a title, and add a legend. Note you will be plotting two sets of data. If you issue two separate plot commands, you will need to use hold on. Solution: Similar to (c) in Example 4.9, here we are updating Listing 3.13 to store the results of each loop iteration to a vector, and then plot the vector results. Remember that vector indices start at 1. So when my loop index starts at 0, I will need to shift up by 1 to refer to the appropriate vector index. Listing 4.22 Example_4_10.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 % % % % % % % % % % Plotting the average speed of water and ethanol molecules over the range 0 to 100 oC Dr. Paluch, Example 4.10 Pre-condition: None. I will hardcode everything since we are asked for a specific temperature range and the two species are specified. Post-condition: Plot of average molecular speed versus temperature % Start by clearing our figure clf % Need hold on because we will plot two sets of data hold on % gas constant in J/(mol K) r = 8.314; % Molecular weight of water and ethanol in kg/mol mw_water = 18/1000; mw_ethanol = 46/1000; % Looping over the range 0 to 100 C and calculating the % average molecular speed. Note our Maxwell-Boltzmann % equation uses K for temperature, so we will perform % the conversion each iteration. for t_C = 0:100 % Temperature in K t_K = t_C + 273.15; % Speed of water in m/s % Remember, vector indices start at 1, so we need to shift % up by 1 speed_water(t_C+1) = sqrt( 3*r*t_K/mw_water ); speed_ethanol(t_C+1) = sqrt( 3*r*t_K/mw_ethanol ); 126 Chapter 4 Logic and vectors 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 end % Plotting % When plotting, we will use the temperature in C, since % that is what the problem statement asked for. We can % easily create the temperature vector t_C = 0:100; % Water then ethanol plot(t_C,speed_water,'-ro') plot(t_C,speed_ethanol,'-bx') % labeling the axis xlabel('T [C]') ylabel('speed [m/s]') % plot title title('Average speed from Maxwell-Boltzmann') % legend. % Data is plotted water then ethanol legend('water','ethanol') % Save and print savefig('Example_4_10.fig') print('-depsc','Exercise_4_10.eps') 4.21 CPB Examples 127 As mentioned in (c) in Example 4.9, there are some potential shortcomings of just shifting up by 1. A more general solution would be to use a loop counter variable. Here is what that might look like: Listing 4.23 Example_4_10_counter.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 % % % % % % % % % % % Plotting the average speed of water and ethanol molecules over the range 0 to 100 oC. In this version of the code I will use a loop counter variable. Dr. Paluch, Example 4.10 Pre-condition: None. I will hardcode everything since we are asked for a specific temperature range and the two species are specified. Post-condition: Plot of average molecular speed versus temperature % Start by clearing our figure clf % Need hold on because we will plot two sets of data hold on % gas constant in J/(mol K) r = 8.314; % Molecular weight of water and ethanol in kg/mol mw_water = 18/1000; mw_ethanol = 46/1000; % Initializing our loop counter variable n = 0; % Looping over the range 0 to 100 C and calculating the % average molecular speed. Note our Maxwell-Boltzmann % equation uses K for temperature, so we will perform % the conversion each iteration. for t_C = 0:100 % Increment the loop counter variable by 1 each loop iteration n = n+1; % Temperature in K t_K = t_C + 273.15; % Speed of water in m/s speed_water(n) = sqrt( 3*r*t_K/mw_water ); speed_ethanol(n) = sqrt( 3*r*t_K/mw_ethanol ); end % Plotting % When plotting, we will use the temperature in C, since % that is what the problem statement asked for. We can % easily create the temperature vector t_C = 0:100; % Water then ethanol plot(t_C,speed_water,'-ro') plot(t_C,speed_ethanol,'-bx') 128 Chapter 4 Logic and vectors 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 % labeling the axis xlabel('T [C]') ylabel('speed [m/s]') % plot title title('Average speed from Maxwell-Boltzmann') % legend. % Data is plotted water then ethanol legend('water','ethanol') % Save and print savefig('Example_4_10_counter.fig') print('-depsc','Exercise_4_10_counter.eps') 4.21 CPB Examples 129 In the spirit of improving, let’s improve it some more! Since we create a temperature vector before we plot, we might as well move it up toward the top of the program. Then for the for loop, we can just loop over the vector indices. This will also allow us to pre-size all of our vectors. Listing 4.24 Example_4_10_vector.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 % % % % % % % % % % % % Plotting the average speed of water and ethanol molecules over the range 0 to 100 oC. In this version of the code I define the temperature vector at the beginning, then loop over the vector indices. This will also allow us to pre-size all of our vectors. Dr. Paluch, Example 4.10 Pre-condition: None. I will hardcode everything since we are asked for a specific temperature range and the two species are specified. Post-condition: Plot of average molecular speed versus temperature % Start by clearing our figure clf % Need hold on because we will plot two sets of data hold on % Creating our vector of temperatures in C. Here I will use colon operator, % but linspace would be another good option. Also, I will update the % notation to use capital letters for vector variables. T_C = 0:100; % While we are at it, let's compute a vector of temperatues in K since we % will need them in the calculations. T_K = T_C + 273.15; % Now we can pre-size all of our other vectors. Again, note the update to % the variable names to coincide with the use of capital letters to % indicate vectors. Speed_water = zeros(1,length(T_C)); Speed_ethanol = zeros(1,length(T_C)); % gas constant in J/(mol K) r = 8.314; % Molecular weight of water and ethanol in kg/mol mw_water = 18/1000; mw_ethanol = 46/1000; % Looping over the temperature range. We will look using vector indices. I % will use T_K since temperature in K is needed for the calculations. for i=1:length(T_K) % Speed of water in m/s Speed_water(i) = sqrt( 3*r*T_K(i)/mw_water ); Speed_ethanol(i) = sqrt( 3*r*T_K(i)/mw_ethanol ); end 130 Chapter 4 Logic and vectors 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 % Plotting % When plotting, we will use the temperature in C, since % that is what the problem statement asked for. % Water then ethanol plot(T_C,Speed_water,'-ro') plot(T_C,Speed_ethanol,'-bx') % labeling the axis xlabel('T [C]') ylabel('speed [m/s]') % plot title title('Average speed from Maxwell-Boltzmann') % legend. % Data is plotted water then ethanol legend('water','ethanol') % Save and print savefig('Example_4_10_vector.fig') print('-depsc','Exercise_4_10_vector.eps') 4.21 CPB Examples 131 Excellent! And now in the spirit of “Spoiling the fun” Listing 4.25 Example_4_10_vector_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 % % % % % % % % % % % % Plotting the average speed of water and ethanol molecules over the range 0 to 100 oC. In this version of the code I define the temperature vector at the beginning, and then perform the calculations using elementwise operations. Dr. Paluch, Example 4.10 Pre-condition: None. I will hardcode everything since we are asked for a specific temperature range and the two species are specified. Post-condition: Plot of average molecular speed versus temperature % Start by clearing our figure clf % Need hold on because we will plot two sets of data hold on % Creating our vector of temperatures in C. Here I will use colon operator, % but linspace would be another good option. Also, I will update the % notation to use capital letters for vector variables. T_C = 0:100; % While we are at it, let's compute a vector of temperatues in K since we % will need them in the calculations. T_K = T_C + 273.15; % gas constant in J/(mol K) r = 8.314; % Molecular weight of water and ethanol in kg/mol mw_water = 18/1000; mw_ethanol = 46/1000; % Now perform the calculations using elementwise operations % Speed of water in m/s Speed_water = sqrt( 3*r*T_K/mw_water ); Speed_ethanol = sqrt( 3*r*T_K/mw_ethanol ); % Plotting % When plotting, we will use the temperature in C, since % that is what the problem statement asked for. % Water then ethanol plot(T_C,Speed_water,'-ro') plot(T_C,Speed_ethanol,'-bx') % labeling the axis xlabel('T [C]') ylabel('speed [m/s]') % plot title title('Average speed from Maxwell-Boltzmann') 132 Chapter 4 Logic and vectors 52 53 54 55 56 57 58 59 % legend. % Data is plotted water then ethanol legend('water','ethanol') % Save and print savefig('Example_4_10_vector_2.fig') print('-depsc','Exercise_4_10_vector_2.eps') 4.22 Glossary compound: A statement, like if and for, that contains other statements in an indented body. nesting: Putting one compound statement in the body of another. relational operator: An operator that compares two values and generates a logical value as a result. logical value: A value that represents either “true” or “false”. MATLAB uses the values 1 and 0, respectively. flag: A variable that contains a logical value, often used to store the status of some condition. scalar: A single value. vector: A sequence of values. matrix: A two-dimensional collection of values (also called “array” in some MATLAB documentation). index: An integer value used to indicate one of the values in a vector or matrix (also called subscript in some MATLAB documentation). element: One of the values in a vector or matrix. elementwise: An operation that acts on the individual elements of a vector or matrix (unlike some linear algebra operations). reduce: A way of processing the elements of a vector and generating a single value; for example, the sum of the elements. apply: A way of processing a vector by performing some operation on each of the elements, producing a vector that contains the results. search: A way of processing a vector by examining the elements in order until one is found that has the desired property. 4.23 Exercises 4.23 Exercises Exercise 4.1 In Exercise 2.1 and 3.1 we created scripts to compute the surface tension of ethanol, propane, and ethanol. Let’s keep the fun going here. First, let’s update our script from Exercise 3.1 in (a) and (b). Then in (c) and (d) we will update our script from Exercise 2.1. a) Update your scripts so that the results are stored to vectors. You should have a total of six vectors: the temperatures and corresponding surface tensions for ethanol, the temperatures and corresponding surface tensions for propane, and the temperature and corresponding surface tensions for acetone. Plot the vectors. Does this make it easier to add a legend? b) Next, update your script to eliminate the use of for loops. Instead, use elementwise operations. c) We wish to create a script to compute the surface tension of ethanol, propane, or acetone, as specified by the user. How do you accomplish this? I would create a “flag” variable to which the user should assign an integer value, where each integer maps to a species. For example, if the flag variable is set to 1, that corresponds to performing the calculations for ethanol. The calculation will be performed at a single temperature as specified by the user. d) Last, update your script to check that the temperature is within the range of applicability of the equation. Exercise 4.2 Let’s update our script from Exercise 3.2. Recall that we computed the enthalpy of vaporization as a function of carbon number for the homologous series of linear alkanes. In this version of the script, store the results to vectors. Use one vector to store the carbon number, and another to store the corresponding enthalpy of vaporization. Plot the results as before. Once you script is working, have a look at documentation pages for diff. Use diff to compute the changes in enthalpy of vaporization. Are they constant? Exercise 4.3 Update your script from Exercise 3.3. First, update your code to eliminate the use of a for loop. To accomplish this, you should create a vector of temperatures and then use elementwise operations. To create a vector of temperatures from Tmin to Tmax , you might find the function linspace useful. Store results to a vector. Then create a plot using your vectors. Exercise 4.4 Update your script from Exercise 3.4, our friction factor problem. We will update it step-by-step. Each step you will be able to run and check your code to make sure nothing has changed. At the end of the problem we still will not make a nice Moody diagram, but we will be closer to being able to accomplish this goal. 1. First, update you script so that each loop iteration, the computed friction factor is stored to a vector. 2. Next, rather than use Re as a loop index variable, at the beginning of your script create a vector of Re values. Then in your loop, loop over the indices (1 to n) of this vector and use the appropriate element each iteration. 3. Next, update your code to use elementwise operations to eliminate the need for a for loop. 133 134 Chapter 4 Logic and vectors With the use of elementwise operations, you should find that your code is much more efficient and hopefully more compact. Before getting to the creation of a Moody diagram, we will discuss more the creation of our vector of Re values for a desired, larger range, and we will also need log axes. Chapter 5 Functions In Chapter 5 we continue to build our foundational knowledge of MATLAB with the introduction of functions. The use of functions will be very important as we continue to work through the course. A function is like a script, but it has its own workspace. Any information you want your function to have from your workspace needs to be passed to it, and any information you want the function to share with your workspace needs to be returned. Many of MATLAB’s built-in numerical methods, from zero finding to solving ordinary differential equations, require us to write functions. By the end of this chapter you will be able to: • Explain the difference between a function and a script • Demonstrate the ability to write a function • Apply the use of functions to solve basic engineering problems If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 5.1 Name Collisions Remember that all of your scripts run in the same workspace, so if one script changes the value of a variable, all your other scripts see the change. With a small number of simple scripts, that’s not a problem, but eventually the interactions between scripts become unmanageable. For example, the following (increasingly familiar) script computes the sum of the first n terms in a geometric sequence, but it also has the side-effect of assigning values to a1, total, i and a. 135 136 Chapter 5 Functions a1 = 1; total = 0; for i=1:10 a = a1 * 0.5^(i-1); total = total + a; end ans = total If you were using any of those variable names before calling this script, you might be surprised to find, after running the script, that their values had changed. If you have two scripts that use the same variable names, you might find that they work separately and then break when you try to combine them. This kind of interaction is called a name collision. As the number of scripts you write increases, and they get longer and more complex, name collisions become more of a problem. Avoiding this problem is one of the motivations for functions. In the example above, after storing the final result to ans we could clear all of the variables used: clear a1, total, i, a. This is one of the main effect of using a function. 5.2 Functions A function is like a script, except • Each function has its own workspace, so any variables defined inside a function only exist while the function is running, and don’t interfere with variables in other workspaces, even if they have the same name. (See the MATLAB documentation page “Base and Function Workspaces”). • Function inputs and outputs are defined carefully to avoid unexpected interactions. t Just as with for loops and if statements, there is no need for a semicolon after the signature line. To define a new function, you create an M-file with the name you want, and put a function definition in it. For example, to create a function named myfunc, create an M-file named myfunc.m and put the following definition into it. Listing 5.1 myfunc.m 1 function res = myfunc(x) 2 s = sin(x) 3 c = cos(x) 4 res = abs(s) + abs(c) 5 end The first word of the file has to be the word function, because that’s how MATLAB tells the difference between a script and a function file. (See the MATLAB documentation page “Create Functions in Files”.) 5.2 Functions 137 You can create a new M-file in MATLAB’s editor using the New Script button, just as we have used previously to create a script; this is what I will use throughout the text. Alternatively, you can use the New button, then select Function from the dropdown menu. This will create a new file as before, but will also include a template for creating a function. t When you use the New button to create a new function, you will see it puts the output variable in brackets. Brackets are required if you want more than one output variable. But more on having multiple/optional output variables later as there is more than meets eye. Figure 5.1 Creating a new function. A function definition is a compound statement. The first line is called the signature of the function; it defines the inputs and outputs of the function. In this case the input variable is named x. When this function is called, the argument provided by the user will be assigned to x. The output variable is named res, which is short for “result.” You can call the output variable whatever you want, but as a convention, I like to call it res. Usually the last thing a function does is assign a value to the output variable. Once you have defined a new function, you call it the same way you call built-in MATLAB functions. If you call the function as a statement, MATLAB puts the result into ans: >> myfunc(1) s = 0.84147 c = 0.54030 res = 1.3818 ans = 1.3818 But it is more common (and better style) to assign the result to a variable: >> y=myfunc(1) s = 0.84147 c = 0.54030 res = 1.3818 y = 1.3818 t Back in Chapter 2: Scripts, I mentioned that I always find that students like to use the “Run” button to run their scripts. However, when you have a function with input variables, you can not simply click the Run button. Doing so is equivalent to typing the name of the M-file in the Command Window . The function requires an input, so this will not work. You need to click on the drop-down arrow and select “Run: type code to run”. This is demonstrated in the screen cast available here. But at this point, why not just get in the habit of running your scripts and functions from the Command Window ? 138 Chapter 5 Functions What will happen if we attempt to suppress output by executing the function as y=myfunc(1);? Not what you might expect. We will suppress the result of the function call, y = 1.3818, but the other intermediate lines will still be displayed since they are not suppressed within the function file. >> y=myfunc(1); s = 0.84147 c = 0.54030 res = 1.3818 t Notice that I changed both the name of the M-file and the name of the function. We will discuss my reason for this momentarily. While you are debugging a new function, you might want to display intermediate results like this, but once it is working, you will want to add semi-colons to make it a silent function. Most built-in functions are silent; they compute a result, but they don’t display anything (except sometimes warning messages). Listing 5.2 myfunc_silent.m 1 function res = myfunc_silent(x) 2 s = sin(x); 3 c = cos(x); 4 res = abs(s) + abs(c); 5 end >> y=myfunc_silent(1) y = 1.3818 Each function has its own workspace, which is created when the function starts and destroyed when the function ends. If you try to access (read or write) the variables defined inside a function, you will find that they don’t exist. >> clear variables >> y=myfunc(1); s = 0.84147 c = 0.54030 res = 1.3818 >> who Your variables are: y >> s Undefined function or variable 's'. The only value from the function that you can access is the result, which in this case is assigned to y. If you have variables named s or c in your workspace before you call myfunc, they will still be there when the function completes. 5.3 Documentation 139 >> s = 1; >> c = 1; >> y = myfunc(1); s = 0.84147 c = 0.54030 res = 1.3818 >> s, c s = 1 c = 1 So inside a function you can use whatever variable names you want without worrying about collisions. 5.3 Documentation At the beginning of every function file, you should include a comment that explains what the function does. Listing 5.3 myfunc_comment.m 1 2 3 4 5 6 7 8 9 % res = myfunc_comment(x) % compute the Manhattan distance from the origin to the % point on the unit circle with angle (x) in radians. function res = myfunc_comment(x) s = sin(x); c = cos(x); res = abs(s) + abs(c); end When you ask for help, MATLAB prints the comment you provide. >> help myfunc_comment res = myfunc_comment(x) Compute the Manhattan distance from the origin to the point on the unit circle with angle (x) in radians. There are lots of conventions about what should be included in these comments. Among other things, it is a good idea to include: • The signature of the function, which includes the name of the function, the input variable(s) and the output variable(s). • A clear, concise, abstract description of what the function does. An abstract description is one that leaves out the details of how the function works, t When you use the New button to create a new function, you will see it puts the documentation material after the function signature. Both formats are correct and have the same effect (give it a try). I prefer to put the comments/documentation at the top of a file, the same convention we used when writing scripts. This is also why I use the New Script button to create function files. Personally, the least I need to try and remember the better. 140 Chapter 5 Functions and includes only information that someone using the function needs to know. You can put additional comments inside the function that explain the details. • An explanation of what the input variables mean; for example, in this case it is important to note that x is considered to be an angle in radians. • Any preconditions and postconditions. t This is a very common mistake made by beginners and can make debugging tricky. Remember the first step of Incremental Development. Start by creating a function that takes no input and performs simple task, such as res =5. Then call the function from the command line. This way you make sure the function you think you are calling is what is actually being called. 5.4 Function names There are three “gotchas” that come up when you start naming functions. The first is that the “real” name of your function is determined by the file name, not by the name you put in the function signature. As a matter of style, you should make sure that they are always the same, but if you make a mistake, or if you change the name of a function, it is easy to get confused. In the spirit of making errors on purpose, create a copy myfunc_silent.m and Save As myfunc_name.m. In myfunc_name.m, change the name of the function to something_else, and then run it again. If this is what you put in myfunc_name.m: Listing 5.4 myfunc_name.m 1 function res = something_else(x) 2 s = sin(x); 3 c = cos(x); 4 res = abs(s) + abs(c); 5 end Then here’s what you’ll get: t When you create a new M-file in the MATLAB Editor and start by writing the function signature, MATLAB will suggest the name of the function as the name of the file. It will help you try to avoid the “first gotcha.” t In MATLAB 2015a, MATLAB will display a warning message in a new pop-up window if you attempt the “second gotcha,” and will suggest a change to avoid it. You can fix the name right there in the pop-up window. (To be clear, I am referring to when you try to save the file in the Editor, not when you try to run it.) >> y = myfunc_name(1) y = 1.3818 >> y = something_else(1) Undefined command/function 'something_else'. The second gotcha is that the name of the file can’t have spaces. We encountered the same rule when naming variables, where we used underscores in place of spaces when desired. More generally, valid function names follow the same rules as variable names. For example, if you write a function and name the file my func.m, which the MATLAB editor will happily allow you to do, and then try to run it, you get: >> y = my func(1) Undefined function or variable 'my'. 5.4 Function names 141 For the first two gotcha’s, I would again point you to the MATLAB documentation page “Create Functions in Files.” The third gotcha is that your function names can collide with built-in MATLAB functions. For example, if you create an M-file named sum.m and save it in your current folder, then MATLAB will always call this new function and not the built-in version. This is a bad idea, especially if you plan to share your code with others. The unsuspecting user may use the command sum and expect to get MATLAB’s built-in function, but instead get the function you created. As an example, put the following code in a file named sum.m: Listing 5.5 sum.m 1 function res = sum(x) 2 res = 7; 3 end And then try this: >> sum(1:3) ans = 7 While this is correct using your code, it is confusing to the unknowing user. Before you create a new function, check to see if there is already a MATLAB function with the same name. If there is, choose another name! MATLAB will attempt to help you if you attempt the third gotcha. When I created the function sum, the editor happily allowed me to save the file. However, the following message was displayed in the Command Window : Warning: Function sum has the same name as a MATLAB built-in. We suggest you rename the function to avoid a potential name conflict. Note that beside name collisions with other functions, name collisions with variables exist too! So take a minute when you name your function. For more insight on the third gotcha, have a look at the MATLAB documentation pages “Files and Folders that MATLAB Accesses and “Function Precedence Order.” Example 5.1 MATLAB contains a function sqrt to compute the square root of a number. Write a function named cubert to compute the cube root of a number. Solution: (Link to screen cast and accompanying M-file.) The cube root of x is x 1/3 . Let’s write the function. Listing 5.6 cubert.m 1 2 3 4 5 % res = cubert (n) % Computing the cube root of a number n. function res = cubert (n) res = n^(1/3); end t Before we move on to Examples, recall from our discussion in Chapter 4 that MATLAB’s built-in functions operate element-wise on vectors. The same is true with functions that you write, so long as you use element-wise operations (i.e., .*). Remember, a scalar is a 1 by 1 matrix, so element-wise operators work as normal with scalars. So writing your functions in this way is not a problem. However, in this Chapter we will only consider functions that pass and return scalars. Functions of vectors will be discussed later. 142 Chapter 5 Functions Beginning with a new MATLAB session, let’s first take a look at our workspace. >> who >> Nothing. We currently do not have any defined variables in our workspace. Now let’s use our function cubert to compute the cube root of 27, which we know should be 3. >> cubert(27) ans = 3 If we again take a look at our workspace: >> who Your variables are: ans The only defined variable in our workspace is ans, the default variable that the result of a calculation is stored to. Notice that while res and n are used in our function, neither appear in our workspace. While ans is a great variable, the problem is it will be over-written the next time we perform a calculation. It is a better practice to store the result of the calculation to a unique variable. >> x = cubert(27) x = 3 Lastly, remember that a variable is equal to the number that is assigned to it. So you can also pass variables to functions. This is especially good if it makes your code more readable. And the variable need not be n, what we call the passed variable within the function; MATLAB doesn’t “see” the variable name, just the number. >> k = 27; >> x = cubert(k) x = 3 In addition to calling a variable from the Command Window , you can call it from a script. (You may find this useful when submitting homework solutions.) After all, a script is just a series of commands you could enter one-by-one from the Command Window . Additionally, you could call a function from within a function. And beginning with MATLAB 2016b, you can now add functions to scripts (i.e., put a “local” function directly in a script file). We will discuss multiple functions in a file in the next chapter. 5.5 Multiple input variables 143 Example 5.2 MATLAB contains a function sin which computes the sine of an angle x, where x is in units of radians, and a function sind where the angle is in units of degrees. Write a new function sin_deg which computes the sine of an angle x in degrees. (I.e., Your own version of sind.) Your function should convert the angle from radians to degrees, and then use sin to compute the sine of the angle. Note: In practice I would always recommend using MATLAB’s built-in functions, so long as you know they exist and how they work. This way you can be guaranteed they are bug free and optimized for efficiency. Here, this is an academic exercise, and the availability of sind provides a means for you to directly check the correctness of your code. Solution: (Link to screen cast and accompanying M-file.) While it is unnecessary, let’s first write a function to convert an angle from degrees to radians, call it deg_to_rad. We will then call this function from within sin_deg. A simple example of calling a function from within a function. Listing 5.7 deg_to_rad.m 1 2 3 4 5 % res = deg_to_rad(n) % Converting an angle from degrees to radians function res = deg_to_rad(n) res = n*pi/180; end Listing 5.8 sin_deg.m 1 2 3 4 5 6 % res = sin_deg(n) % Computing the sin of angle n in units of degress function res = sin_deg(n) x = deg_to_rad(n); res = sin(x); end >> y = sin_deg(90) y = 1 >> y = sin_deg(180) y = 1.2246e-16 5.5 Multiple input variables Functions can, and often do, take more than one input variable. For example, the following function takes two input variables, a and b: 144 Chapter 5 Functions Listing 5.9 hypotenuse.m 1 2 3 4 5 6 % res = hypotenuse(a, b) % compute the hypotenuse of a right triangle with sides of % length a and b. function res = hypotenuse(a, b) res = sqrt(a^2 + b^2); end This function computes the length of the hypotenuse of a right triangle if the lengths of the adjacent sides are a and b using the Pythagorean Theorem. (Note: there is a MATLAB function called hypot that does the same thing.) If we call it from the Command Window with arguments 3 and 4, we can confirm that the length of the third side is 5. >> c = hypotenuse(3, 4) c = 5 The arguments you provide are assigned to the input variables in order, so in this case 3 is assigned to a and 4 is assigned to b. MATLAB checks that you provide the right number of arguments; if you provide too few, you get >> c = hypotenuse(3) Error using hypotenuse (line 2) Not enough input arguments. This error message might seem confusing if you were just to read the first line, because it suggests that the problem is in hypotenuse rather than in the function call. But the second line should clear-up what the problem is. Keep that in mind when you are debugging. If you provide too many arguments, you get >> c = hypotenuse(3, 4, 5) Error using hypotenuse Too many input arguments. Example 5.3 Write a function rect_area to compute the area of a rectangle of length l and height h. Solution: (Link to screen cast and accompanying M-file.) For a rectangle, the area A is computed as A = l h. Let’s write a function to do this for us. 5.5 Multiple input variables 145 Listing 5.10 rect_area.m 1 2 3 4 5 % res = rect_area (l,h) % Computing the area of a rectangle of length l and heigh h. function res = rect_area(l,h) res = l*h; end >> a = rect_area(2,4) a = 8 Note that while I sometimes deviate from my own suggestion, a good habit is to use lowercase letter variables for scalar quantities, and capital letter variables for vectors/matrices. Remember MATLAB is case-sensitive, so this will keep you from mixing things up. So while A for area is attractive, I instead use a since it is a scalar quantity. Example 5.4 Rather than having a separate function to compute the sine of an angle in radians and in degrees, create a new function sin_angle that can compute either, depending on the specification of the user. Solution: (Link to screen cast and accompanying M-file.) In addition to passing the angle to the function, we will also need to pass a flag to tell the function what units the angle is in. For this, I tend to use integers. For instance, if the flag variable is 1 the angle is in radians, if it is -1 the angle is in degrees, and if it is any other value, tell the user they didn’t correctly indicate their choice of units. You might also think of using a string. Say ‘deg’ for degrees ‘rad’ for radians. This could work for this particular example. But in general, you need to be careful when using strings in condition statements, as the length of the strings being compared need to agree. 146 Chapter 5 Functions Listing 5.11 sin_angle.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 % res = sin_angle (x,units) % Computing the sine of angle x. The units of x are determined by % the variable units. % if units == 1, we are using radians % if units == -1, we are using degrees % else you made a mistake! function res = sin_angle (x,units) if units == 1 res = sin(x); elseif units == -1 res = sin_deg(x); % Could instead use built-in sind(x) else disp('Incorrect choice of units. Please try again.') res = NaN; end end Notice two things. First, I use the function disp to display my error message, which is a string. Since the output is not suppressed, it will be displayed in the Command Window if I do not provide a valid value for units. Second, if I do not provide a valid value of units, NaN is assigned to res. Remember NaN stands for “Not a Number”, and will be propagated though subsequent calculations. I will run sin_angle passing a value of units = 2 to confirm this result, and will multiply ans by 2 and then 0 to confirm that the result is still be NaN. >> sin_angle(180,2) Incorrect choice of units. Please try again. ans = NaN >> ans*2 ans = NaN >> ans*0 ans = NaN Nice! 5.6 Logical functions In Section 4.4 we used logical operators to compare values. MATLAB also provides logical functions that check for certain conditions and return logical values: 1 for “true” and 0 for “false”. For example, isprime checks to see whether a number is prime. 5.6 Logical functions 147 >> isprime(17) ans = 1 >> isprime(21) ans = 0 The functions isscalar and isvector check whether a value is a scalar or vector; if both are false, you can assume it is a matrix (at least for now). To check whether a value you have computed is an integer, you might be tempted to use isinteger. But that would be wrong, so very wrong. isinteger checks whether a value belongs to one of the integer types (a topic we have not discussed); it doesn’t check whether a floating-point value happens to be an integer. >> c = hypotenuse(3, 4) c = 5 >> isinteger(c) ans = 0 To do that, we have to write our own logical function, which we’ll call isintegral: Listing 5.12 isintegral.m 1 2 3 4 5 6 7 8 9 10 % res = isintegral(x) % checks whether or not x is an integer. If it is, it returns % a value of 1 for true. If not, it returns a value of 0 for false. function res = isintegral(x) if round(x) == x res = 1; else res = 0; end end This function is good enough for most applications, but remember that floatingpoint values are only approximately right; in some cases the approximation is an integer but the actual value is not. Example 5.5 Write a function iseven to check if an integer is even. Hint: An even number is a number that when divided by 2 has a remainder of 0. This is called the modulus after division, and MATLAB has a function to compute it, mod. Type help mod and have a look at the documentation. If we were interested instead in odd numbers, an odd number is a number that when divided by 2 has a remainder of 1. 148 Chapter 5 Functions Solution: (Link to screen cast and accompanying M-file.) Listing 5.13 iseven.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 % res = iseven(n) % A function to determine if integer n is even. An even number is a % number that when divided by 2 has a remainder of 0. We will compute % the remainder of dividing n by 2 using the MATLAB function mod. If % mod(n,2) is found to be 0, we have an even number so set % res = 1 (true). Otherwise, set res = 0 (false). function res = iseven(n) r = mod(n,2); if r == 0 res = 1; else res = 0; end end Let’s consider a couple of cases: >> iseven(222) ans = 1 >> iseven(211) ans = 0 What if you had never heard of the term “modulus” before? Well, using the hint that an even number is a number that when divided by 2 has a remainder of 0, we can cleverly use the round command. Listing 5.14 iseven_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 % res = iseven_2(n) % A function to determine if integer n is even. An even number is a % number that when divided by 2 has a remainder of 0. We will take the % integer, divide by two, and then round the answer to the nearest integer. % We will then if the rounded and original answer are equivalent. If they % are, the we have a remainder of zero and we have an evn number, so set % res = 1 (true). Otherwise, set res = 0 (false). function res = iseven_2(n) r1 = n/2; r2 = round(r1); if r1==r2 res = 1; else res = 0; end end Additionally note that beside mod, MATLAB has a built-in function rem to compute the remainder after division. Using either in this example would yield the same 5.7 An incremental development example result. The difference between mod and rem is provided in the documentation for rem if you are interested, under the header “Difference between rem and mod.” 5.7 An incremental development example Let’s say that we want to write a program to search for “Pythagorean triples:” sets of integer values, like 3, 4 and 5, that are the lengths of the sides of a right triangle. In other words, we would like to find integer values a, b and c such that a2 + b2 = c 2. Here are the steps we will follow to develop the program incrementally. • Write a script named find_triples and start with a simple statement like x=5. • Write a loop that enumerates values of a from 1 to 3, and displays them. • Write a nested loop that enumerates values of b from 1 to 4, and displays them. • Inside the loop, call hypotenuse to compute c and display it. • Use isintegral to check whether c is an integer value. • Use an if statement to print only the triples a, b and c that pass the test. • Transform the script into a function. • Generalize the function to take input variables that specify the range to search. So the first draft of this program is x=5, which might seem silly, but if you start simple and add a little bit at a time, you will avoid a lot of debugging. Here’s the second draft: for a=1:3 a end At each step, the program is testable: it produces output (or another visible effect) that you can check. 5.8 Nested loops The third draft contains a nested loop: 149 150 Chapter 5 Functions for a=1:3 a for b=1:4 b end end The inner loop gets executed 3 times, once for each value of a, so here’s what the output loops like (I adjusted the spacing to make the structure clear): >> find_triples a = 1 b b b b = = = = 1 2 3 4 a = 2 b b b b = = = = 1 2 3 4 a = 3 b b b b = = = = 1 2 3 4 The next step is to compute c for each pair of values a and b. for a=1:3 for b=1:4 c = hypotenuse(a, b); [a, b, c] end end To display the values of a, b and c, I am using a feature we haven’t seen before. The bracket operator creates a new vector which, when it is displayed, shows the three values on one line: >> find_triples ans = 1.0000 ans = 1.0000 1.0000 2.0000 1.4142 2.2361 5.9 Conditions and flags ans ans ans ans ans ans ans ans ans ans = = = = = = = = = = 1.0000 1.0000 2.0000 2.0000 2.0000 2.0000 3.0000 3.0000 3.0000 3 3.0000 4.0000 1.0000 2.0000 3.0000 4.0000 1.0000 2.0000 3.0000 4 151 3.1623 4.1231 2.2361 2.8284 3.6056 4.4721 3.1623 3.6056 4.2426 5 Sharp-eyed readers will notice that we are wasting some effort here. After checking a = 1 and b = 2, there is no point in checking a = 2 and b = 1. We can eliminate the extra work by adjusting the range of the second loop: for a=1:3 for b=a:4 c = hypotenuse(a, b); [a, b, c] end end If you are following along, run this version to make sure it has the expected effect. >> find_triples ans = 1.0000 ans = 1.0000 ans = 1.0000 ans = 1.0000 ans = 2.0000 ans = 2.0000 ans = 2.0000 ans = 3.0000 ans = 3 4 1.0000 2.0000 3.0000 4.0000 2.0000 3.0000 4.0000 3.0000 5 1.4142 2.2361 3.1623 4.1231 2.8284 3.6056 4.4721 4.2426 5.9 Conditions and flags The next step is to check for integer values of c. This loop calls isintegral and prints the resulting logical value. 152 Chapter 5 Functions for a=1:3 for b=a:4 c = hypotenuse(a, b); flag = isintegral(c); [c, flag] end end By not displaying a and b I made it easy to scan the output to make sure that the values of c and flag look right. >> find_triples ans = 1.4142 ans = 2.2361 ans = 3.1623 ans = 4.1231 ans = 2.8284 ans = 3.6056 ans = 4.4721 ans = 4.2426 ans = 5 0 0 0 0 0 0 0 0 1 I chose the ranges for a and b to be small (so the amount of output is manageable), but to contain at least one Pythagorean triple. A constant challenge of debugging is to generate enough output to demonstrate that the code is working (or not) without being overwhelmed. The next step is to use flag to display only the successful triples: for a=1:3 for b=a:4 c = hypotenuse(a, b); flag = isintegral(c); if flag [a, b, c] end end end Now the output is elegant and simple: >> find_triples ans = 3 4 5 5.10 Encapsulation and generalization 5.10 Encapsulation and generalization As a script, this program has the side-effect of assigning values to a, b, c and flag, which would make it hard to use if any of those names were in use. By wrapping the code in a function, we can avoid name collisions; this process is called encapsulation because it isolates this program from the workspace. In order to put the code we have written inside a function, we have to indent the whole thing. The MATLAB editor provides a shortcut for doing that, the Increase indent button on the Editor tab; highlight the text to indent and then click Increase indent , our you could type Tab instead. Just don’t forget to unselect the text before you start typing! The first draft of the function takes no input variables: 1 2 3 4 5 6 7 8 9 10 11 function res = find_triples() for a=1:3 for b=a:4 c = hypotenuse(a, b); flag = isintegral(c); if flag [a, b, c] end end end end The empty parentheses in the signature are not strictly necessary, but they make it apparent that there are no input variables. Similarly, when I call the new function, I like to use parentheses to remind me that it is a function, not a script; but again, here too the use of the parentheses is optional since there are no input variables. >> find_triples() ans = 3 4 5 The output variable (res) isn’t strictly necessary, either; it never gets assigned a value. But I put it there as a matter of habit, and also so my function signatures all have the same structure. For now, we are just displaying the triples. Later in the text, you will learn how you can pack all of the triples up into a matrix and return it. But we will save that for later. The next step is to generalize this function by adding input variables. The natural generalization is to replace the constant values 3 and 4 with a variable so we can search an arbitrarily large range of values. 153 154 Chapter 5 Functions 1 2 3 4 5 6 7 8 9 10 11 function res = find_triples(n) for a=1:n for b=a:n c = hypotenuse(a, b); flag = isintegral(c); if flag [a, b, c] end end end end Here are the results for the range from 1 to 15: >> find_triples(15) ans = 3 4 5 ans = 5 12 13 ans = 6 8 10 ans = 8 15 17 ans = 9 12 15 Some of these are more interesting than others. The triples 5, 12, 13 and 8, 15, 17 are “new,” but the others are just multiples of the 3, 4, 5 triangle we already knew. 5.11 A misstep When you change the signature of a function, you have to change all the places that call the function, too. For example, suppose I decided to add a third input variable to hypotenuse: 1 2 3 function res = hypotenuse(a, b, d) res = (a.^d + b.^d) ^ (1/d); end When d is 2, this does the same thing it did before. There is no practical reason to generalize the function in this way; it’s just an example. Now when you run find_triples, you get: 5.12 continue >> find_triples(20) Error using hypotenuse (line 2) Not enough input arguments. Error in find_triples (line 7) c = hypotenuse(a, b); So that makes it pretty easy to find the error. This is an example of a development technique that is sometimes useful: rather than search the program for all the places that use hypotenuse, you can run the program and use the error messages to guide you. But this technique is risky, especially if the error messages make suggestions about what to change. If you do what you’re told, you might make the error message go away, but that doesn’t mean the program will do the right thing. MATLAB doesn’t know what the program is supposed to do, but you should. And that brings us to the Eighth Theorem of debugging: Error messages sometimes tell you what’s wrong, but they seldom tell you what to do (and when they try, they’re usually wrong). 5.12 continue As one final improvement, let’s modify the function so that it only displays the “lowest” of each Pythagorean triple, and not the multiples. The simplest way to eliminate the multiples is to check whether a and b share a common factor. If they do, then dividing both by the common factor yields a smaller, similar triangle that has already been checked. MATLAB provides a gcd function that computes the greatest common divisor of two numbers. If the result is greater than 1, then a and b share a common factor and we can use the continue statement to skip to the next pair. This brings us to the final version of our function: 155 156 Chapter 5 Functions Listing 5.15 find_triples.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 % res = find_triples (n) % finding Pythagorean triples over the range 1 to n. function res = find_triples (n) for a=1:n for b=a:n if gcd(a,b) > 1 continue end c = hypotenuse(a, b); if isintegral(c) [a, b, c] end end end end continue causes the program to end the current iteration immediately (without executing the rest of the body), jump to the top of the loop, and “continue” with the next iteration. In this case, since there are two loops, it might not be obvious which loop to jump to, but the rule is to jump to the inner-most loop (which is what we wanted). I also simplified the program slightly by eliminating flag and using isintegral as the condition of the if statement. Here are the results with n=40: >> find_triples(40) ans = 3 4 5 ans = 5 12 13 ans = 7 24 25 ans = 8 15 17 ans = 9 40 41 ans = 12 35 37 ans = 20 21 29 There is an interesting connection between Fibonacci numbers and Pythagorean triples. If F is a Fibonacci sequence, then 2 2 (F n F n+3 , 2F n+1 F n+2 , F n+1 + F n+2 ) is a Pythagorean triple for all n ≥ 1. 5.12 continue 157 Example 5.6 Write a function named fib_triple that takes an input variable n, uses new function fibonacci_seq to compute the first n Fibonacci numbers, and then checks whether this formula produces a Pythagorean triple for each number in the sequence. Note that fibonacci_seq will just be Listing 4.3 on page 93 converted from a script to a function. Solution: Listing 5.16 fib_triple.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 % res = fib_triple(n) % checks whether the provided formula using 3 consecutive Fibonacci % numbers produces a Pythagorean triple over the range 1 to n. % The result will be a vector containing 1 (for true) and 0 (for false) % for an n corresponding to the index. function res = fib_triple(n) F = fibonacci_seq(n+3); for i = 1:n a = F(i) * F(i+3); b = 2 * F(i+1) * F(i+2); c = F(i+1)^2 + F(i+2)^2; % Will be 1 if true (it is a triple), 0 if not res(i) = istriple(a, b, c); end end Listing 5.17 fibonacci_seq.m 1 2 3 4 5 6 7 8 9 10 % res = fibonacci_seq(n) % computing the fibonacci series out to n function res = fibonacci_seq(n) F(1) = 1; F(2) = 1; for i=3:n F(i) = F(i-1) + F(i-2); end res = F; end 158 Chapter 5 Functions Listing 5.18 istriple.m 1 2 3 4 5 6 7 8 9 10 11 12 % res = istriple a, b, c) % Checking if a, b, and c form a Pythagorean triple. % If yes, then return 1 for true. If fale, return 0. function res = istriple(a, b, c) % We need to be careful using ==. If we find that % it is not working as expected here, we can modify. if hypotenuse(a, b) == c res = 1; else res = 0; end end Note that while I could combine my code into a single function, I have chosen to modulate my code and create three separate function files. This helps make my code more readable, and additionally helps reduce the occurrence of bugs. Independent of fib_triple, I can write istriple and fibonacci_seq, and test/debug them independently. Not to mention, this would facilitate the use of istriple and fibonacci_seq by other functions too. Also note here that I have functions with vector inputs and outputs. We will discuss this further later. 5.13 Mechanism and leap of faith Let’s review the sequence of steps that occur when you call a function: 1. Before the function starts running, MATLAB creates a new workspace for it. 2. MATLAB evaluates each of the arguments and assigns the resulting values, in order, to the input variables (which live in the new workspace). 3. The body of the code executes. Somewhere in the body (often the last line) a value gets assigned to the output variable. 4. The function’s workspace is destroyed; the only thing that remains is the value of the output variable and any side effects the function had (like displaying values or creating a figure). 5. The program resumes from where it left off. The value of the function call is the value of the output variable. When you are reading a program and you come to a function call, there are two ways to interpret it: • You can think about the mechanism I just described, and follow the execution of the program into the function and back, or 5.14 Examples 159 • You can take the “leap of faith”: assume that the function works correctly, and go on to the next statement after the function call. When you use built-in functions, it is natural to take the leap of faith, in part because you expect that the built-in MATLAB functions work, and in part because you don’t generally have access to the code in the body of the function. But when you start writing your own functions, you will probably find yourself following the “flow of execution.” This can be useful while you are learning, but as you gain experience, you should get more comfortable with the idea of writing a function, testing it to make sure it works, and then forgetting about the details of how it works. Forgetting about details is called abstraction; in the context of functions, abstraction means forgetting about how a function works, and just assuming (after appropriate testing) that it works. 5.14 Examples Example 5.7 Write a function isodd to check if an integer is odd. Once you are certain isodd is working correctly, write a function that multiplies two numbers (say m and n) if and only if only one of the numbers is odd. Otherwise, add them together. Note, this may seem like a silly thing to do. But what I want is for you to try and use a logical function as the condition in an if statement. Also, before writing isodd, have a look at iseven in Example 5.5. Solution: Listing 5.19 isodd.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 % res = isodd(n) % A function to determine if integer n is odd. An odd number is a % number that when divided by 2 the remainder is 1. We will compute the % remainder of dividing n by 2 using the MATLAB function mod. If % mod(n,2) is found to be 1, we have an odd number so set res = 1 (true). % Otherwise, set res = 0 (false). % function res = isodd(n) r = mod(n,2); if r == 1 res = 1; else res = 0; end end 160 Chapter 5 Functions Listing 5.20 one_odd.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 % res = one_odd(n,m) % The function multiplies n*m if and only if only only one number is % odd. Otherwise, it adds them together. This function uses the % function isodd, so it must be in the same folder or in your path. % function res = one_odd(n,m) % Rather than call isodd a bunch of times, call it once % and store the result to a variable flag_n = isodd(n); flag_m = isodd(m); % % If both n and m are odd, add the numbers together % Note that since true is equal ts 1, we can leave out the % == 1 if flag_n && flag_m res = n+m; % If both n and m are not odd, they are both even, % add the numbers together elseif flag_n == 0 && flag_m==0 res = n+m; % If both numbers are not odd and both numbers are not even, then % it must be that one number is even and one number is odd. For % this case multiply the numbers together else res = n*m; end % end if end % end function 5.15 CPB Examples Example 5.8 Let’s keep working with Antoine’s equation (the subject of Examples 1.5, 2.4, 3.9, and 4.9. a) Write a function to compute p sat using the Antoine equation. The user should provide as input variables the parameters A, B , and C , along with the desired temperature. In your documentation, please be sure to inform the user of the units. b) Building on (a), could you allow the user to choose what units they would like the pressure returned in? c) Write a function to generate a Clapeyron plot using Antoine’s equation (i.e., plot log10 p sat versus 1/T ). The user should provide as input variables the parameters A, B , and C , along with the desired temperature range (Tmin and Tmax ). In your documentation, please be sure to inform the user of the units. Note than when you generate a plot from within a function, it will be displayed in a new Figure Window . 5.15 CPB Examples 161 Solution: a) The first part to this problem is straightforward. We will create a function that reads in the Antoine parameters A, B, and C (which assume the temperature is in ◦ C and the pressure is in mmHg), and the temperature of interest in ◦ C, and returns the vapor pressure in mmHg. Notice that I do use element-wise operations, so our function is vectorized and you could pass a vector of temperatures for which to perform calculations. Listing 5.21 antoine_p.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 % res = antoine_p( a,b,c,t_C ) % % Computing the vapor prssure of a fluid using Antoine's equation. The user % must provide Antoine parameters A, B, and C. I will assume they are from % the same source as the ethanol parameters we have used previously, so the % default units will be t in degress C and p in mmHg. Please be sure to % check this for future cases. The user must also provide the temperature % in degrees C (t_C). The result will be pressure in mmHg. % % a,b,c = Antoine parameters with t in C and p in mmHg % t_C = temperature in Celsius % res = vapor pressure in mmHg % function res = antoine_p( a,b,c,t_C ) log_p_mmHg = a-b./(t_C+c); res = 10.^(log_p_mmHg); end b) Next, we update our function to take on the an additional flag variable that indicates which units to use. In my case, units will be an integer between 1 and 4. I like to use integer values for flag variables as they facilitate logical comparisons. Rather than re-type my Antoine code, I call the function we just wrote. While this may seem silly, it is a case of incremental development. Since our previous code was bug free, any errors introduced must solely be do to our logical comparisons. 162 Chapter 5 Functions Listing 5.22 antoine_p_units.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 % res = antoine_p_units( a,b,c,t_C,units ) % % Computing the vapor prssure of a fluid using Antoine's equation. The user % must provide Antoine parameters A, B, and C. I will assume they are from % the same source as the ethanol parameters used previously, so the % default units will be t in degress C and p in mmHg. Please be sure to % check this for future cases. The user must also provide the temperature % in degrees C (t_C). This will result in a pressure in mmHg. % In this version of the code, the user must also provide a value for % units, which will indicate the desired units for the final reported % pressure. The value of units is as follows: % % a,b,c = Antoine parameters with t in C and p in mmHg % t_C = temperature in Celsius % units = 1 for mmHg % = 2 for atm % = 3 for kPa % = 4 for bar % function res = antoine_p_units( a,b,c,t_C,units ) % First, call antoine_p to compute p in mmHg p_mmHg = antoine_p( a,b,c,t_C ); % Checking for desired units if units == 1 % mmHg res = p_mmHg; elseif units == 2 % atm res = p_mmHg/760; elseif units == 3 % kPa res = p_mmHg*101.325/760; elseif units == 4 % bar res = p_mmHg*101.325/760/100; else disp('Invalid choice of units. p is returned in mmHg.') res = p_mmHg; end % if statement end % function You may wish instead to perform logical comparisons with strings. This is possible, but you need to be careful. As we discussed previously in the text, when you create a string, MATLAB saves the string as a vector, with the index going 1 to the number of letters, from left to right. When comparing strings: 1) MATLAB will compare each element for equivalence and return a logical vector of 1’s and 0’s, 2) MATLAB is case sensitive. c) Last but not least, we will create a function to create a Clapeyron plot. I will not use a for loop but instead will use vector operations. In this case I directly perform the Antoine calculation, although we could just as well used our function from the first part of this problem. 5.15 CPB Examples 163 Listing 5.23 antoine_clap_plot.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 % res = antoine_clap_plot( a,b,c,t_C_min,t_C_max ) % % Generating a Clapeyron plot of a fluid using Antoine's equation. The user % must provide Antoine parameters A, B, and C. I will assume they are from % the same source as the ethanol parameters used previously, so the % default units will be t in degress C and p in mmHg. Pleasue be sure to % check this for future cases. The user must also provide the % miniumum and maximum temperature in degrees C (t_C_min and t_C_max). % % I will then generate a Clapeyron plot over the range, or a plot of % log10 psat vs 1/T. % Note that here I will convert t to K. % % Also, note that I will not assign anything to res, so nothing will % be returned from the function to my Workspace. Only the plot will % be generated. % % a,b,c = Antoine parameters with t in C and p in mmHg % t_C_min and t_C_max = minimum and maximum temperature in Celsius % res = Nothing is returned % function res = antoine_clap_plot( a,b,c,t_C_min,t_C_max ) % Creating a vector of temperatures in C from tmin to tmax in % increments of 1. t_C = t_C_min:1:t_C_max; % Computing log10 psat with psat in mmHg for each temperature. % Here I will use elementwise operations. log_p_mmHg =a-b./(t_C+c); % Computing 1/t with t in K tinv = 1./(t_C+273.15); % Plotting plot(log_p_mmHg,tinv) % labeling the axis xlabel('1/T [1/K]') ylabel('log10 p/mmHg') % plot title title('Clapeyron plot') end Example 5.9 We wrote a function to perform Antoine equation calculations, how about the Maxwell-Boltzmann equation? (Examples 1.6, 2.5, 3.10, 4.10). Write a function mb_speed that takes as input molecular weight and temperature, and returns the average molecular speed. Please be sure to communicate to the user the units of molecular weight, temperature, and the speed. 164 Chapter 5 Functions Solution: As was the case for our Antoine function, this problem is straightforward. We will encapsulate and convert our Maxwell-Boltzmann script to a function. Listing 5.24 mb_speed.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 % % % % % % % % % % % % % function res = mb_speed( mw, t_C ) Function to compute the average molecular speed of a species using the Maxwell-Boltzmann equation. The user must provide the molecular weight of the species in g/mol (or equivalently Da or amu), and the desired temperature in oC. The result will be the speed in m/s. mw = molecular weight in g/mol t_C = temperature in Celsius res = speed in m/s function res = mb_speed( mw, t_C ) % Convert the temperature from oC to K t_K = t_C+273.15; % Avogadro's number navo = 6.022e23; % Gas constant in J/mol K r = 8.314; % Boltzmann's constant, kB, in J/K kB = r/navo; % Calculating the mass of a molecule in kg mass_kg = mw/navo/1000; % Calculating the speed in m/s res = sqrt(3*kB*t_K/mass_kg) end 5.15 CPB Examples 165 Example 5.10 A central goal of phase-equilibrium thermodynamics is understanding, modeling, and predicting the pvT behavior of a fluid. By pvT , we mean the relationship between pressure (p), molar volume (v), and the temperature (T ). For a single component, single phase system, two intensive thermodynamic properties are needed to fix the state of the system. So if we specify T and p, we can compute v. Put differently, v = f (T, p). Note that p, v, and T , are all intensive, so any two could be used to fix the state of the system (and to compute the third property). For this exercise we will write two functions to compute v at a specified T and p using two different methods. For all cases, we will apply the methods to estimate the molar volume of n-butane at 350 K and 2 bar for which n-butane is a superheated vapor, and at 440 K and 60 bar for which n-butane is a superheated vapor. (Remember 1 bar = 1 × 105 Pa.) For all cases, please report v in units of cm3 /mol. At these conditions NIST WebBook reports v = 14, 043 cm3 /mol at 350 K and 2 bar, and v = 172.98 cm3 /mol at 440 K and 60 bar. 1 . How do your predictions compare? Note that you are making predictions, which will disagree with the reference data. a) Write a function to calculate v at a specified T and p using the ideal gas equation of state. Remember pV ig = nRT so that v ig = V ig RT = n p (5.1) where the superscript “ig” is used to designate the property of an ideal gas. b) The equation of state for a “real” fluid is pv = Z RT , where Z is the compressibility factor. For a real fluid then we have v=Z RT = Z v ig p (5.2) So we see that Z can be thought of as a correction factor that accounts for deviations from ideal gas behavior. A first order correction that may be used to estimate Z is the virial equation truncated after two terms Z ≈ 1+ Bp RT (5.3) where B is the second virial coefficient. Okay, so how does one compute the second virial coefficient? One simple method is to use a correlation based on Pitzer’s corresponding states theory B pc = B (0) + ωB (1) RTc Therefore B= ¤ RTc £ (0) B + ωB (1) pc where B (0) = 0.083 − 1 http://webbook.nist.gov/chemistry/fluid/ 0.422 Tr1.6 (5.4) (5.5) (5.6) 166 Chapter 5 Functions B (1) = 0.139 − 0.172 Tr4.2 (5.7) where Tc and p c are the critical temperature and pressure, Tr is the reduced temperature defined as Tr = T /Tc , and ω is the accentric factor. From NIST WebBook for n-butane we have: Tc = 425.125 K, p c = 37.960 bar, and ω = 0.201. Write a function to calculate v at a specified T and p using the truncated virial equation. The user should also provide as input: Tc , p c , and ω. Your function should use the function written for (a) to compute v ig . Solution: Let’s start with the ideal gas equation of state. Once we have this code working, we will be able to re-use it in our virial calculation code. The function will have input variables of the temperature and pressure. Here I will use units of bar for pressure and K for temperature. Within the code I will convert the pressure to SI units, perform the calculation, and then convert the volume to the desired units of cm3 /mol. Listing 5.25 v_ideal_gas.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 % res = v_ideal_gas(t,p_bar) % % Calculating the molar volume of a fluid in units of cm^3/mol using % the ideal gas equation of state. The user must specify the % temperature (t) in units of K, and the pressure (p) in units of bar. % The function will then calculate the molar volume in units of cm^3/mol. % % t = temperature in K % p_bar = pressure in bar % res = molar volume in cc/mol % function res = v_ideal_gas( t,p_bar ) % Converting the pressure from bar to Pa so that everything % is in SI units p = p_bar*1e5; % The molar gas constant in units of J/(mol K), SI units r = 8.314; % With all SI units, computing the molar volume in m^3/mol v = r*t/p; % Converting the molar volume to cm^3/mol res = v*(100^(3)); end Let’s use the function to compute the ideal gas molar volume at T = 350 K and p = 2 bar. >> t_K = 350; >> p_bar = 2; >> vig_cc_mol = v_ideal_gas(t_K,p_bar) vig_cc_mol = 1.4550e+04 5.15 CPB Examples 167 This is only slightly larger than the reference value of 14,043 cm3 /mol. Next, we will try to account for deviations from the ideal gas limit using the truncated virial equation. Using just the second virial coefficient, this can be thought of as a Maclaurin series expansion truncated after the second term. We will use the “real” gas equation of state, v = Z RT /p = Z v ig . So we need just calculate Z , which is dimensionless, then multiply by the molar volume of an ideal gas in our preferred units. Let’s do it! Listing 5.26 v_virial.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 % res = v_virial(t,p_bar,tc,pc_bar,omega) % % Calculating the molar volume of a fluid in units of cm^3/mol using the % truncated virial equation with correspondings states to estimate % the second virial coefficient, B. % The user must specify the temperature (t) and critical temperature(tc) % in units of K, the pressure (p) and critical pressure (pc) in units % of bar, and the accentric factor (which is dimensionless). % The function will then calculate the molar volume in units % of cm^3/mol. % % t,tc = temperature and critical temperature in K % p_bar,pc_bar = pressure and critical pressure in bar % omega = accentric factor (dimensionless) % res = molar volume in cc/mol % % Note that we will use the function v_ideal_gas to compute the % molar volume of an ideal gas, which must be in the current path. % function res = v_virial( t,p_bar,tc,pc_bar,omega ) % First, we will want to work in SI units, so let's convert p_bar and % pc_bar to Pa before we forget p = p_bar*1e5; pc = pc_bar*1e5; % Molar gas constant in J/(mol K) r = 8.314; % Calculating the relative temperature, tr. It is important here that % we are using absolute temperature units. tr = t/tc; % % Now let's calculate B0 and B1 using the provided equations b0 = 0.083-0.422/(tr^(1.6)); b1 = 0.139-0.172/(tr^(4.2)); % Now calculating B b = r*tc/pc*(b0+omega*b1); % % Calculating the compressibility factor Z z = 1 + b*p/(r*t); % % Calculating the ideal gas molar volume using our function % v_ideal_gas which takes as input T in K and p in bar. % It returns the molar volume in units of cm^3/mol. vig = v_ideal_gas(t,p_bar); % 168 Chapter 5 Functions 45 46 47 48 49 end % Finally, calculating the molar volume of our fluid in cm^3/mol using % the truncated virial. Note that since Z is dimensionless, multiplying % by the ideal gas volume in cm^3/mol gives the desired result. res = z*vig; Now let’s test it for our system. For n-butane we are given Tc = 425.125 K, p c = 37.960 bar and ω = 0.201. >> t_K = 350; >> p_bar = 2; >> tc_K = 425.125; >> pc_bar = 37.96; >> omega = 0.201; >> v_cc_mol = v_virial(t_K,p_bar,tc_K,pc_bar,omega) v_cc_mol = 1.4044e+04 Nice! The truncated virial equation predicts a molar volume of 14,044 cm3 /mol, in excellent agreement with the reference value of 14,043 cm3 /mol. Next, let’s look at the more challenging problem of T = 440 K and p = 60 bar where n-butane is a supercritical fluid, and v = 172.98 cm3 /mol. Let’s look at predictions made using the ideal gas equation of state and the truncated virial equation at these conditions. First, the case of an ideal gas: >> t_K = 440; >> p_bar = 60; >> vig_cc_mol = v_ideal_gas(t_K,p_bar) vig_cc_mol = 609.6933 This is much larger than the reference value. Next, we will try to account for deviations from the ideal gas limit using the truncated virial coefficient. >> t_K = 440; >> p_bar = 60; >> tc_K = 425.125; >> pc_bar = 37.96; >> omega = 0.201; >> v_cc_mol = v_virial(t_K,p_bar,tc_K,pc_bar,omega) v_cc_mol = 313.2382 Using the truncated virial equation, our prediction is still off from the reference value of v = 172.98 cm3 /mol. However, the prediction is greatly improved relative to the ideal gas equation of state. 5.16 Glossary 5.16 Glossary side-effect: An effect, like modifying the workspace, that is not the primary purpose of a script. name collision: The scenario where two scripts that use the same variable name interfere with each other. input variable: A variable in a function that gets its value, when the function is called, from one of the arguments. output variable: A variable in a function that is used to return a value from the function to the caller. signature: The first line of a function definition, which specifies the names of the function, the input variables and the output variables. silent function: A function that doesn’t display anything or generate a figure, or have any other side-effects. logical function: A function that returns a logical value (1 for “true” or 0 for “false”). encapsulation: The process of wrapping part of a program in a function in order to limit interactions (including name collisions) between the function and the rest of the program. generalization: Making a function more versatile by replacing specific values with input variables. abstraction: The process of ignoring the details of how a function works in order to focus on a simpler model of what the function does. 169 170 Chapter 5 Functions 5.17 Exercises Exercise 5.1 “Yaws’ Critical Property Data for Chemical Engineers and Chemists”2 recommends use of a modified Watson equation to calculate the enthalpy of vaporization as a function of temperature: ∆H vap ¶ µ T n = A 1− B (5.8) where A, B and n are regressed coefficients, T is the temperature in K, and ∆H vap is the enthalpy of vaporization in units of kJ/mol. compound ethanol ethane propane n-butane n-pentane n-hexane n-heptane A 60.8036 21.3420 26.8896 33.0198 39.8543 45.61 49.73 B 516.25 305.42 369.82 425.18 469.65 507.43 540.26 n 0.380 0.403 0.365 0.377 0.398 0.401 0.386 Tmin [K ] 300.00 90.35 85.44 134.86 143.42 177.84 182.56 Tmax [K ] 516.25 305.42 369.82 425.18 469.65 507.43 540.26 Write a function to compute ∆H vap and compare against the values computed with your script from Exercise 2.2. Your function should take as input A, B , n and T , and return ∆H vap . Exercise 5.2 Let’s keep building upon your friction factor code! In your transport phenomenon course (fluid mechanics), you likely solved many problems that required you to read values of friction constants from a Moody chart, or to use analytic expressions for the friction factor of limited range. Recently, Díaz-Damacillo and Plascencia published an article in AIChE Journal titled: “A New Six Parameter Model to Estimate the Friction Factor.”3 In that work, the authors propose a new analytic expression containing six parameters that is capable of estimating the friction factor for flow in pipes at all conditions (i.e., Reynold’s numbers and relative roughness). The proposed expression takes the form: f = 64 λ1 λ2 ³ ´+ ³ + τ −Re Re 1 + exp 1 1 + exp τ2 −Re · 100 600 ² D ´ (5.9) where f is the friction factor, Re is the Reynold’s number, defined as: Re = ρV D µ where ρ is the density of the fluid, V is the fluid flow velocity, and D is the diameter of the pipe. The term ² is the pipe roughness, and the term ²/D is dimensionless and commonly referred to as the relative roughness. In addition to Re and ²/D (two parameters), the other six parameters are λ1 , λ2 , τ1 , and τ2 . The parameter λ1 is the residual stress contribution 2 Yaws, Carl L. (2012; 2013; 2014). Yaws’ Critical Property Data for Chemical Engineers and Chemists. Knovel. Retrieved from https://app.knovel.com/hotlink/toc/id:kpYCPDCECD/ yaws-critical-property/yaws-critical-property 3 L. Díaz-Damacillo and G. Plascencia, AIChE J. 2019, 65, 1144–1148. DOI: 10.1002/aic.16535 5.17 Exercises 171 from the laminar to turbulant transition to the friction factor, λ2 is the residual stress contribution from the pipe roughness to the friction factor, τ1 is the value of Re at which occurs the first transition in the friction factor, and τ2 is the value of Re at which the second transition occurs. The values of λ1 and τ1 are constant and equal to: λ1 = 0.02 τ1 = 3000 and λ2 and τ2 are given by the following expressions: !2 ¯ ¯ ¯ ¢ ¯ 1 ² ¯ 3.7065 · D ¯ à ¯ 1 ¯ λ2 = ¯λ1 − ¡ ¯ −2 log 10 0.77505 10.984 τ2 = ¡ ¢2 − ¡ ² ¢ + 7953.8 ² D D As you try to keep track of units, remember that Re and ²/D are dimensionless. a) Write a function that takes as input Re and ²/D, and returns f . As a reference to check your code, solving I find that for Re = 1 × 105 with ²/D = 1/30 I get f = 0.0601. With ²/D = 1/1014 I get f = 0.0207. Note that given the values of ²/D, you might also consider instead using D/² as your input variable. b) Next, use your function to plot f versus Re for a given value of ²/D. That is, make your own Moody chart. Cool! For the purpose of this question, write a script or function where you loop over Re values over the range of 1 × 103 to 1 × 105 , for each Re use your function to compute f for the case of ²/D = 0.1 and ²/D = 0.001, and plot. In a Moody chart, a log-scale is used for both the x- and y-axis. To do this, replace the plot command with the loglog command. The command loglog works exactly the same as plot, only it uses log axes. Be sure to label your axes, label your data sets, and print your figure to file. And if you would like to include a grid in your plot, like an actual Moody chart, use the command grid on. After you plot, execute the command grid on. As a future refrence, if you wanted to make a semi-log plot where only one of the axes uses a log-scale and the other uses a linear-scale, you can use the command semilogx or semilogy. In the next chapter we will look at having vectors as inputs and outputs in our functions, and will revisit this problem. Chapter 6 Functions of vectors In Chapter 6 we finish building our foundational knowledge of MATLAB with a continued discussion of functions. By the end of this chapter you will be able to: • Extend ability to create functions to vector inputs and outputs • Demonstrate the ability to create vectorized functions • Write functions with multiple/optional output variables If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 6.1 Functions and files So far we have only put one function in each file. It is also possible to put more than one function in a file, but only the first one, the top-level function can be called from the Command Window . The other helper functions can be called from anywhere inside the file, but not from any other file. Large programs almost always require more than one function; keeping all the functions in one file is convenient, but it makes debugging difficult because you can’t call helper functions from the Command Window . To help with this problem, I often use the top-level function to develop and test my helper functions. For example, for Example 5.10 we wrote a function v_virial to compute the compressibility (Z ) of a fluid using the truncated virial equation with corresponding states theory. To compute the molar volume (v) in cm3 /mol, we called a second function v_ideal_gas to compute the molar volume of an ideal gas (v ig ) and then computed the molar volume as v = Z v ig . Both functions were stored in separate files. Since every time I wish to estimate the molar volume using the truncated virial I will need to calculate v ig , it would be convenient to keep both functions in the same file. My final file might look as follows: 173 174 Chapter 6 Functions of vectors Listing 6.1 v_virial.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 % res = v_virial(t,p_bar,tc,pc_bar,omega) % % Calculating the molar volume of a fluid in units of cm^3/mol using the % truncated virial equation with correspondings states to estimate % B. The user must specify the temperature (t) and critical temperature(tc) % in units of K, the pressure (p) and critical pressure (pc) in units % of bar, and the accentric factor (which is dimensionless). % The function will then calculate the molar volume in units % of cm^3/mol. % function res = v_virial( t,p_bar,tc,pc_bar,omega ) % First, we will want to work in SI units, so let's convert p_bar and % pc_bar to Pa before we forget p = p_bar*1e5; pc = pc_bar*1e5; % Molar gas constant in J/(mol K) r = 8.314; % Calculating the relative temperature, tr. It is important here that we % are using absolute temperature units. tr = t/tc; % % Now let's calculate B0 and B1 using the provided equations b0 = 0.083-0.422/(tr^(1.6)); b1 = 0.139-0.172/(tr^(4.2)); % Now calculating B b = r*tc/pc*(b0+omega*b1); % % Calculating the compressibility factor Z z = 1 + b*p/(r*t); % % Calculating the ideal gas molar volume. We will use our function to do % this which takes as input T in K and p in bar. It returns the molar % volume in units of cm^3/mol, which is what we desire. vig = v_ideal_gas(t,p_bar); % % Finally, calculating the molar volume of our fluiding in cm^3/mol using % the truncated virial. Note that since Z is dimensionless, multiplying by % the ideal gas volume in cm^3/mol gives the desired result. res = z*vig; end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % res = v_ideal_gas(t,p) % % Calculating the molar volume of a fluid in units of cm^3/mol using % the ideal gas equation of state. The functiona call must pass % the temperature (t) in units of K, and the pressure (p) in % units of bar. The function will then calculate the molar volume in units % of cm^3/mol. % function res = v_ideal_gas( t,p_bar ) % Converting the pressure from bar to Pa so that everything is in SI units 6.1 Functions and files 54 55 56 57 58 59 60 61 p = p_bar*1e5; % The molar gas constant in units of J/(mol K), SI units r = 8.314; % With all SI units, computing the molar volume in m^3/mol v = r*t/p; % Converting the molar volume to cm^3/mol res = v*(100^(3)); end First, as mentioned earlier, helper functions can not be called from the Command Window . Here is what would happen if I tried: >> v_ideal_gas(440,60) Undefined function or variable 'v_ideal_gas'. The only function that MATLAB can see from the Command Window is the toplevel function. Trying instead >> v_virial(440,60,425.125,37.960,0.201) ans = 313.2382 It works as expected. In writing v_virial for Exercise 5.10, we first wrote v_ideal_gas and tested it from the Command Window to ensure it was working. We just saw that we can not do that here. Here I might start by writing v_ideal_gas in two steps. First, I might create a file v_virial.m and start with a top-level function named v_virial that takes no input variables and returns no output value. I would then write a (helper) function v_ideal_gas. My first test would be to pass a variable from v_virial to v_ideal_gas and back to v_virial. The purpose of this is to make sure I have my file set-up correctly and that the functions can properly communicate with each other. This code might look like the following 1 2 3 4 function res = v_virial() test_pass = 10 test_return = v_ideal_gas(test_pass) end 5 6 7 8 9 function res = v_ideal_gas(r) test_in = r res = r; end Executing from the Command Window 175 176 Chapter 6 Functions of vectors >> v_virial test_pass = 10 test_in = 10 test_return = 10 Nice! Next, I would pass from v_virial a value of the temperature and pressure to v_ideal_gas and compute v ig to check for correctness, just as we did before from the Command Window . 1 2 3 function res = v_virial () test = v_ideal_gas(440,60) end 4 5 6 7 8 9 10 function res = v_ideal_gas(t,p_bar) p = p_bar*1e5; r = 8.314; v = r*t/p; res = v*(100^(3)); end From the Command Window >> v_virial test = 609.6933 Perfect! Now we are confident v_ideal_gas is bug free. So next if we move on and write v_virial, we know any bugs are in v_virial and not elsewhere. I might next write v_virial in two step. First, I can calculate Z with all of my parameters specified within v_virial. This way I can localize any errors to v_virial and not due to an issue passing variables. 6.2 Vectors as input variables 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function res = v_virial() % parameters t = 440; p_bar = 60; tc = 425.15; pc_bar = 37.960; omega = 0.201; % p = p_bar*1e5; pc = pc_bar*1e5; r = 8.314; tr = t/tc; b0 = 0.083-0.422/(tr^(1.6)); b1 = 0.139-0.172/(tr^(4.2)); b = r*tc/pc*(b0+omega*b1); z = 1 + b*p/(r*t); vig = v_ideal_gas(t,p_bar); res = z*vig; end 20 21 22 23 24 25 26 function res = v_ideal_gas(t,p_bar) p = p_bar*1e5; r = 8.314; v = r*t/p; res = v*(100^(3)); end Notice that I have left my comments out here to save space. Running from the Command Window : >> v_virial ans = 313.2382 Last, we would pass the parameters to the function, which would bring us back to our final code. This code was relatively simple. If you had a larger function, you might build it up in smaller, testable parts. For this problem I need only two functions, but if there were more, I could write and test them one at a time, and then combine them into a working program. 6.2 Vectors as input variables Since many of the built-in functions take vectors as arguments, it should come as no surprise that you can write functions that take vectors. Here’s a simple (silly) 177 178 Chapter 6 Functions of vectors example: 1 2 3 function res = display_vector(X) X end There’s nothing special about this function at all. The only difference from the scalar functions we’ve seen is that I used a capital letter to remind me that X is a vector. This is another example of a function that doesn’t actually have a return value; it just displays the value of the input variable: >> display_vector(1:3) X = 1 2 3 Here’s a more interesting example that encapsulates the code from Section 4.15 (page 102) that adds up the elements of a vector: Listing 6.2 mysum.m 1 2 3 4 5 6 7 function res = mysum(X) total = 0; for i=1:length(X) total = total + X(i); end res = total; end I called it mysum to avoid a collision with the built-in function sum, which does pretty much the same thing. Here’s how you call it from the Command Window : >> total = mysum(1:3) total = 6 Because this function has a return value, I made a point of assigning it to a variable. 6.3 Vectors as output variables There’s also nothing wrong with assigning a vector to an output variable. Here’s an example that encapsulates the code from Section 4.16 (page 103): 6.3 Vectors as output variables Listing 6.3 myapply.m 1 2 3 4 5 6 7 8 function res = myapply(X) % Pre-size Y, the vector containing the result Y = zeros(1,length(X)); for i=1:length(X) Y(i) = X(i)^2; end res = Y; end Ideally I would have changed the name of the output variable to Res, as a reminder that it is supposed to get a vector value, but I didn’t. Here’s how myapply works: >> V = myapply(1:3) V = 1 4 9 Example 6.1 Write a function named find_negative_element that encapsulates the code, from Example 4.9, that finds the location of (or index of) the first negative number in a vector. Solution: (Link to screen cast and accompanying M-files.) Listing 6.4 find_negative_element.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 % % % % % % % function res = find_negative_element(X) Function to find the index of the first negative number in vector X. An answer will only be returned to the Command Window if a negative number is found. This is an improvement over the previous version of our code. function res = find_negative_element(X) for i=1:length(X) if X(i) < 0 res = i; break end end end Note that if you desired, if there is no negative element, you could have MATLAB throw an error message with the command, error. 179 180 Chapter 6 Functions of vectors 6.4 Multiple/optional output variables When we first introduced functions in Section 5.2, we mentioned that you could create a new M-file in MATLAB’s Editor by using the New button, then select Function from the dropdown menu. If done this way, MATLAB will provide the following template for a new function. Listing 6.5 untitled.m 1 2 3 function [ output_args ] = untitled( input_args ) %UNTITLED Summary of this function goes here % Detailed explanation goes here 4 5 6 end We notice two differences. First, the documentation is placed under the function signature. Recall, in scripts, we always put the documentation at the top of the file. For functions, you can add the documentation either immediately above or below the function signature. Either will work and provide the same output when you use help or doc. My preference is to put the documentation at the top. This way I am using the same convention for both scripts and functions, and the top of the file is where I am accustomed to looking for this information. The second difference is the output variable is placed in brackets. At first glance, after our previous discussion, it may appear that MATLAB is packing multiple variables up into a vector to be returned. But there is more than meets the eye. We can in fact place multiple variables separated by a comma in between the brackets to return multiple variables. However, whether or not more than one variable is actually returned is dependent on how the function is called. Whether subsequent variables (the variables after the first) are actually returned is optional, although they do need to be assigned within the function. Let’s take a look at an example to see what I mean. I will work with our truncated virial equation function, Listing 6.1, which we are already familiar with. In the interest of space, I will remove all of the documentation. I will also keep appending the name of the top function and the file so that I can save a unique copy of each iteration. Let’s start by taking the output variable (res) in the function signature of the top function (v_virial) and put it in brackets. 6.4 Multiple/optional output variables Listing 6.6 v_virial_1.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 function [res] = v_virial_1( t,p_bar,tc,pc_bar,omega ) % First, we will want to work in SI units, so let's convert p_bar and % pc_bar to Pa before we forget p = p_bar*1e5; pc = pc_bar*1e5; % Molar gas constant in J/(mol K) r = 8.314; % Calculating the relative temperature, tr. It is important here that we % are using absolute temperature units. tr = t/tc; % % Now let's calculate B0 and B1 using the provided equations b0 = 0.083-0.422/(tr^(1.6)); b1 = 0.139-0.172/(tr^(4.2)); % Now calculating B b = r*tc/pc*(b0+omega*b1); % % Calculating the compressibility factor Z z = 1 + b*p/(r*t); % % Calculating the ideal gas molar volume. We will use our function to do % this which takes as input T in K and p in bar. It returns the molar % volume in units of cm^3/mol, which is what we desire. vig = v_ideal_gas(t,p_bar); % % Finally, calculating the molar volume of our fluiding in cm^3/mol using % the truncated virial. Note that since Z is dimensionless, multiplying by % the ideal gas volume in cm^3/mol gives the desired result. res = z*vig; end function res = v_ideal_gas( t,p_bar ) % Converting the pressure from bar to Pa so that everything is in SI units p = p_bar*1e5; % The molar gas constant in units of J/(mol K), SI units r = 8.314; % With all SI units, computing the molar volume in m^3/mol v = r*t/p; % Converting the molar volume to cm^3/mol res = v*(100^(3)); end Let’s confirm that we get the same answer as before: >> v_virial_1(440,60,425.125,37.960,0.201) ans = 313.2382 Nice! Next, imagine you might also wish to return the computed compressibility, z, and the ideal gas molar volume, vig. We will add both to the brackets, separating the variables by a comma. 181 182 Chapter 6 Functions of vectors Listing 6.7 v_virial_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 function [res,z,vig] = v_virial_2( t,p_bar,tc,pc_bar,omega ) % First, we will want to work in SI units, so let's convert p_bar and % pc_bar to Pa before we forget p = p_bar*1e5; pc = pc_bar*1e5; % Molar gas constant in J/(mol K) r = 8.314; % Calculating the relative temperature, tr. It is important here that we % are using absolute temperature units. tr = t/tc; % % Now let's calculate B0 and B1 using the provided equations b0 = 0.083-0.422/(tr^(1.6)); b1 = 0.139-0.172/(tr^(4.2)); % Now calculating B b = r*tc/pc*(b0+omega*b1); % % Calculating the compressibility factor Z z = 1 + b*p/(r*t); % % Calculating the ideal gas molar volume. We will use our function to do % this which takes as input T in K and p in bar. It returns the molar % volume in units of cm^3/mol, which is what we desire. vig = v_ideal_gas(t,p_bar); % % Finally, calculating the molar volume of our fluiding in cm^3/mol using % the truncated virial. Note that since Z is dimensionless, multiplying by % the ideal gas volume in cm^3/mol gives the desired result. res = z*vig; end function res = v_ideal_gas( t,p_bar ) % Converting the pressure from bar to Pa so that everything is in SI units p = p_bar*1e5; % The molar gas constant in units of J/(mol K), SI units r = 8.314; % With all SI units, computing the molar volume in m^3/mol v = r*t/p; % Converting the molar volume to cm^3/mol res = v*(100^(3)); end Let’s try running as before, and let’s also try assigning the output to a variable: >> v_virial_2(440,60,425.125,37.960,0.201) ans = 313.2382 >> v = v_virial_2(440,60,425.125,37.960,0.201) v = 313.2382 6.5 Vectorizing your functions Hey, what gives! The compressibility and ideal gas molar volume are not returned. The issue is we need to tell MATLAB to return them. Here’s how. First we will return none, then one and both of the optional variables. >> v = v_virial_2(440,60,425.125,37.960,0.201) v = 313.2382 >> [v,z] = v_virial_2(440,60,425.125,37.960,0.201) v = 313.2382 z = 0.5138 >> [v,z,vig] = v_virial_2(440,60,425.125,37.960,0.201) v = 313.2382 z = 0.5138 vig = 609.6933 Cool! We see that what is returned in not a vector, but instead each is returned as its own variable. You will find that I do not always utilize this feature of MATLAB when I wish to return multiple scalar variables. Many times if I wish to return multiple scalar variables, I will often pack them up to a single vector and return it. This way I am certain that I am returning all of the variables regardless of how I call the function. This is just my preference and style. You are free to disagree. Just know that there are some cases where you are not free to choose, such as when we will create function files to be used by fsolve and ode45. Also, know that this feature is powerful in that it can allow you to return variables of different sizes and types, where attempting to pack them up to a single vector or matrix would fail. Last, know that when we call fzero, fsolve and ode45, they were written to return optional variables as done here. 6.5 Vectorizing your functions Functions that work on vectors will almost always work on scalars as well, because MATLAB considers a scalar to be a vector with length 1. >> mysum(17) ans = 17 >> myapply(9) ans = 81 Unfortunately, the converse is not always true. If you write a function with scalar inputs in mind, it might not work on vectors. But it might! If the operators and functions you use in the body of your function work on vectors, then your 183 184 Chapter 6 Functions of vectors function will probably work on vectors. For example, here is the very first function we wrote: 1 2 3 4 5 function res = myfunc (x) s = sin(x) c = cos(x) res = abs(s) + abs(c) end And lo! It turns out to work on vectors: >> Y = myfunc(1:3) Y = 1.3818 1.3254 1.1311 At this point, I want to take a minute to acknowledge that I have been a little harsh in my presentation of MATLAB, because there are a number of features that I think make life harder than it needs to be for beginners. But here, finally, we are seeing features that show MATLAB’s strengths. MATLAB is extremely good with matrix (and vector) operations. In fact, MATLAB’s name comes from MAtrix LABoratory. We started the text working with scalars and for loops because I assumed most of you had no prior programming experience. Many students initially find thinking like a computer to be challenging, let alone thinking about vector operations. You can always use for loops, and when in doubt, it is good to go this route. But matrix (or vector) operations are much more efficient (both in terms of execution time and the resulting compactness of your code) and can make your code much easier to read. Some of the other functions we wrote don’t work on vectors, but they can be patched up with just a little effort. For example, here’s hypotenuse from Section 5.5 (and Listing 5.9): 1 2 3 function res = hypotenuse(a, b) res = sqrt(a^2 + b^2); end This doesn’t work on vectors because the ^ operator tries to do matrix exponentiation, which only works on square matrices. >> hypotenuse(1:3, 1:3) Error using ==> mpower Matrix must be square. But if you replace ^ with the elementwise operator .^, it works! 6.6 Sums and differences 185 Listing 6.8 hypotenuse.m 1 2 3 function res = hypotenuse(a, b) res = sqrt(a.^2 + b.^2); end >> A = [3,5,8]; >> B = [4,12,15]; >> C = hypotenuse(A, B) C = 5 13 17 In this case, it matches up corresponding elements from the two input vectors, so the elements of C are the hypotenuses of the pairs (3, 4), (5, 12) and (8, 15), respectively. In general, if you write a function using only elementwise operators and the function works on vectors, then the new function will also work on scalars. Remember, to MATLAB, everything is a matrix. 6.6 Sums and differences Another common vector operation is cumulative sum, which takes a vector as an input and computes a new vector that contains all of the partial sums of the original. In math notation, if V is the original vector, then the elements of the cumulative sum, C , are: Ci = i X Vj j =1 In other words, the i th element of C is the sum of the first i elements from V . MATLAB provides a function named cumsum that computes cumulative sums: >> V = 1:5 V = 1 2 3 4 5 >> C = cumsum(V) C = 1 3 6 10 15 Example 6.2 Write a function named cumulative_sum that uses a loop to compute the cumulative sum of the input vector. As we have seen before, when learning a new skill, writing our own functions to perform the same operations as a built-in function is excellent practice as we can quickly check if we get the correct answer. 186 Chapter 6 Functions of vectors Solution: (Link to screen cast and accompanying M-files.) Listing 6.9 cumulative_sum.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 % function res = cumulative_sum(X) % % Function to compute the cumulative sum of vector X which % is returned as res. % function res = cumulative_sum(X) % Y will be our vector to store the cumulative sum. % Initialize it with values of zero since we will be % performing a summation Y = zeros(1,length(X)); for i=1:length(X) for j=1:i Y(i) = Y(i) + X(j); end end res = Y; end >> V = 1:5 V = 1 2 3 4 >> C = cumulative_sum(V) C = 1 3 6 10 5 15 This agrees exactly with cumsum. The inverse operation of cumsum is diff, which computes the difference between successive elements of the input vector. >> D = diff(C) D = 2 3 4 5 Notice that the output vector is shorter by one than the input vector. As a result, MATLAB’s version of diff is not exactly the inverse of cumsum. If it were, then we would expect cumsum(diff(X)) to be X: >> cumsum(diff(V)) ans = 1 2 3 But it isn’t. 4 6.7 Products and ratios 187 Example 6.3 Write a function named mydiff that computes the inverse of cumsum, so that cumsum(mydiff(X)) and mydiff(cumsum(X)) both return X. Solution: Listing 6.10 mydiff.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 % function res = mydiff(X) % % Function to compute the inverse of the cumulative sum, % which is returned as res. % function res = mydiff(X) % Y will be our vector to store the inverse cumulative sum. % Initialize it with values of X. Y = X; for i=2:length(X) Y(i) = Y(i)-X(i-1); end res = Y; end >> V = 1:5 V = 1 2 3 4 5 >> C = cumulative_sum(V) C = 1 3 6 10 >> VI = mydiff(C) VI = 1 2 3 15 4 5 6.7 Products and ratios The multiplicative version of cumsum is cumprod, which computes the cumulative product. In math notation, that’s: Pi = i Y j =1 In MATLAB, that looks like: >> V = 1:5 V = 1 2 3 4 5 Vj 188 Chapter 6 Functions of vectors >> P = cumprod(V) P = 1 2 6 24 120 Example 6.4 Write a function named cumulative_product that uses a loop to compute the cumulative product of the input vector. Solution: (Link to screen cast and accompanying M-files.) Listing 6.11 cumulative_product.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 % function res = cumulative_product(X) % % Function to compute the cumulative product of vector X which % is returned as res. % function res = cumulative_product(X) % Y will be our vector to store the cumulative product. % Initialize it with values of one since we will be % performing a continuous product. Y = ones(1,length(X)); for i=1:length(X) for j=1:i Y(i) = Y(i)*X(j); end end res = Y; end >> V = 1:5 V = 1 2 3 4 5 >> P = cumulative_product(V) P = 1 2 6 24 120 A perfect match to cumprod. MATLAB doesn’t provide the multiplicative version of diff, which would be called ratio, and which would compute the ratio of successive elements of the input vector. 6.7 Products and ratios 189 Example 6.5 Write a function named myratio that computes the inverse of cumprod, so that cumprod(myratio(X)) and myratio(cumprod(X)) both return X. You can use a loop, or if you want to be clever, you can take advantage of the fact that e ln a+ln b = ab. If you apply myratio to a vector that contains Fibonacci numbers, you can p confirm that the ratio of successive elements converges on the golden ratio, (1 + 5)/2 (see Example 4.6 on page 108). Solution: Listing 6.12 myratio.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 % function res = myratio(X) % % Function to compute the inverse of the continuous product, % which is returned as res. % function res = myratio(X) % Y will be our vector to store the inverse continous product. % Initialize it with values of X. Y = X; for i=2:length(X) Y(i) = Y(i)/X(i-1); end res = Y; end >> V = 1:5 V = 1 2 3 4 5 >> P = cumulative_product(V) P = 1 2 6 24 120 >> VI = myratio(P) VI = 1 2 3 4 5 190 Chapter 6 Functions of vectors Listing 6.13 myratio_clever.m 1 2 3 4 5 6 7 8 9 10 % function res = myratio_clever(X) % % Function to compute the inverse of the continuous product, % which is returned as res. % function res = myratio_clever(X) Y = log(X); Z = mydiff(Y); res = exp(Z); end >> VI = myratio_clever(P) VI = 1 2 3 4 5 6.8 Existential quantification It is often useful to check the elements of a vector to see if there are any that satisfy a condition. For example, you might want to know if there are any positive elements. In logic, this condition is called existential quantification, and it is denoted with the symbol ∃, which is pronounced “there exists.” For example, this expression ∃x in S : x > 0 means, “there exists some element x in the set S such that x > 0.” In MATLAB it is natural to express this idea with a logical function, like exists, that returns 1 if there is such an element and 0 if there is not. Listing 6.14 exists.m 1 2 3 4 5 6 7 8 9 function res = exists(X) for i=1:length(X) if X(i) > 0 res = 1; return end end res = 0; end We haven’t seen the return statement before; it is similar to break except that it breaks out of the whole function, not just the loop. That behavior is what we want here because as soon as we find a positive element, we know the answer 6.9 Universal quantification 191 (it exists!) and we can end the function immediately without looking at the rest of the elements. If we exit at the end of the loop, that means we didn’t find what we were looking for (because if we had, we would have hit the return statement). This isn’t the only way, although it was a nice way to introduce return. I could accomplish the same result with the following function Listing 6.15 exists_2.m 1 2 3 4 5 6 7 8 9 function res = exists_2(X) res = 0; for i=1:length(X) if X(i) > 0 res = 1; break end end end In this case, I initialize res with a value of 0 (false). If I find an element that is positive, then there exists at least one element that is positive and I change the value of res to 1 (true). Once I have found a positive element, there is no need to continue searching since I only asked if there was at least one positive element. I therefore add break which exits us out of the loop and hence the function. Note that if you didn’t include break your function would still work and return the correct result. You will just make the computer work harder than it needs to, which is matter of efficiency. 6.9 Universal quantification Another common operation on vectors is to check whether all of the elements satisfy a condition, which is known to logicians as universal quantification and denoted with the symbol ∀ which is pronounced “for all.” So this expression ∀x in S : x > 0 means “for all elements, x, in the set S, x > 0.” A slightly silly way to evaluate this expression in MATLAB is to count the number of elements that satisfy the condition. A better way is to reduce the problem to existential quantification; that is, to rewrite ∀x in S : x > 0 as 192 Chapter 6 Functions of vectors ∼ ∃x in S : x ≤ 0 Where ∼ ∃ means “does not exist.” In other words, checking that all the elements are positive is the same as checking that there are no elements that are non-positive. Example 6.6 Write a function named forall that takes a vector and returns 1 if all of the elements are positive and 0 if there are any non-positive elements. Solution: (Link to screen cast and accompanying M-file.) Listing 6.16 forall.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 % function res = forall(X) % % A function to check if all of the elements % of X are positive. We know that if at least % one of the elements is negative, then they % are not all positive. So we check this % simpler case. % % Return 1 for true (all positive), 0 for false % function res = forall(X) res = 1; for i = 1:length(X) if X(i) < 0 res = 0; return end end end Note that here I use return which exits us out of the function, but break in this case would cause the same effect. 6.10 Logical vectors When you apply a logical operator to a vector, the result is a logical vector; that is, a vector whose elements are the logical values 1 and 0. >> V = -3:3 V = -3 -2 -1 0 1 2 3 6.10 Logical vectors >> L = V>0 L = 0 0 193 0 0 1 1 1 You can think of logical operators as acting elementwise. In this example, L is a logical vector whose elements correspond to the logical comparison of each of the corresponding elements of V. For each positive element of V, the corresponding element of L is 1, “true”. Logical vectors can be used like flags to store the state of a condition. They are frequently used to extract the elements of a vector or matrix that are “true” (i.e., that satisfy a specific condition). They are also often used with the find function, which takes a logical vector and returns a vector that contains the indices of the elements that are “true.” Applying find to L yields >> find(L) ans = 5 6 7 which indicates that elements 5, 6 and 7 have the value 1. (Remember, 1 corresponds to “true”.) If there are no “true” elements, the result is an empty vector. >> find(V>10) ans = Empty matrix: 1-by-0 This example computes the logical vector and passes it as an argument to find without assigning it to an intermediate variable. You can read this version abstractly as “find the indices of elements of V that are greater than 10.” We can also use find to write exists more concisely: 1 2 3 4 function res = exists(X) L = find(X>0) res = length(L) > 0 end This could also be achieved using a logical vector: 1 2 3 4 5 6 function res = exists(X) res = 1; % Start by assuming true if sum(X>0) == 0 % Check if every element is negative res = 0; end end 194 Chapter 6 Functions of vectors And here is an even more compact solution: 1 2 3 function res = exists(X) res = sum(X>0) ~= 0; end Example 6.7 Write a version of forall using find. Solution: (Link to screen cast and accompanying M-file.) Listing 6.17 forall_find.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 % function res = forall_find(X) % % A function to check if all of the elements % of X are positive. We know that if at least % one of the elements is negative, then they % are not all positive. So we check this % simpler case. % % Return 1 for true (all positive), 0 for false % function res = forall_find(X) L = find(X<0); res = length(L) == 0; end In writing the solution, MATLAB suggests that isempty would be more efficient than checking if a vector has a length of zero. After consulting MATLAB’s documentation, here is what the updated function would look like: 6.10 Logical vectors 195 Listing 6.18 forall_find_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 % function res = forall_find_2(X) % % A function to check if all of the elements % of X are positive. We know that if at least % one of the elements is negative, then they % are not all positive. So we check this % simpler case. % % Return 1 for true (all positive), 0 for false % function res = forall_find_2(X) L = find(X<0); res = isempty(L); end And in the interest of exploring alternatives, we could use a logical vector instead of find. Listing 6.19 forall_logical.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 % function res = forall_logical(X) % % A function to check if all of the elements % of X are positive. We know that if at least % one of the elements is negative, then they % are not all positive. So we check this % simpler case. % % Return 1 for true (all positive), 0 for false % function res = forall_logical(X) L = X<0; res = sum(L)==0; end Not to be out done, we can use the MATLAB function any in the following version, where here we use ∼, the logical “not” expression. 196 Chapter 6 Functions of vectors Listing 6.20 forall_logical_any.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 % function res = forall_logical_any(X) % % A function to check if all of the elements % of X are positive. We know that if at least % one of the elements is negative, then they % are not all positive. So we check this % simpler case. % % Return 1 for true (all positive), 0 for false % function res = forall_logical_any(X) L = X<0; res = ~any(L); end And how did I come up with the idea of using any? MATLAB suggested its use as a comment in forall_find_2. Recall our discussion of vector indexing errors in Section 4.10 on page 92 when we first discussed vectors. We found that we could refer to elements of a vector by either their integer index or their logical value. At that point we said we would discuss the use of logicals later. Well, what better time than now! It is best to see how we might do this using an example. Imagine we again have the vector V: >> V = -3:3 V = -3 -2 -1 0 1 2 3 Using the find command before, we found that elements 5, 6, and 7 were greater than 0. Now imagine if the value of an element is greater than 0, we want to assign it a value of 10. We can do this two ways. First, let’s use integer indices: >> integer_index = find(V>0) ans = 5 6 7 >> V(integer_index) = 10 V = -3 -2 -1 0 10 10 10 And if you had wanted to, the two statements could be combined as V(find(V>0)) = 10. Next, let’s use logical values: >> V = -3:3; >> logical_index = V>0 logical_index = 0 0 0 0 1 1 1 6.11 CPB Examples 197 >> V(logical_index) = 10 V = -3 -2 -1 0 10 10 10 Once again, the two statements could be combined as V(V>0)=10. While here the use of find and a logical vector were equivalent to identify elements of vector V satisfying a given condition, Know that the use of a logical vector is preferred. In addition to an apply operation as was done here, we will frequently want to isolate specific elements from a vector or matrix satisfying a given condition. Let’s consider the same vector V, but know we want assign the elements of V greater than 0 to a new vector VP: >> V = -3:3; >> VP = V(V>0) VP = 1 2 3 While this example is rather trivial, we will find it much more useful in some of our future examples. 6.11 CPB Examples Example 6.8 While I am certain at this point you are tired of the Antoine equation, let’s again revisit our old friend. a) In Example 5.8 part b) we wrote a beautiful function to compute p sat where the user input A, B , C , the desired temperature, and the desired units. Vectorize the function so that you can pass a vector of temperatures and return a vector of values of p sat . To test your new function, compare to your result for Example 5.8 part c). b) With your working function from a), test the use of the built-in find function. When computing p sat using the Antoine equation we provide a range of temperatures, unaware beforehand what the corresponding values of p sat are. We may desire only values of p sat below a certain value. Try using find as a filter for this particular case. That is, check your returned p sat vector for values less than some specified value. Then save these values and the corresponding temperature to a pair of new vectors. Depending on the range of temperatures you are using, try 0.5 times p sat at the highest temperature. c) In b) you used the find function. Could you instead use a logical vector? Remember, the use of a logical vector is preferred. The use of find in b) is an exercise to get you accustomed to using the find command. 198 Chapter 6 Functions of vectors Solution: Antoine equation fun time! a) Okay, we already have a working function. Here we just want to vectorize it so that we can input a vector of temperatures and compute/return a vector of vapor pressures. All that we mean by vectorize is we need to change our *, /, and ˆ operators to their elementwise equivalent by adding a period: .*, ./, and .ˆ. The updated function is provided below: Listing 6.21 antoine_p_units.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 % res = antoine_p_units( a,b,c,tC,units ) % % Computing the vapor prssure of a fluid using Antoine's equation. The user % must provide Antoine parameters A, B, and C. I will assume they are from % the same source as the ethanol parameters provided in class, so the % default units will be t in degress C and p in mmHg. Pleasue be sure to % check this for future cases. The user must also provide the temperature % in degrees C (tC). This will result in a pressure in mmHg. % In this version of the code, the user must also provide a value for % units, which will indicate the desired units for the final reported % pressure. The value of units is as follows: % units = 1 for mmHg % = 2 for atm % = 3 for kPa % = 4 for bar % function res = antoine_p_units( a,b,c,tC,units ) % First, call antoine_p to compute p in mmHg logp_mmHg = a-b./(tC+c); p_mmHg = 10.^(logp_mmHg); % Checking for desired units if units == 1 % mmHg res = p_mmHg; elseif units == 2 % atm res = p_mmHg./760; elseif units == 3 % kPa res = p_mmHg.*101.325./760; elseif units == 4 % bar res = p_mmHg.*101.325./760./100; else disp('Invalid choice of units. p is returned in mmHg') res = p_mmHg; end end To test its use, let’s compute vapor pressure in bar over the range –114.1 to 243.1 ◦ C, the entire range of applicability of our Antoine equation for ethanol. In the interest of space, we will only use a small number of temperatures. >> >> >> >> a = 8.13484; b = 1662.48; c = 238.141; tC = -114.1:30:243.1 6.11 CPB Examples tC = Columns 1 -114.1000 Columns 9 125.9000 199 through 8 -84.1000 -54.1000 through 12 155.9000 185.9000 -24.1000 5.9000 35.9000 65.9000 95.9000 0.0280 0.1560 0.6192 1.9181 0.6192 1.9181 215.9000 >> p_bar = antoine_p_units(a,b,c,tC,4) Columns 1 through 8 0.0000 0.0000 0.0002 0.0031 Columns 9 through 12 4.9318 10.9822 21.8363 39.6482 Nice! Know that the first two pressures are not 0. This is just an issue with MATLAB’s chosen format. >> p_bar(1) ans = 7.1958e-09 b) Now we will use the find command to create a new vector with just the values of p sat less than 0.5p sat at the highest T (since p sat increases with T ). >> psat_max = p_bar(length(tC)) psat_max = 39.648 >> index = find(p_bar < 0.5*psat_max) index = 1 2 3 4 5 6 7 >> p_trim = p_bar(index) p_trim = Columns 1 through 8 0.0000 0.0000 0.0002 Columns 9 through 10 4.9318 10.9822 8 0.0031 9 10 0.0280 0.1560 Also, know that instead of psat_max = p_bar(length(tC)) we could equivalently use psat_max = p_bar(end). end is a special keyword for the last element index. c) Besides find, we could accomplish the same result using a logical comparison. This is what MATLAB would actually prefer. After all, find is undoubtedly performing a logical comparison to determine for which elements the statement is true and false. >> index_l = p_bar < 0.5*psat_max index_l = 1 1 1 1 1 1 1 1 >> p_trim_l = p_bar(index_l) p_trim = Columns 1 through 6 1 1 0 0 200 Chapter 6 Functions of vectors 0.0000 0.0000 0.0002 Columns 7 through 10 0.6192 1.9181 4.9318 0.0031 0.0280 0.1560 10.9822 6.12 Glossary top-level function: The first function in an M-file; it is the only function that can be called from the Command Window or from another file. helper function: A function in an M-file that is not the top-level function; it only be called from another function in the same file. existential quantification: A logical condition that expresses the idea that “there exists” an element of a set with a certain property. universal quantification: A logical condition that expresses the idea that all elements of a set have a certain property. logical vector: A vector, usually the result of applying a logical operator to a vector, that contains logical values 1 and 0. 6.13 Exercises 6.13 Exercises Exercise 6.1 In Example 5.6 we used several functions stored in separate M-files to compute the first n Fibonacci numbers, and then checked whether this formula produced a Pythagorean triple for each number in the sequence. Combine all of these functions into a single file where fib_triple.m is the top-level file. Check the function for correctness by comparing to your results from Example 5.6. Note, do not forget about the M-file hypotenuse, Listing 5.9. Exercise 6.2 Revisit and modify find_negative_element from Example 6.1 to use the find command to return the location (or index) of the first negative element in a vector. Exercise 6.3 Let’s continue to build upon our solution to Exercises 2.1, 3.1 and 4.1. Specifically, encapsulate your script from Exercise 4.1 to compute the surface tension of a fluid. Your function should take as inputs the model parameters and the temperature. Make sure the function is vectorized so that it can take as an input a vector of temperatures and return a vector of surface tensions. To confirm the correctness of your code, compute the surface tension over the entire temperature range for which the expression is applicable for propane, ethanol, and acetone. Compare to your results from Exercise 4.1. Exercise 6.4 In Example 5.10 we used the ideal gas equation of state and the truncated virial equation with corresponding states theory to estimate the molar volume of n-butane at 350 K and 2 bar. Using the truncated virial equation, we computed the compressibility, Z , and then calculated the molar volume as v = Z v ig , where v ig is the molar volume of an ideal gas at the same temperature and pressure. Since our function for the truncated virial (v_virial) is dependent on our function for the molar volume of an ideal gas (v_ideal_gas), in Section 6.1 we decided to combine the functions into a single file, where v_virial is the top function and v_ideal_gas is the helper function. Let’s continue to build upon that function here. Recall that from the NIST WebBook1 for n-butane we have: Tc = 425.125 K, p c = 37.960 bar, and ω = 0.201, a) First, let’s vectorize v_virial and v_ideal_gas. By vectorize, you should be able to pass a vector of temperatures and/or a vector of pressures and compute a vector of molar volumes. b) Test your function by computing the molar volume at a constant pressure of 2 bar over the temperature range 350 to 450 K. How does the molar volume change with respect to temperature at constant pressure? c) Next, update your function so that it additionally returns the compressibility, Z . Following the last question, how does Z change with respect to temperature at constant pressure? Exercise 6.5 Let’s build upon your friction factor code from Exercise 5.2, specifically your Moody chart. Vectorize your function and eliminate the for loop. As a hint, remember your work from Exercise 4.4. 1 http://webbook.nist.gov/chemistry/fluid/ 201 Chapter 7 Zero-finding In Chapter 7 we begin to explore MATLAB’s numerical methods, starting with zero-finding and roots of polynomials. By the end of this chapter you will be able to: • Explain how MATLAB’s zero-finding algorithms work • Exhibit ability to construct “error” functions for use with fzero • Make use of fzero to find zero’s of an equation • Apply roots to obtain all of the roots of a cubic equation of state If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 7.1 Why functions? Chapter 5 explained some of the benefits of functions, including • Each function has its own workspace, so using functions helps avoid name collisions. • Functions lend themselves to incremental development: you can debug the body of the function first (as a script), then encapsulate it as a function, and then generalize it by adding input variables. • Functions allow you to divide a large problem into small pieces, work on the pieces one at a time, and then assemble a complete solution. • Once you have a function working, you can forget about the details of how it works and concentrate on what it does. This process of abstraction is an important tool for managing the complexity of large programs. 203 204 Chapter 7 Zero-finding Another reason you should consider using functions is that many of the tools provided by MATLAB require you to write functions. For example, in this chapter we will use fzero to find solutions of nonlinear equations. In the next chapter we will use fsolve to solve systems of nonlinear equations. And later we will use ode45 to approximate solutions to differential equations. There are many other cases, but there is only so much class time. 7.2 Maps In mathematics, a map is a correspondence between one set called the range and another set called the domain. For each element of the range, the map specifies the corresponding element of the domain. You can think of a sequence as a map from positive integers to elements. You can think of a vector as a map from indices to elements. In these cases the maps are discrete because the elements of the range are countable. You can also think of a function as a map from inputs to outputs, but in this case the range is continuous because the inputs can take any value, not just integers. (Strictly speaking, the set of floating-point numbers is discrete, but since floating-point numbers are meant to represent real numbers, we think of them as continuous.) 7.3 A note on notation In this chapter I need to start talking about mathematical functions, and I am going to use a notation you might not have seen before. If you have studied functions in a math class, you have probably seen something like f (x) = x 2 − 2x − 3 which is supposed to mean that f is a function that maps from x to x 2 − 2x − 3. The problem is that f (x) is also used to mean the value of f that corresponds to a particular value of x. To make clear what this means, here I will instead use arrow notation f : x → x 2 − 2x − 3 which means “ f is the function that maps from x to x 2 − 2x − 3.” In MATLAB, this would be expressed like this: Listing 7.1 error_func.m 1 2 3 function res = error_func(x) res = x.^2 - 2.*x - 3; end 7.4 Nonlinear equations I’ll explain soon why this function is called error_func. Also note that I have vectorized the function; it will work regardless if x is a scalar or vector. Vectorizing your functions is in general a good practice. Now, back to our regularly-scheduled programming. 7.4 Nonlinear equations What does it mean to “solve” an equation? That may seem like an obvious question, but I want to take a minute to think about it, starting with a simple example: let’s say that we want to know the value of a variable, x, but all we know about it is the relationship x 2 = a. If you have taken algebra, you probably know how to p “solve” this equation: you take the square root of both sides and get x = a. Then, with the satisfaction of a job well done, you move on to the next problem. But what have you really done? The relationship you derived is equivalent to the relationship you started with—they contain the same information about x—so why is the second one preferable to the first? There are two reasons. One is that the relationship is now “explicit in x;” because x is all alone on the left side, we can treat the right side as a recipe for computing x, assuming that we know the value of a. The other reason is that the recipe is written in terms of operations we know how to perform. Assuming that we know how to compute square roots, we can compute the value of x for any value of a. When people talk about solving an equation, what they usually mean is something like “finding an equivalent relationship that is explicit in one of the variables.” In the context of this book, that’s what I will call an analytic solution, to distinguish it from a numerical solution, which is what we are going to do next. To demonstrate a numerical solution, consider the equation x 2 − 2x = 3. You could solve this analytically, either by factoring it or by using the quadratic equation, and you would discover that there are two solutions, x = 3 and x = −1. p Alternatively, you could solve it numerically by rewriting it as x = 2x + 3. This equation is not explicit, since x appears on both sides, so it is not clear that this move did any good at all. But suppose that we had some reason to expect there to be a solution near 4. We could start with x = 4 as an “initial p guess,” and then use the equation x = 2x + 3 iteratively to compute successive approximations of the solution. Here’s what would happen: >> x = 4; >> x = sqrt(2*x+3) x = 3.3166 >> x = sqrt(2*x+3) x = 3.1037 205 206 Chapter 7 Zero-finding >> x = sqrt(2*x+3) x = 3.0344 >> x = sqrt(2*x+3) x = 3.0114 >> x = sqrt(2*x+3) x = 3.0038 After each iteration, x is closer to the correct answer, and after 5 iterations, the relative error is about 0.1%, which is good enough for most purposes. You could readily use a loop to automate the process. Techniques that generate numerical solutions are called numerical methods. The nice thing about the method I just demonstrated is that it is simple, but it doesn’t always work as well as it did in this example, and it is not used very often in practice. We’ll see one of the more practical alternatives in a minute. 7.5 Zero-finding A nonlinear equation like x 2 − 2x = 3 is a statement of equality that is true for some values of x and false for others. A value that makes it true is a solution; any other value is a non-solution. But for any given non-solution, there is no sense of whether it is close or far from a solution, or where we might look to find one. To address this limitation, it is useful to rewrite nonlinear equations as zerofinding (or root-finding) problems: • The first step is to define an “error function” that computes how far a given value of x is from being a solution. In this example, the error function is f : x → x 2 − 2x − 3 Any value of x that makes f (x) = 0 is also a solution of the original equation. More formally, solving the equation g (x) = h (x) is equivalent to finding the zeros (or roots) of the function f (x) = g (x) − h (x). • The next step is to find values of x that make f (x) = 0. These values are called zeros of the function, or equivalently roots. Zero-finding lends itself to numerical solution because we can use the values of f , evaluated at various values of x, to make reasonable inferences about where to look for zeros. For example, if we can find two values x l and x r such that f (x l ) > 0 and f (x r ) < 0, and f is continuous, then per the intermediate value theorem we can 7.6 fzero 207 be certain that there is at least one zero between x l and x r . In this case we would say that x l and x r bracket a zero. This is the basis of bracketing methods of root finding. Graphically this scenario might look like fig. 7.1. Figure 7.1 Graphic example of x l and x r that bracket a zero. If this was all you knew about f , where would you go looking for a zero? If you said “halfway between x l and x r ,” then congratulations! You just invented a numerical method called bisection! If you said, “I would connect the dots with a straight line and compute the zero of the line,” then congratulations! You just invented the secant (or linear interpolation) method! And if you said, “I would evaluate f at a third point, find the parabola that passes through all three points, and compute the zeros of the parabola,” then... well, you probably didn’t say that. Finally, if you said, “I would use a built-in MATLAB function that combines the best features of several efficient and robust numerical methods,” then you are ready to go on to the next section. 7.6 fzero fzero is a built-in MATLAB function that combines the best features of several efficient and robust numerical methods. Specifically, fzero is a bracketing method of root finding that uses bisection, secant, and inverse quadratic interpolation methods. In order to use fzero, you have to define a MATLAB function that computes the error function you derived from the original nonlinear equation, and you have 208 Chapter 7 Zero-finding to provide two values that bracket the location of a zero or an initial guess of the location of a zero; if you provide an initial guess, it is actually used to find brackets. Providing brackets is preferred, but often an initial guess is more convenient. We’ve already seen an example of an error function in Listing 7.1: 1 2 3 function res = error_func(x) res = x.^2 - 2.*x - 3; end You can call error_func from the Command Window , and confirm that there are zeros at 3 and -1. >> error_func(3) ans = 0 >> error_func(-1) ans = 0 But let’s pretend that we don’t know exactly where the roots are; we only know that one of them is near 4. Then we could call fzero like this: >> fzero(@error_func, 4) ans = 3 Success! We found one of the zeros. The first argument is a function handle that names the M-file that evaluates the error function. The @ symbol allows us to name the function without calling it. The interesting thing here is that you are not actually calling error_func directly; you are just telling fzero where it is. In turn, fzero calls your error function—more than once, in fact. The second argument is the initial guess. If we provide a different initial guess, we get a different root (at least sometimes). >> fzero(@error_func, -2) ans = -1 Alternatively, if you know two values that bracket the root, you can provide both: >> fzero(@error_func, [2,4]) ans = 3 The second argument here is actually a vector that contains two elements. The 7.7 What could go wrong? bracket operator is a convenient way (one of several) to create a new vector. You might be curious to know how many times fzero calls your function, and where. If you modify error_func so that it displays the value of x every time it is called and then run fzero again, you get: >> fzero(@error_func, [2,4]) x = 2 x = 4 x = 2.75000000000000 x = 3.03708133971292 x = 2.99755211623500 x = 2.99997750209270 x = 3.00000000025200 x = 3.00000000000000 x = 3 x = 3 ans = 3 Not surprisingly, it starts by computing f (2) and f (4) to confirm that they in fact bracket a zero. After each iteration, the interval that brackets the root gets smaller; fzero stops when the interval is so small that the estimated zero is correct to 16 digits, 2.2204 × 10−16 to be exact. If you don’t need that much precision, you can tell fzero to give you a quicker, dirtier answer (see the documentation for details about changing the tolerance). 7.7 What could go wrong? The most common problem people have with fzero is leaving out the @. In that case, you get something like: >> fzero(error_func, [2,4]) Error using error_func (line 2) Not enough input arguments. Which is a very confusing error message. The problem is that MATLAB treats the first argument as a function call, so it calls error_func with no arguments. Since error_func requires one argument, the message indicates that the input argument is “undefined,” although it might be clearer to say that you haven’t provided a value for it. Another common problem is writing an error function that never assigns a value to the output variable. In general, functions should always assign a value to the output variable, but MATLAB doesn’t enforce this rule, so it is easy to forget. For example, if you were to re-write Listing 7.1: 209 210 Chapter 7 Zero-finding 1 2 3 function res = error_func(x) y = x.^2 - 2.*x -3 end and then call it from the Command Window : >> error_func(4) y = 5 It looks like it worked, but don’t be fooled. This function assigns a value to y, and it displays the result, but when the function ends, y disappears along with the function’s workspace. If you try to use it with fzero, you get >> fzero(@error_func, [2,4]) y = -3 Error using fzero (line 233) FZERO cannot continue because user-supplied function_handle ==> error_func failed with the error below. Output argument "res" (and maybe others) not assigned during call to "error_func". If you read it carefully, this is a pretty good error message (with the quibble that “output argument” is not a good synonym for “output variable”). You would have seen the same error message when you called error_func from the Command Window , if only you had assigned the result to a variable: >> x = error_func(4) y = 5 Error in error_func (line 2) y = x.^2 - 2.*x -3; Output argument "res" (and maybe others) not assigned during call to "error_func". You can avoid all of this if you remember these two rules: • Functions should always assign values to their output variables. • When you call a function, you should always do something with the result (either assign it to a variable or use it as part of an expression, etc.). 7.8 Finding an initial guess When you write your own functions and use them yourself, it is easy for mistakes to go undetected. But when you use your functions with MATLAB functions like fzero, you have to get it right! Yet another thing that can go wrong: if you provide an interval for the initial guess and it doesn’t actually contain a root, you get >> fzero(@error_func, [0,1]) Error using fzero (line 274) The function values at the interval endpoints must differ in sign. Or it may be that the interval contains a root, it is just not guaranteed per the intermediate value theorem. This is where understanding the basis of fzero is beneficial. There is one other thing that can go wrong when you use fzero, but this one is less likely to be your fault. It is possible that fzero won’t be able to find a root. fzero is generally pretty robust, so you may never have a problem, but you should remember that there is no guarantee that fzero will work, especially if you provide a single value as an initial guess. Even if you provide an interval that brackets a root, things can still go wrong if the error function is discontinuous. Remember the basis of bracketing methods. If your function is continuous and changes signs over an interval, then at least one zero exists. 211 t Well, okay, there are exceptions to the rule that functions should always assign values to their output variables, including find_triples (Listing 5.15). Functions that don’t return a value are sometimes called “commands,” because they do something (like display values or generate a figure) but either don’t have an output variable or don’t make an assignment to it. 7.8 Finding an initial guess The better your initial guess (or interval) is, the more likely it is that fzero will work, and the fewer iterations it will need. When you are solving problems in the real world, you will usually have some intuition about the answer. This intuition is often enough to provide a good initial guess for zero-finding. Another approach is to plot the function and see if you can approximate the zeros visually. If you have a function, like error_func that takes a single input variable and returns a single output variable, you can plot it with fplot: >> fplot(@error_func, [-2,5]) t Note that in previous semesters we would use ezplot. However, the MATLAB documentation now indicates that ezplot is not recommended, to use fplot instead. 212 Chapter 7 Zero-finding 12 10 8 6 4 2 0 -2 -4 -2 -1 0 1 2 3 4 5 Figure 7.2 fplot(@error_func, [-2,5]) The first argument is a function handle; the second is the interval you want to plot the function in. You can modify the plot appearance just as you did before with plot. >> fplot(@error_func, [-2,5],'-ro') 12 10 8 6 4 2 0 -2 -4 -2 -1 0 1 2 3 4 Figure 7.3 fplot(@error_func, [-2,5],’-ro’) 5 7.8 Finding an initial guess 213 As this example shows you, fplot calls your function several times, so you will probably want to make sure your function is silent before you plot. All of the other tricks we used before with plot work here too. For example, you can add a title and axis labels, and you can save and print your figure. In general, when using fplot to obtain estimates of location of zeros, we will not worry too much about making the plot pretty. But fplot is a very convenient function, and you may find it useful for other applications. Last, students often find it helpful to plot a reference y = 0 line to help identify zeros. This can readily be accomplished. Here’s an example where the reference line is plotted as dotted black line: >> hold on >> fplot(@error_func, [-2,5]) >> plot([-2,5],[0,0]) 12 10 8 6 4 2 0 -2 -4 -2 -1 0 1 2 3 4 5 Figure 7.4 fplot(@error_func, [-2,5]) and plot([-2,5],[0,0]) And if that seems like a bit much, you could alternatively just tell MATLAB to turn the plot grid on. That would look something like this: >> fplot(@error_func, [-2,5]) >> grid on 214 Chapter 7 Zero-finding 12 10 8 6 4 2 0 -2 -4 -2 -1 0 1 2 3 4 5 Figure 7.5 fplot(@error_func, [-2,5]) and grid on 7.9 More name collisions Functions and variables occupy the same “name-space,” which means that whenever a name appears in an expression, MATLAB starts by looking for a variable with that name, and if there isn’t one, it looks for a function. As a result, if you have a variable with the same name as a function, the variable shadows the function. For example, if you assign a value to sin, and then try to use the sin function, you might get an error: >> sin = 3; >> x = 5; >> sin(x) Index exceeds matrix dimensions. In this example, the problem is clear. Since the value of sin is a scalar, and a scalar is really a 1×1 matrix, MATLAB tries to access the 5th element of the matrix and finds that there isn’t one. Of course, if there were more distance between the assignment and the “function call,” this message would be pretty confusing. But the only thing worse than getting an error message is not getting an error message. If the value of sin was a vector, or if the value of x was smaller, you would really be in trouble. 7.9 More name collisions >> sin = 3; >> sin(1) ans = 3 Just to review, the sine of 1 is not 3! The converse error can also happen if you try to access an undefined variable that also happens to be the name of a function. For example, if you have a function named f, and then try to increment a variable named f (and if you forget to initialize f), you get >> f = f+1 Error: "f" previously appeared to be used as a function or command, conflicting with its use here as the name of a variable. A possible cause of this error is that you forgot to initialize the variable, or you have initialized it implicitly using load or eval. At least, that’s what you get if you are lucky. If this happens inside a function, MATLAB tries to call f as a function, and you get this Error using f (line 2) Not enough input arguments. There is no universal way to avoid these kind of collisions, but you can improve your chances by choosing variable names that don’t shadow existing functions, and by choosing function names that you are unlikely to use as variables. That’s why in Section 7.3 I called the error function error_func rather than f. I often give functions names that end in func, so that helps too. This warning came up previously when discussing variable names. The MATLAB documentation page “Variable Names” recommends the use of the exist function if you are unsure. exist returns 0 if there are no existing variables, functions, or other artifacts with the proposed name. For example: >> exist test_func ans = 0 or equivalently >> exist('test_func') ans = 0 215 216 Chapter 7 Zero-finding 7.10 Debugging in four acts When you are debugging a program, and especially if you are working on a hard bug, there are four things to try: reading: Examine your code, read it back to yourself, and check that it means what you meant to say. running: Experiment by making changes and running different versions. Often if you display the right thing at the right place in the program, the problem becomes obvious, but sometimes you have to spend some time to build scaffolding. ruminating: Take some time to think! What kind of error is it: syntax, run-time, logical? What information can you get from the error messages, or from the output of the program? What kind of error could cause the problem you’re seeing? What did you change last, before the problem appeared? retreating: At some point, the best thing to do is back off, undoing recent changes, until you get back to a program that works, and that you understand. Then you can starting rebuilding. Beginning programmers sometimes get stuck on one of these activities and forget the others. Each activity comes with its own failure mode. For example, reading your code might help if the problem is a typographical error, but not if the problem is a conceptual misunderstanding. If you don’t understand what your program does, you can read it 100 times and never see the error, because the error is in your head. Running experiments can help, especially if you run small, simple tests. But if you run experiments without thinking or reading your code, you might fall into a pattern I call “random walk programming,” which is the process of making random changes until the program does the right thing. Needless to say, random walk programming can take a long time. The way out is to take more time to think. Debugging is like an experimental science. You should have at least one hypothesis about what the problem is. If there are two or more possibilities, try to think of a test that would eliminate one of them. Taking a break sometimes helps with the thinking. So does talking. If you explain the problem to someone else (or even yourself), you will sometimes find the answer before you finish asking the question. But even the best debugging techniques will fail if there are too many errors, or if the code you are trying to fix is too big and complicated. Sometimes the best option is to retreat, simplifying the program until you get to something that works, and then rebuild. Beginning programmers are often reluctant to retreat, because they can’t stand to delete a line of code (even if it’s wrong). If it makes you feel better, copy 7.11 Examples 217 your program into another file before you start stripping it down. Then you can paste the pieces back in a little bit at a time. To summarize, here’s the Ninth Theorem of debugging: Finding a hard bug requires reading, running, ruminating, and sometimes retreating. If you get stuck on one of these activities, try the others. 7.11 Examples Example 7.1 (a) Write a function called cheby6 that evaluates the 6th Chebyshev polynomial. It should take an input variable, x, and return 32x 6 − 48x 4 + 18x 2 − 1 (7.1) (b) Use fplot to display a graph of this function in the interval from 0 to 1. Estimate the location of any zeros in this range. (c) Use fzero to find as many different roots as you can. Does fzero always find the root that is closest to the initial guess? Solution: (Link to screen cast and accompanying M-file.) Let’s begin by creating the M-file cheby6. Listing 7.2 cheby6.m 1 2 3 4 5 % res = cheby6(x) % Function to evaluate the 6th Chebyshev polynomial function res = cheby6( x ) res = 32.*x.^(6) -48.*x.^(4) + 18.*x.^(2) -1; end To get an estimate of the locations of the roots over the interval 0 to 1, let’s use fplot. >> >> >> >> hold on fplot(@cheby6,[0,1]) title('cheby6') plot([0,1],[0,0],':k') 218 Chapter 7 Zero-finding cheby6 1 0.8 0.6 0.4 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 0 0.2 0.4 0.6 0.8 1 Figure 7.6 fplot(@cheby6,[0,1]) We appear to have roots near 0.2, 0.7, and 0.9. Let’s use fzero with these initial guesses to find them. >> zero_1 = fzero(@cheby6,0.2) zero_1 = 0.2588 >> zero_2 = fzero(@cheby6,0.7) zero_2 = 0.7071 >> zero_3 = fzero(@cheby6,0.9) zero_3 = 0.9659 Lastly, we are asked if fzero always returns the root closest to the initial guess. The key is to be careful. Here’s an example: >> fzero(@cheby6,0.85) ans = 0.9659 >> fzero(@cheby6,0.84) ans = 0.7071 >> fzero(@cheby6,0.49) ans = 0.7071 >> fzero(@cheby6,0.48) ans = 0.2588 A small change in the guess causes you to find a different root. Remember that 7.11 Examples 219 MATLAB uses the guess to find brackets of the root. This is one of the main reason I said that specifying a bracket is preferred, while specifying an initial condition is typically more convenient. When we have multiple roots, if I specify a bracket around the root of interest, I will be certain to obtain that root. When I specify an initial guess, it is not clear to me how MATLAB searches for a bracket. Example 7.2 The density of a duck, ρ d , is 0.3 g/cm3 (0.3 times the density of water, ρ w = 1 g/cm3 ). The volume of a sphere with radius r is 43 πr 3 . If a sphere with radius r is submerged in water to a depth d , the volume (V ) of the sphere below the water line is V= π (3r d 2 − d 3 ) as long as d < 2r 3 An object floats at the level where the weight of the displaced water equals the total weight of the object. Assuming that a duck is a sphere with radius 10 cm, at what depth does a duck float? Here are some suggestions about how to proceed: • Write an equation relating ρ, d and r . That is, perform a force balance. • Rearrange the equation so the right-hand side is zero. Our goal is to find values of d that are roots of this equation. • Write a MATLAB function that evaluates this function. Test it, then make it a silent function. • Use fplot to make a guess about the brackets or to make a guess about the value of d 0 to use as a starting place. With physical systems you can often come up with a bracket or initial guess without needing fplot. This is a perfect example. • Use fzero to find the root. • Check to make sure the result makes sense. In particular, check that d < 2r , because otherwise the volume equation doesn’t work! The sphere would be submerged in water. Solution: Let’s begin by writing an expression for the weight of the duck (Wd ) and the weight of the displaced water (Ww ) 4 4 Wd = ρ d g V = ρ d g πr 3 = 0.3ρ w g πr 3 3 3 Ww = ρ w g V = ρ w g π (3r d 2 − d 3 ) 3 Our criteria for buoyancy is Wd = Ww , giving (7.2) (7.3) 220 Chapter 7 Zero-finding 4 π 0.3ρ w g πr 3 = ρ w g (3r d 2 − d 3 ) 3 3 (7.4) (0.3)(4)r 3 = (3r d 2 − d 3 ) (7.5) which simplifies to Putting this expression in a form suitable for fzero (0.3)(4)r 3 − (3r d 2 − d 3 ) = 0 (7.6) We know that r = 10 cm, so that the only unknown is d , which you can think of as our x in this case. (The units of d will agree with r .) Next, let’s create a function called duck that we will pass to fzero. Listing 7.3 duck.m 1 2 3 4 5 6 % res = duck(x) % Function to find the required depth of a duck to remain % buoyant in water function res = duck( x ) res = 0.3.*4.*10.^(3)-(3.*10.*x.^(2)-x.^(3)); end To get an estimate of the locations of the roots over the interval 0 to 2r = 20 cm, let’s use fplot. >> >> >> >> hold on fplot(@duck,[0,20]) title('duck') plot([0,20],[0,0],':k') 7.12 Functions: what we’ve done so far 221 duck 1500 1000 500 0 -500 -1000 -1500 -2000 -2500 -3000 0 5 10 15 20 Figure 7.7 fplot(@duck,[0,20]) It looks like we only have one solution, which is near d = 8 cm. Let’s use this as our initial guess and have fzero find the root. >> d = fzero(@duck,8) d = 7.2651 Nice! So for our spherical duck with a radius of 10 cm (or diameter of 20 cm), 7.2651 cm is below the surface of the pond. This satisfies the constraint that d < 2r . For a buoyancy problem, we expect there only to be one unique solution. So for problems of this type, you might instead specify the range of all possible values. >> d = fzero(@duck,[0,20]) d = 7.2651 As a final note, in my presented solution I simplified the force balance expression in an attempt to make the resulting error function as simple as possible. However, know that this is not necessary. In fact, I often do not simplify expressions so as not to introduce the possibility of errors. Before moving on, if interested, have a look at the MATLAB documentation page “Roots of Scalar Functions.” 7.12 Functions: what we’ve done so far So far we have created function M-files using the Editor. In Chapter 5 when we first introduced the use of functions, their use was motivated by the fact that 222 Chapter 7 Zero-finding each function has its own workspace, and that inputs and outputs are defined carefully to avoid unexpected interactions. Additionally, for large calculations we have seen that they allow us to modulate our code, making our code both more readable and easier to debug. We have seen so far that fzero and fplot both expect a function with just a single input variable. But what if we wish to pass an extra parameter? This can be accomplished using an anonymous function, which we will discuss here. Additionally, the very first section of this text, Section 1.1, is titled “A glorified calculator”. There are many times where you will want to obtain an answer to a problem quickly. Using MATLAB as a calculator in these cases is desirable. In this chapter we will additionally show how fzero and fplot can be used in calculator mode with the help of anonymous functions. I do, however, recommend writing script and function M-files whenever possible. This will allow you to document and save your work. t There are two additional techniques that can be used 7.13 Anonymous function to pass extra parameters. One is the use of nested functions. We will use An anonymous function is a function that is not stored in a file, but is instead nested functions later in stored as a variable (a function_handle). An anonymous function behaves just the course, and I would like a function stored in a file, except it can only contain a single executable suggest you avoid them for statement. now as we are just getting Imagine that you wish to evaluate the cube root of a number. You know this started. With nested funcis a calculation you will need to perform often, so you decide to write a function tions the workspace rules M-file. This leads to change slightly, which is why I suggest to wait. The other is to use global variListing 7.4 cubert.m ables. However, as MATLAB notes, global variables carry 1 function res = cubert(X) noticeable risks, and should 2 res = X.^(1/3); be used sparingly, if at all. 3 end As a note, GNU Octave does not support nested functions, but does support Then from the Command Window anonymous functions and global variables. >> cubert(9) ans = 2.0801 >> cubert(27) ans = 3 >> Y = [9, 24, 56, 87]; >> cubert(Y) ans = 2.0801 2.8845 3.8259 4.4310 7.13 Anonymous function 223 Beautiful! Now imagine that this is a calculation you will need to perform often, but just within this session, and maybe you are not using your own computer so you do not wish to (or can not) save a file to the computer. Or you are in the middle of an exam and do not have the time to write a function file! We can instead create an anonymous function in the Command Window . Repeating the same calculations: >> crt = @(X) X.^(1/3); >> crt(9) ans = 2.0801 >> crt(27) ans = 3 >> Y = [9, 24, 56, 87]; >> crt(Y) ans = 2.0801 2.8845 3.8259 4.4310 Notice that in the assignment statement for crt, the first term on the right is a handle, @; the variable therefore corresponds to a function handle. The parentheses immediately following the handle indicate the input variable(s). Just as with function files, anonymous functions can have more than a single input variable. To compute the length of the hypotenuse of a triangle: >> hyp = @(A,B) sqrt(A.^(2)+B.^(2)); >> hyp(3,4) ans = 5 The answer can also be stored to a new variable rather than the default ans which will be overwritten the next calculation: >> hyp = @(A,B) sqrt(A.^(2)+B.^(2)); >> sol = hyp(3,4) sol = 5 Another very useful use of anonymous functions is to quickly plot a function over a range using fplot. Imagine you wish to plot the function y = a sin(bx) + c, t Remember a script file is nothing more than a document containing a series of commands you would otherwise execute in the Command Window . When we run a script, we execute commands line-by-line as if we had entered them in the Command Window . One could therefore create a script that contains anonymous functions to save your work. Anonymous functions could also be used in function files. 224 Chapter 7 Zero-finding where a = 4, b = 2 and c = 1, over the range 0 to 2π. Let’s start using what we have done so far and create a function M-file. Listing 7.5 sin_fun.m 1 2 3 4 5 6 function res = sin_fun(X) a = 4; b = 2; c = 1; res = a.*sin(b.*X)+c; end Then >> fplot(@sin_fun,[0,2*pi]) We could equivalently do this from the Command Window with an anonymous function. I will declare parameters a, b, and c so that we can latter show issues that might arise with parameters. >> a = 4; b = 2; c = 1; >> sfun = @(X) a.*sin(b.*X)+c; >> fplot(sfun,[0,2*pi]) 5 4 3 2 1 0 -1 -2 -3 0 1 2 3 4 Figure 7.8 fplot(sfun,[0,2*pi]) 5 6 7.14 What could go wrong? 225 Notice that when using fplot to plot our anonymous function, we do not need @ in the function call. This is because the anonymous function, here sfun, is already of type function handle. Another use of anonymous functions is to quickly find the zeroes of a function. For example, find where y = 4 sin(2x) + 1 is equal to zero over the range 0 to 2. I will confirm the solution by also using our function file. >> sol = fzero(sfun,[0,2]) sol = 1.6971 >> sol = fzero(@sin_fun,[0,2]) sol = 1.6971 7.14 What could go wrong? The main issue to look out for is when using anonymous functions with parameters. For instance, we were just looking at the function y = a sin(bx) + c, where a = 4, b = 2 and c = 1. Let’s start by evaluating the function at x = 2. Throughout this section will will evaluate the function using both our function file and anonymous function for comparison. >> sin_fun(2) ans = -2.0272 >> sfun(2) ans = -2.0272 A perfect match! Now what if instead we wanted to change the parameter c from 1 to 2. For the case of our function file, I will create a copy and save as sin_fun_2, where I change the value of c. Listing 7.6 sin_fun_2.m 1 2 3 4 5 6 function res = sin_fun_2(X) a = 4; b = 2; c = 2; res = a.*sin(b.*X)+c; end 226 Chapter 7 Zero-finding And evaluating >> sin_fun_2(2) ans = -1.0272 The resulting value increases by 1 as expected. How do we update our anonymous function? It first might be tempting to simply update the value of c in the Command Window and then re-evaluate the function: >> c = 2; >> sfun(2) ans = -2.0272 But this is wrong, very wrong. Remember, whenever we assign a number to a variable, whenever MATLAB comes across the variable name, it substitutes in its value. Therefore, when we originally defined our anonymous function, it was defined using the original values of a, b, and c. After its creation it has no recollection of a, b, and c; it just used their values. Therefore, when we update the value of c, we need to re-define the function. Let’s try again: >> c = 2; >> sfun = = @(X) a.*sin(b.*X)+c; >> sfun(2) ans = -1.0272 Now that is better. 7.15 Saving anonymous functions t What is meant by another or new session? Well a new session would be if you closed MATLAB and then re-opened it later. As already mentioned, it is always a good idea to write script to document your work. Remember a script M-file is a file that contains human-readable MATLAB code. You could alternatively save your anonymous function (or your entire workspace) as a MAT-file for use in another session. A MAT-file is not human-readable. It is a file that contains MATLAB formatted data. Data can be written to or loaded from a MAT-file using the save and load command respectfully. To save our anonymous function sfun to a file sfun.mat, the command would be save sfun.mat sfun. The file name does not need to be the same as the anonymous function, but I do so here because it is the function of interest. Then to load the anonymous function in a new session, with the MAT-file in your path, the command would be load(’sfun.mat’). I will demonstrate the commands in action below by 7.16 Revisiting the duck 227 simulating a new session by clearing all variables after saving the anonymous function. At each step I will evaluate the function at x = 2. >> sfun(2) ans = -1.0272 >> save sfun.mat sfun >> clear variables >> sfun(2) Undefined function or variable 'sfun'. >> load('sfun.mat') >> sfun(2) ans = -1.0272 This is NOT generally something I find myself using, but I thought I would mention it for completeness. I typically prefer to write scripts and save my work in human-readable form. 7.16 Revisiting the duck As a final example, let’s revisit Example 7.2, our duck buoyancy problem. In the problem we were solving for the depth (d ) to which a duck of radius r was submerged in water of density ρ using fzero. fzero expects a function to which we pass only a single variable. We therefore specified r and ρ in the function to solve. Here, let’s generalize the function to allow us to pass r and ρ too, and see how an anonymous function can be used to pass these extra parameters. This would be useful, for example, if you wished to look at the effect of these parameters. The alternative would be to edit and re-save the original error function M-file each time you wished to make a change. Let’s start by working out a new expression that contains ρ (for an arbitrary liquid) and r as parameters. Rather than writing ρ d as 0.3ρ w , let’s just use 0.3 since the density of the liquid will change. We will just need to be sure we use correct units throughout. Density in g/cm3 and length in cm. Beginning with eq. (7.4): 4 π 0.3g πr 3 = ρg (3r d 2 − d 3 ) 3 3 (7.7) (0.3)(4)r 3 = ρ(3r d 2 − d 3 ) (7.8) Simplifying Putting in a form for fzero 228 Chapter 7 Zero-finding (0.3)(4)r 3 − ρ(3r d 2 − d 3 ) = 0 (7.9) Now lets create a new function duck2 to which we will also pass parameters r and ρ. Listing 7.7 duck2.m 1 2 3 4 5 6 7 8 % res = duck2(x,r,rho) % Function to find the required depth of a duck to remain % buoyant in water. duck2 requires that we pass parameters % r (radius of duck) and rho (density of the liquid), in % addition to the depth (x) which is what we will solve for. function res = duck2( x,r,rho ) res = 0.3.*4.*r.^(3)-rho.*(3.*r.*x.^(2)-x.^(3)); end Next, let’s verify that we get the same solution as before with duck >> d0 = 7; >> dref = fzero(@duck,d0) dref = 7.2651 >> r = 10; >> rho = 1; >> duck_zero = @(X) duck2(X,r,rho); >> d = fzero(duck_zero,d0) d = 7.2651 A perfect match! If you wanted to look at different values of r and ρ, you would update their values, re-define the anonymous function, and then re-solve using fzero. Cool! After seeing this example, think of how you might be able to use anonymous functions to generalize the Antoine equation functions you have already written? We could write a function to perform an Antoine equation calculation for the general case in which we pass the temperature in addition to parameters A, B , and C . This is great in that the function can be used for any fluid. But it is annoying in that if for a given problem I am only interested in a single fluid, say ethanol, I need to pass the same values of A, B , and C every call. I could instead use an anonymous function to pass A, B , and C , to create an ethanol-specific function for the problem at hand. This is a good practice because if I know my general code is bug-free, I can rest assured my anonymous functions is bug-free too, and it can streamline the calculations. 7.17 Numerical methods: zero-finding 229 7.17 Numerical methods: zero-finding 7.17.1 The bisection method The function fzero is great, and in general, I would recommend using MATLAB’s built-in (intrinsic) functions whenever possible. You can be confident they are bug free and efficient. Nonetheless, it is always valuable to know how the functions (here fzero) work. This will help you understand errors and what may go wrong, and how to tweak the options (when available) for a particular problem. Or at the very least, it will help you appreciate the hard work that went in to developing MATLAB. With that, let’s discuss the bisection method. It is a numerical method that falls in the category of bracketing methods for zero-finding. To keep matters simple for now, think of the the bisection method in the context of Example 7.2, our duck buoyancy problem. This will serve as a good starting point since there is only one zero (or root or solution to our problem). In general, our goal is to find the value of x = x ans for which f (x ans ) = 0. To one side of x ans , the function will be positive, on the other side it will be negative, and somewhere in between it is zero; the basis of bracketing methods is the intermediate value theorem, which you should have encountered in your first Calculus course. If x l is the value of x to the left and x r is the value of x to the right of the solution, then we have f (x r ) f (x l ) < 0. This is the premise of the bisection method. 1. Choose a value of x l and x r (the range of the search) such that the function changes sign over the interval. Put differently, x l and x r should have different signs such that f (x r ) f (x l ) < 0. 2. Estimate the root as the mid-point between x l and x r : x ans = 1 (x l + x r ) 2 3. Evaluate f (x ans ). 4. Next, we need to decide what to do: (a) If f (x r ) f (x ans ) < 0, then the root lies in this new sub-interval. Set x l = x ans and return to Step 2. (b) Else if f (x l ) f (x ans ) < 0, then the root must lie in this new sub-interval. Set x r = x ans and return to Step 2.(Note, you could just as well check if f (x r ) f (x ans ) > 0.) (c) Else if f (x ans ) = 0, we have found our answer, terminate the calculation. (Note, since anything times anything is 0, you could just as well check it f (x r ) f (x ans ) = 0.) t Remember, in Example 3.6 we wrote a script to compute sin(x) using a Taylor series expansion. This is what MATLAB does, and gave us an appreciation of the numerical error in the calculation. 230 Chapter 7 Zero-finding t Another common fzero error is the failure to provide correct brackets to the zero. Likewise, if you provide a guess of location of the zero that is very bad, MATLAB will be unable to find brackets. Eventually, the interval becomes so small that we converge on our answer. So long as x l and x r bracket a zero, and the function is continuous over the range, it will find a solution. Before we go ahead and write a MATLAB function to perform the bisection method, let’s think about the settings or options that need to be specified to get this to work. First, we need to decide how close we need be to zero to stop. We typically call this the tolerance. Remember, in MATLAB sin(pi)=1.2246e-16, and not exactly 0. Also note that since we are looking for a value close to zero, you will likely encounter values that are positive and negative. Therefore, do not simply check that the value is < tolerance; all negative values will satisfy this. Rather, use absolute values or the square of the value. (A negative times a negative is positive.) As the tolerance decreases, the number of iterations required for convergence increases, which brings us to our second setting. Second, we need to specify how many iterations to perform before giving up. Perhaps the specified tolerance was much too small such that your code does not converge in a reasonable amount of time. This is also useful in the early stages of code developing where a bug might cause your program to otherwise run forever. Third, we saw that with fzero we can specify either an initial guess or a range. How do you use the bisection method if only an initial guess is provided? Well, we assume the guess is close to the root, so we can use this to guess an initial value of x l and x r . If you wanted to provide this functionality, you would need to decide how to estimate this range. Enough talk, let’s do it! 7.17 Numerical methods: zero-finding Listing 7.8 bisection_zero.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 % res = bisection_zero(fcn_handle,Bracket,maxit,tol) % Function to perform bisection method to find the root % of a function (evaluated/provided in fcn_handle) % in the range xl to xr. maxit sets the maximum number % of iterations and tol sets the tolerance for convergence. % Bracket is a vector where the first element is xl and the % second element is xr. function res = bisection_zero(fcn_handle,Bracket,maxit,tol) %hold on % Start by un-packing Bracket into x1 and xr xl = Bracket(1); xr = Bracket(2); % Check to make sure f(xl) and f(xr) have opposite signs. % If not, don't do anything. if fcn_handle(xl)*fcn_handle(xr) > 0 %f(xl) and f(xr) have the same sign disp('f(xl) and f(xr) have the same sign. Exit.') % else, f(xl) and f(xr) have the opposite sign,proceed else for i=1:maxit % Estimate location of zero as midpoint between % xl and xr xans = 0.5*(xl+xr); fxans = fcn_handle(xans); fxl = fcn_handle(xl); fxr = fcn_handle(xr); % Check if we found our root if fxans^(2) < tol^(2) res = xans; %i break % Otherwise, update our brackets elseif fxl*fxans < 0 xr = xans; elseif fxr*fxans < 0 xl = xans; else % We should never get here, but just in case disp('Critical error. Exit') disp('Current estimate:') xans fxans break 231 232 Chapter 7 Zero-finding 44 45 46 47 48 49 50 51 52 53 54 55 56 end end end end % Check to see if we reached the maximum number of % iterations. If we did, print the current estimates. if i == maxit disp('Reached maximum number of iterations. Exit') disp('Current estimate:') xans fxans end %plot(i,xans,'ro') Notice the variable fcn_handle and its use in the function. Just as we use a function handle to tell fzero which function to find the roots of, we can use a function handle here too. Now let’s see it in action. >> xl = 0; >> xr = 20; >> tolerance = 1e-12; >> maxiter = 1000; >> bisection_zero(@duck,[xl,xr],maxiter,tolerance) ans = 7.2651 This is exactly what we had before. If you uncomment lines 9 and 53, you can generate a plot of the estimate of the root location versus the iteration number, which is shown below. 7.17 Numerical methods: zero-finding 233 10 9.5 9 8.5 8 7.5 7 6.5 6 5.5 5 0 10 20 30 40 50 Figure 7.9 The current estimate of the root location versus the iteration number for our duck buoyancy problem using bisection_zero. Let’s walk through what MATLAB is doing step-by-step. Let’s start by revisiting fig. 7.1, which is for our duck problem, which is shown again in the margin as fig. 7.10 for convenience. We begin with x l = 0 and x r = 20, which bracket our zero, with the zero we are searching for located where the red lines cross. Using bisection method, our guess of the zero location each iteration is the midpoint between x l and x r . Our first guess then is x ans = 10. Figure 7.10 Our initial estimate of x l and x r that bracket our zero. 234 Chapter 7 Zero-finding Figure 7.11 Our first estimate of x ans . We find that x l and our current estimate of x ans bracket our solution. So we replace x r with x ans and repeat. This interval 0 to 10 then gets cut in half, and our second guess of the zero location is x ans = 5. Figure 7.12 Our second estimate of x ans . We find now that our current estimate of x ans and x r bracket the solution. So we replace x l with x ans and repeat. (At this point it becomes difficult for me to sketch clear figures... but I hope you get the idea.) 7.17 Numerical methods: zero-finding The interval 5 to 10 then gets cut in half, and our third guess of the zero location is 7.5. At this point the range over which we are searching is 2.5, 8 times smaller than the original range of 20. The refinement continues, and we see that by the 10th iteration we appear to be very close to our final answer. Next, let’s see what happens if we provide a value of x r < 7.2651 and then a small value for the maximum number of iterations. >> bisection_zero(@duck,[xl,5],maxiter,tolerance) f(xl) and f(xr) have the same sign. Exit. >> bisection_zero(@duck,[xl,xr],4,tolerance) Reached maximum number of iterations. Exit Current estimate: xans = 6.2500 fxans = 272.2656 The last point I would like to make is if I uncomment line 30, I can see how many iterations it takes to find the root. We are using format short, so our answer only contains four decimal places. If I set the tolerance to 1×10−4 , I get our answer after 25 iterations. As the tolerance becomes smaller, the number of interactions increases. For a tolerance of 1 × 10−12 , we require 51 iterations. While this is not a problem here, it is something to keep in mind if you have a more complicated problem. Last, before moving on, it is useful to demonstrate that we can more properly extract additional information by returning additional/optional variables. I will save a copy of the function and demonstrate how we can return the number of iterations and the value of the error function at the solution. 235 236 Chapter 7 Zero-finding Listing 7.9 bisection_zero_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 % res = bisection_zero_2(fcn_handle,Bracket,maxit,tol) % Function to perform bisection method to find the root % of a function (evaluated/provided in fcn_handle) % in the range xl to xr. maxit sets the maximum number % of iterations and tol sets the tolerance for convergence. % Bracket is a vector where the first element is xl and the % second element is xr. function [res,i,fxans] = bisection_zero_2(fcn_handle,Bracket,maxit,tol) %hold on % Start by un-packing Bracket into x1 and xr xl = Bracket(1); xr = Bracket(2); % Check to make sure f(xl) and f(xr) have opposite signs. % If not, don't do anything. if fcn_handle(xl)*fcn_handle(xr) > 0 %f(xl) and f(xr) have the same sign disp('f(xl) and f(xr) have the same sign. Exit.') % else, f(xl) and f(xr) have the opposite sign,proceed else for i=1:maxit % Estimate location of zero as midpoint between % xl and xr xans = 0.5*(xl+xr); fxans = fcn_handle(xans); fxl = fcn_handle(xl); fxr = fcn_handle(xr); % Check if we found our root if fxans^(2) < tol^(2) res = xans; %i break % Otherwise, update our brackets elseif fxl*fxans < 0 xr = xans; elseif fxr*fxans < 0 xl = xans; else % We should never get here, but just in case disp('Critical error. Exit') disp('Current estimate:') xans fxans break 7.17 Numerical methods: zero-finding 44 45 46 47 48 49 50 51 52 53 54 55 56 end end end end % Check to see if we reached the maximum number of % iterations. If we did, print the current estimates. if i == maxit disp('Reached maximum number of iterations. Exit') disp('Current estimate:') xans fxans end %plot(i,xans,'ro') >> xl = 0; >> xr = 20; >> tolerance = 1e-12; >> maxiter = 1000; >> d = bisection_zero(@duck,[xl,xr],maxiter,tolerance) d = 7.2651 >> d = bisection_zero_2(@duck,[xl,xr],maxiter,tolerance) d = 7.2651 >> [d,niter,fans] = bisection_zero_2(@duck,[xl,xr],maxiter,tolerance) d = 7.2651 niter = 51 fans = -4.5475e-13 7.17.2 The secant (or linear interpolation) method More efficient techniques exist. One such technique that is very similar to the bisection method is the secant (or linear interpolation) method. All that differs is Step 2, guessing a value of x ans . With the bisection method, we guess a value of x ans as the mid-point between x l and x r . Therefore, each iteration the search interval is halved. To guess a value of x ans with the linear interpolation method, we connect f (x l ) and f (x r ) with a straight line and find where the line equals 0 (or crosses the x-axis). We use this value as our guess of x ans . A schematic of how we would find our first guess of x ans for our duck buoyancy problem using linear interpolation is provided in fig. 7.13. We see that this clearly differs from the midpoint used by the bisection method. 237 238 Chapter 7 Zero-finding Figure 7.13 Finding our first guess of x ans for our duck buoyancy problem using linear interpolation. If the function is in fact linear over the region x l to x r , we obtain the root of the function. Eventually the interval becomes so small that the linear approximation is very good. Just like the bisection method, convergence is guaranteed so long as your initial estimates of x l and x r bracket a zero and your function is continuous over the range. The hope is that with an improved estimation technique, the number of iterations required for convergence decreases. If you need help connecting f (x l ) and f (x r ) with a straight line, have a look at the problem statement for Example 1.7 on page 19. Let’s work out the expression we will need to generate this new estimate. Assuming f (x l ), g (x ans ) and f (x r ) are co-linear, where here I use g (x ans ) to designate the straight line connecting f (x l ) to f (x r ) evaluated at x ans : f (x l ) − g (x ans ) g (x ans ) − f (x r ) = x l − x ans x ans − x r At our new estimate of the root x ans , g (x ans ) = 0 so that: f (x l ) − f (x r ) = x l − x ans x ans − x r Solving for x ans we find: x ans = x r f (x l ) − x l f (x l ) f (x l ) − f (x r ) Updating our bisection_zero_2 function for the linear interpolation method we have: 7.17 Numerical methods: zero-finding Listing 7.10 linear_interpol_zerp.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 % res = linear_interpol_zero(fcn_handle,xl,xr,maxit,tol) % Function to perform linear interpolation method to find the root % of a function (evaluated/provided in fcn_handle) % in the range xl to xr. maxit sets the maximum number % of iterations and tol sets the tolerance for convergence. % Bracket is a vector where the first element is xl and the % second element is xr. function [res,i,fxans] = linear_interpol_zero(fcn_handle,Bracket,maxit,tol) %hold on % Start by un-packing Bracket into x1 and xr xl = Bracket(1); xr = Bracket(2); % Check to make sure f(xl) and f(xr) have opposite signs. % If not, don't do anything. if fcn_handle(xl)*fcn_handle(xr) > 0 %f(xl) and f(xr) have the same sign disp('f(xl) and f(xr) have the same sign. Exit.') % else, f(xl) and f(xr) have the opposite sign,proceed else for i=1:maxit fxl = fcn_handle(xl); fxr = fcn_handle(xr); % Find our new guess of the root location xans = (xr*fxl-xl*fxr)/(fxl-fxr); % Evaluating our funciton at this new estimate of the root fxans = fcn_handle(xans); % Check if we found our root if fxans^(2) < tol^(2) res = xans; %i break elseif fxl*fxans < 0 xr = xans; elseif fxr*fxans < 0 xl = xans; else disp('Critical error. Exit') break end if i == maxit disp('Reached maximum number of iterations') xans fxans 239 240 Chapter 7 Zero-finding 44 45 46 47 48 end end end end %plot(i,xans,'ro') >> xl = 0; >> xr = 20; >> tolerance = 1e-12; >> maxiter = 1000; >> [d,niter,fans] = bisection_zero_2(@duck,[xl,xr],maxiter,tolerance) d = 7.2651 niter = 51 fans = -4.5475e-13 >> [d,niter,fans] = linear_interpol_zero(@duck,[xl,xr],maxiter,tolerance) d = 7.2651 niter = 9 fans = 0 And with just this small improvement, the number of iterations decreases from 51 to 9 while obtaining the same solution. 7.18 roots The function fzero is very robust and can be used to find the roots of any linear or non-linear equation. For example, t Notice how I used a function handle to refer to MATLAB’s built-in function sin. >> fzero(@sin,[1,4]) ans = 3.1416 π, just as we expected. Most often, we tend to encounter polynomials. For example, cubic equations of state, routinely used to predict the phase behavior of fluid systems, are cubic equations. It would be great if MATLAB had a function to find all of the roots of an equation. Otherwise, I would need to use fplot to estimate the range of each root, and then apply fzero for each root. This was exactly the case in Example 7.1. To find all of the roots of a polynomial, we can use MATLAB’s built-in function roots. I stress, this is only for polynomials. Also, notice that I did not mention that we would find all of the roots over a particular range. MATLAB will return all of the roots; it does not allow you to specify a range. You will therefore have to manually check which of the roots are within the desired range. With roots, MATLAB does not use a bracketing technique. Instead, it uses algebraic tricks, 7.18 roots 241 which are beyond the scope of this class. If you are interested, have a look at the freely available material from the textbook “Numerical Methods with Applications” at the University of South Florida, specifically the material on the “Solution of Quadratic Equations” followed by the “Solution of Cubic Equations.” And if you are really interested, have a look at “Tea Time Numerical Analysis” in the Open Texbook Library. Imagine we have a polynomial of the form C (1)x N +C (2)x N −1 + ... +C (N )x + C (N + 1) = 0. You need to create a vector C with all of the coefficients. Then to find the roots, use roots(C). Let’s apply it to the 6th Chebyshev polynomial in Example 7.1. It is a 6th order polynomial, so we can have up to 6 roots. To begin, write your function just as you would for fzero. For this case, we have 32x 6 − 48x 4 + 18x 2 − 1 We could equivalently write this as 32x 6 + 0x 5 − 48x 4 + 0x 3 + 18x 2 + 0x − 1 (7.10) Next, we need to create a vector. We create a (row) vector by separating the vector elements by commas, and enclose this list with brackets. So here we would have the following coefficient vector: >> C = [32, 0, -48, 0, 18, 0, -1]; Finding the roots >> Sol = roots(C) Sol = -0.9659 -0.7071 0.9659 0.7071 -0.2588 0.2588 Within the range 0 to 1, we have 0.9659, 0.7071, and 0.2588. This agrees perfectly with what we found using fzero. Formally, while C is a row vector (1 × 7), Sol is a column vector (6×1). So to select an element from Sol, we would use a command like >> zero_1 = Sol(3) zero_1 = 0.9659 or equivalently t For now, don’t worry about the difference between a row and column vector. Both are vectors, so you can select an element using a single index. We will revisit row and column vectors later. 242 Chapter 7 Zero-finding >> zero_1 = Sol(3,1) zero_1 = 0.9659 If interested, have a look at the MATLAB documentation page “Roots of Polynomials.” 7.19 Examples Example 7.3 Use fzero to determine the first nontrivial root of sin(x) = x 2 , where x is in radians. (By nontrivial, I mean x > 0.) Solution: (Link to screen cast and accompanying M-file.) We start by re-writing the equation such that it is equal to 0, as is needed for fzero. sin(x) − x 2 = 0 Now creating our function file: Listing 7.11 sin_error_func.m 1 function res = sin_error_func(x) 2 res = sin(x)-x.^(2); 3 end Before using fzero, let’s use fplot to estimate the location of the desired root. We know that sin(x) is bound between 0 and 1. Therefore, the desired root must be over the range 0 ≤ x ≤ 1. Use this as our bounds for the plot: >> hold on >> fplot(@sin_error_func,[0,1]) >> plot([0,1],[0,0],':k') 7.19 Examples 243 0.25 0.2 0.15 0.1 0.05 0 -0.05 -0.1 -0.15 -0.2 0 0.2 0.4 0.6 0.8 1 Figure 7.14 fplot(@sin_error_func,[0,1]) The non-trivial root (x 6= 0) appears to be between 0.8 and 1.0. Using fzero over this range: >> fzero(@sin_error_func,[0.8,1]) ans = 0.8767 Example 7.4 Use fzero to determine the real root of ln x 2 = 7. Hint: start by using fplot to look at your function over the range 0 to 40. Solution: (Link to screen cast and accompanying M-file.) We start by re-writing the equation such that it is equal to 0, as is needed for fzero. ln x 2 − 7 = 0 Now creating our function file: Listing 7.12 log_error_func.m 1 function res = log_error_func(x) 2 res = log(x.^(2))-7; 3 end 244 Chapter 7 Zero-finding Before using fzero, let’s use fplot to estimate the location of the desired root. It is suggested that we look over the range 0 to 40. >> fplot(@log_error_func,[0,40]) 0 -2 -4 -6 -8 -10 -12 0 5 10 15 20 25 30 35 40 Figure 7.15 fplot(@log_error_func,[0,40]) The root appears to be between 30 and 40. Using fzero over this range: >> fzero(@log_error_func,[30,40]) ans = 33.1155 From the plot, I might have instead estimated the lower-bound of the range to be 35. Had I done this, I would receive the following error message: >> fzero(@log_error_func,[35,40]) Error using fzero (line 274) The function values at the interval endpoints must differ in sign. After our discussion of the bisection method, this error should now make more sense to you. 7.19 Examples 245 Example 7.5 Determine the real roots of: f (x) = −26 + 82.3x − 88x 2 + 45.4x 3 − 9x 4 + 0.65x 5 a) Use fzero to find each root. Note, we have a 5th order polynomial, so we can have up to 5 roots. b) Use roots to find the roots. Please first try to solve a). This way you can better understand the difference of between fzero and roots. Solution: (Link to screen cast and accompanying M-file.) a) We start by re-writing the equation such that it is equal to 0, as is needed for fzero. 0.65x 5 − 9x 4 + 45.4x 3 − 88x 2 + 82.3x − 26 = 0 Now creating our function file: Listing 7.13 poly_error_func.m 1 function res = poly_error_func(x) 2 res = 0.65.*x.^5 - 9.*x.^4 + 45.4.*x.^3 - 88.*x.^2 + 82.3.*x - 26; 3 end Before using fzero, let’s use fplot to estimate the location of the desired root. Since we have a fifth order polynomial, can have up to five real roots. Since we don’t have a good idea of the roots, let’s look at the extreme interval of –1000 to 1000. Using fplot with these bounds we have: >> fplot(@poly_error_func,[-1000,1000]) 246 Chapter 7 Zero-finding 10 14 6 4 2 0 -2 -4 -6 -1000 -500 0 500 1000 Figure 7.16 fplot(@poly_error_func,[-1000,1000]) Over this range we see that the function only crosses the x-axis once. We therefore suspect that it has only one real root. Zooming in on the range 0 to 1: >> fplot(@poly_error_func,[0,1]) 5 0 -5 -10 -15 -20 -25 0 0.2 0.4 0.6 0.8 1 Figure 7.17 fplot(@poly_error_func,[0,1]) We find that the function crosses the x-axis over this range. Using fzero to find 7.20 CPB Examples 247 the root: >> fzero(@poly_error_func,[0,1]) ans = 0.5793 b) Next, let’s use the roots function. Remember, fzero is a robust function that may be applied to any non-linear equation. On the other hand, roots only applies to polynomials. The added benefit of roots is that it returns all of the roots from one call, both real and imaginary. We start by creating a vector of coefficients starting from the highest power in x (the n th power) down to the 0th power. (Remember, x 0 = 1, so the 0th power is the term without an x.) >> C = [0.65, -9, 45.4, -88, 82.3, -26]; Now we can use the roots function which takes as an argument the vector of coefficients. >> roots(C) ans = 5.5561 + 5.5561 1.0773 + 1.0773 0.5793 + 2.3338i 2.3338i 0.8606i 0.8606i 0.0000i We have just one real root, x = 0.5793. This agrees exactly with the result of fzero. 7.20 CPB Examples Example 7.6 Going all the way back to Example 1.5 on page 15, you were asked to calculated the normal boiling point of ethanol using Antoine’s equation. We did this using eq. (1.3) where we analytically solved Antoine’s equation for T . Doing so we calculated the normal boiling point of ethanol to be T = 78.289◦ C. Now armed with fzero, we no longer need to perform the algebra to solve for T . Using the original form of the Antoine equation, eq. (1.2), use fzero to calculate the normal boiling point of ethanol. Does you answer agree with the analytic solution? In your error function, code in the Antoine parameters and pressure. This leaves T as the only unknown. Also try using an anonymous function and check that you obtain the same answer. 248 Chapter 7 Zero-finding Solution: All the way back in Example 1.5, we analytically solved Antoine’s equation for T and used this to calculated the normal boiling point of ethanol. Doing so we found the normal boiling point to be T = 78.289 ◦ C. Here, we are asked to solve numerically using fzero. When learning a new function such as fzero, testing on problems for which you know the answer is always a good idea. The Antoine equation takes the form: log10 p sat = A − B T +C Next, we need to re-write Antoine’s equation as an error function suitable for use with fzero: log10 p sat − A + B =0 T +C Now we can write our MATLAB error function. In the function file I will define p = 760 mmHg, along with the Antoine parameters. I will minimize comments as we have seen Antoine’s equation all too much by this point. Listing 7.14 error_antoine.m 1 function res = error_antoine(t) 2 % Vapor pressure in mmHg 3 psat = 760; 4 % Antoine paramters, which use T in C and p in mmHg 5 a = 8.13484; 6 b = 1662.48; 7 c = 238.131; 8 % evaluating our error function 9 res = log10(psat)-a+b./(t+c); 10 end Taking an initial guess of T = 50 ◦ C and solving: >> tsat = fzero(@error_antoine,50) tsat = 78.2892 A perfect match with our analytic solution! Nice! So if you encounter a challenging problem in the future, why not give MATLAB a try? There is no need for an analytic solution. Also, note that while here I provide an initial guess, we know physically that Antoine’s equation is single valued. We therefore could have just specified the entire temperature range as a bracket and would have obtained the same result. In the interest of fun and incremental development, let’s keep building up our example. Rather than specify the desired pressure within the error function, let’s add it as an input variable so that the user can readily change the pressure and compare the corresponding temperature at saturation. The challenge with doing this is fzero expects a function with a single input and a single output variable. 7.21 Glossary 249 We can pass this additional information to the error function using an anonymous function. Here is what that might look like. Listing 7.15 error_antoine_tp.m 1 function res = error_antoine_tp(t,psat) 2 % Vapor pressure in mmHg 3 %psat = 760; 4 % Antoine paramters, which use T in C and p in mmHg 5 a = 8.13484; 6 b = 1662.48; 7 c = 238.131; 8 % evaluating our error function 9 res = log10(psat)-a+b./(t+c); 10 end >> psat_mmHg = 760; >> error_func = @(t) error_antoine_tp(t,psat_mmHg); >> tsat = fzero(@error_func,50) tsat = 78.2892 The same answer. Great! If you wished, you could further generalize the error function by additionally taking your Antoine parameters as input variables. 7.21 Glossary analytic solution: A way of solving an equation by performing algebraic operations and deriving an explicit way to compute a value that is only known implicitly. numerical solution: A way of solving an equation by finding a numerical value that satisfies the equation, often approximately. numerical method: A method (or algorithm) for generating a numerical solution. map: A correspondence between the elements of one set (the range) and the elements of another (the domain). You can think of sequences, vectors and functions as different kinds of maps. range: The set of values a map maps from. domain: The set of values a map maps to. discrete set: A set, like the integers, whose elements are countable. continuous set: A set, like the real numbers, whose elements are not countable. You can think of floating-point numbers as a continuous set. 250 Chapter 7 Zero-finding zero (of a function): A value in the range of a function that maps to 0. function handle: In MATLAB, a function handle is a way of referring to a function by name (and passing it as an argument) without calling it. shadow: A kind of name collision in which a new definition causes an existing definition to become invisible. In MATLAB, variable names can shadow built-in functions (with hilarious results). anonymous function: A function that is not stored in a file, but is instead stored as a variable (a function_handle). It behaves just like a function stored in a file, except it can only contain a single executable statement. MAT-file: A file containing MATLAB formatted data. We can load data from or write date to these files using the load and save functions, respectively. 7.22 Exercises 251 7.22 Exercises Exercise 7.1 Air under normal conditions flows through a 4 mm diameter pipe with an average velocity of V = 60 m/s. We wish to determine the pressure drop in a 0.2 m section of the pipe. For air under normal conditions, the density and viscosity can be taken to be ρ = 1.23 kg/m3 and µ = 1.79 × 10−5 N·s/m2 . The pressure drop (∆p) may be computed as ∆p = f l 1 ρV 2 D2 (7.11) where l is the length of the pipe, D is the pipe diameter, ρ is the density of the fluid (air), V is the fluid (air) velocity, and f is the friction factor. For this problem we have turbulant flow. The turbulant portion of the Moody chart is represented by the Colebrook formula à ! 2.51 1 ²/D + p (7.12) p = −2 log10 3.7 Re f f where ² is the pipe roughness, and Re is the Reynold’s number, defined as Re = ρV D µ (7.13) Compute the pressure drop for the following two cases, and comment on the effect of the pipe material: 1. Cast iron pipe, ² = 0.26 mm 2. Drawn tubing, ² = 0.0015 mm Note that I find the pressure drop is 9.1160 kPa for cast iron, and 3.0864 kPa for drawn tubing. Use these values to debug your code. Exercise 7.2 Building upon Exercise 7.2, it is common that you measure the pressure drop via a manometer and use this to calculate the velocity (or in general the flow rate) of the fluid. I will recast Exercise 7.2 as such a problem. Air under normal conditions flows through a 4 mm diameter pipe. Over a 0.2 m section of pipe we measure that the pressure drop, ∆p, is 50 kPa. Knowing this we wish to determine the average velocity of the flowing air. For air under normal conditions, the density and viscosity can be taken to be ρ = 1.23 kg/m3 and µ = 1.79 × 10−5 N·s/m2 . The pressure drop (∆p) may be computed as ∆p = f l 1 ρV 2 D2 (7.14) where l is the length of the pipe, D is the pipe diameter, ρ is the density of the fluid (air), V is the fluid (air) velocity, and f is the friction factor. For this problem we have turbulant flow. The turbulant portion of the Moody chart is represented by the Colebrook formula à ! 1 ²/D 2.51 + p (7.15) p = −2 log10 3.7 Re f f where ² is the pipe roughness, and Re is the Reynold’s number, defined as Re = ρV D µ (7.16) 252 Chapter 7 Zero-finding Compute the average velocity for the following two cases, and comment on the effect of the pipe material: 1. Cast iron pipe, ² = 0.26 mm 2. Drawn tubing, ² = 0.0015 mm Note that in reality, when modelling compressible fluids such as air, it is important to account for the temperature and pressure dependence of ρ and µ. It is ignored here for simplicity. For the case of the cast iron pipe, I compute a velocity of 141.1115 m/s, and for drawn tubing I compute a velocity of 281.0068 m/s. Exercise 7.3 Have you ever heard of a Galilean thermometer1 ? It is a clever thermometer invented in the 1660s. In fact, you can still find Galilean thermometers available for purchase today, although today they are mostly used for decoration. The basis of the thermometer is that the density of a liquid changes with temperature, which in turn would effect the buoyancy of an object in that liquid. While we will not exactly model a Galilean thermometer, we will model something similar. We will model the buoyancy of a spherical object of radius r o and density ρ o . The volume of a sphere with radius r o is 4/3πr o3 . If a sphere with radius r o is submerged in a liquid to a depth d , the volume (V ) of the sphere below the liquid line (or equivalently the volume of displaced liquid) is: V= ¢ π¡ 3r o d 2 − d 3 3 as long as d < 2r ; that is, so long as the object is not submerged! An object floats at the level where the weight of the displaced liquid equals the total weight of the object. The weight of the object (Wo ) and the weight of the displaced liquid (Wl ) may be computed as: 4 Wo = ρ o g Vo = ρ o g πr o3 3 Wl = ρ l g V = ρ l g ¢ π¡ 3r o d 2 − d 3 3 where g is the gravitational constant. Equating we have: ¢ 4 π¡ ρ o g πr o3 = ρ l g 3r o d 2 − d 3 3 3 And simplifying: ¡ ¢ 4ρ o r o3 = ρ l 3r o d 2 − d 3 (7.17) Okay, now we need some parameters. The spherical object can be assumed to have a constant density of ρ o = 0.3 g/mL, and have a radius of r o = 100 cm. We will model the liquid as ethanol, for which the density is described by the following equation2 : n ρ l = A · B −(1−T /C ) (7.18) 1 https://en.wikipedia.org/wiki/Galileo_thermometer 2 Yaws, Carl L. (2012; 2013; 2014). Yaws’ Critical Property Data for Chemical Engineers and Chemists. Knovel. Retrieved from https://app.knovel.com/hotlink/toc/id:kpYCPDCECD/ yaws-critical-property/yaws-critical-property 7.22 Exercises 253 where T is the temperature in K, ρ l is the liquid density in g/mL, and A = 0.276, B = 0.27668, C = 516.25, and n = 0.2367 are constants. This expression is valid over the range 159.05 to 516.25 K. As a note to make sure you are correctly calculating the density, at 298.15 K we have ρ l = 0.787 g/mL. The buoyancy of the object will change with temperature. If you measure the depth of the object, d , you can find the temperature. Cool! Before moving on, let’s pause and make a note about units. It is all about being consistent. In equation 7.17, use the same units for ρ o and ρ l as they will cancel out. For both you are provided with units of g/mL, so just use this. The units of r o and d will be the same. So if you use units of cm for r o (which you should), then d will be in units of cm. My last note is in equation 7.18, T is in K. Write a function that takes as an input the depth (d ) in cm and returns the temperature in ◦ C. To check that your code is working correctly, for d = 180 cm, I obtain 243.0829 ◦ C. For d = 80 cm, I obtain –54.1430 ◦ C. Please take your time and check your code for correctness. As a final note, solving using fzero I use a search range of 159.05 to 516.25 K. If you encounter a temperature greater than 516.25 K, you will encounter a numerical error. Exercise 7.4 In the separation of binary mixtures via distillation, the presence of an azeotrope puts a limit on the achievable separation. For a binary system at vapor/liquid equilibrium, assuming the vapor phase is an ideal gas and the Poynting correction is negligible, the relative volatility (α) takes the form α (T, x 1 ) = γ1 (T, x 1 ) p 1sat (T ) γ2 (T, x 1 ) p 2sat (T ) where x 1 is the mole fraction of component 1 (x 1 + x 2 = 1), T is the temperature, γ1 and γ2 are the activity coefficient of component 1 and 2, respectively, and p 1sat and p 2sat are the pure component vapor pressure of component 1 and 2, respectively. Component 1 corresponds to the most volatile component (MVC) and component 2 is the least volatile component (LVC). In the design of vapor/liquid separation processes (i.e., distillation), the greater the deviation of α from unity the easier the separation. When α > 1 the MVC is concentrated in the vapor phase, when α < 1 the LVC is concentrated in the vapor phase, and when α = 1 we have an azeotrope, no separation is possible. Next, we will adopt the standard assumption that γ1 and γ2 are independent of pressure, and will consider only isothermal (constant T ) vapor/liquid equilibrium (P x y). For this case, α is a function of just a single variable, x 1 . At 313.15 K, the binary system ethyl acetate(1)/ethanol(2) exhibits an azeotrope. I would like you to solve for the composition of the azeotrope. Put differently, what value of x 1 results in α = 1? Next, I will provide you with some additional information to solve the problem. The vapor pressure of ethyl acetate (p 1sat ) and ethanol (p 2sat ) can be computed using Antoine’s equation: log10 p sat = A − B T +C where A, B , and C are constants, p sat is the vapor pressure in units of mmHg, and T is the temperature in ◦ C. In the table below, I provide parameters along with the minimum (Tmin ) and maximum (Tmax ) temperature for which use of the parameters is appropriate. The parameters are all adopted from those in the freely available Dortmund Data Bank.3 3 http://ddbonline.ddbst.com/AntoineCalculation/AntoineCalculationCGI.exe 254 Chapter 7 Zero-finding Note that the Dortmund Data Bank also provides an online calculator you can use to make sure your Antoine functions are working correctly. compound ethyl acetate ethanol A 7.10179 8.20417 B 1244.95 1642.89 Tmin [◦ C] 16 –57 C 217.9 230.3 Tmax [[◦ C] 76 80 The activity coefficient can be modeled using the NRTL (non-random two-liquid) equation: · µ ln γ1 = x 22 τ21 G 21 x 1 + x 2G 21 ¶2 · µ ln γ2 = x 12 τ12 G 12 x 2 + x 1G 12 ¶2 + + τ12G 12 ¸ (x 2 + x 1G 12 )2 τ21G 21 ¸ (x 1 + x 2G 21 )2 where τ12 = g 12 − g 22 g 21 − g 11 τ21 = RT RT and G 12 = exp (−α12 τ12 ) G 21 = exp (−α21 τ21 ) where R is the molar gas constant and T is the absolute temperature. Note that the terms τ12 and τ21 are dimensionless. In theory, the parameter g i j is an energy parameter characteristic of the i - j molecular interaction, and αi j is the non-randomness parameter corresponding to the inverse of the coordination number. Know that α12 and α21 , the non-randomness parameters, are different than the relative volatility, α. We will use the following set of parameters that were regressed by students in my chemical engineering thermodynamics class during fall of 2018 using freely available reference data from the Dortmund Data Bank.4 α12 = α21 = 0.296 g 12 − g 22 = 72.347 J/mol g 21 − g 11 = 3682.7378 J/mol If you wished to compare values of α from the reference data, they would be computed as: α= y 1 /x 1 y 1 (1 − x 1 ) = y 2 /x 2 y 2 (1 − x 2 ) Using the parameters provided and a molar gas constant of R = 8.314 J/(mol K), I obtain x 1 = 0.6092. Use this to help you debug your code... if necessary. 4 http://www.ddbst.com/en/EED/VLE/VLE%20Ethanol%3BEthyl%20acetate.php 7.22 Exercises Exercise 7.5 In Example 5.10 we used the ideal gas equation of state and the truncated virial equation with corresponding states theory to estimate the molar volume of n-butane at 350 K and 2 bar. Using the truncated virial equation, we computed the compressibility, Z , and then calculated the molar volume as v = Z v ig , where v ig is the molar volume of an ideal gas at the same temperature and pressure. Since our function for the truncated virial (v_virial) is dependent on our function for the molar volume of an ideal gas (v_ideal_gas), in Section 6.1 we decided to combine the functions into a single file, where v_virial is the top function and v_ideal_gas is the helper function. Then in Exercise 6.4 we vectorized our code. Let’s keep the fun going here! So far we have used the truncated virial equation in its natural form, an expression explicit in molar volume (or equivalently the compressibility, Z ). Knowing the pure component constants, Tc , p c , and ω, and specifying the temperature and pressure of interest, the molar volume is readily computed. But what if the pressure and molar volume were specified, and you were asked to solve for the temperature? While I would not attempt to solve this problem analytically, now using fzero this is entirely possible. Let’s give it a try! Recall that from the NIST WebBook5 for n-butane we have: Tc = 425.125 K, p c = 37.960 bar, and ω = 0.201. For a pressure of p = 2 bar and molar volume of v = 1 × 104 cm3 /mol, what is the temperature? You must use fzero in your solution. To check the reasonableness of your solution, have a look at Figure 6.3 in the solution manual. 5 http://webbook.nist.gov/chemistry/fluid/ 255 Chapter 8 Systems of Equations In Chapter 7 we learned how to use MATLAB for zero-finding and to find the roots of polynomials. In Chapter 8 we continue and solve systems of linear and non-linear equations. By the end of this chapter you will be able to: • Exhibit ability to construct vectorized “error” functions for use with fsolve • Apply fsolve to solve systems of nonlinear equations • Apply rref to solve a system of linear equations If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 8.1 Matrices A matrix is a two-dimensional version of a vector. Like a vector, it contains elements that are identified by indices. The difference is that the elements are arranged in rows and columns, so it takes two indices to identify an element. We have already seen a number of ways to create a matrix of a given size (see Section 4.13 on page 97); okay, we created vectors, but a vector is a matrix. To create a matrix in which all entries are 1, we can use the built-in ones function. We have used this function so far as ones(1,N) which creates a vector of length N. The first entry (1) actually corresponds to the number of rows and the second entry corresponds to the number of columns (N). What we have been calling a vector in MATLAB you can equally call a “row” vector; it is a single rowed matrix. You could also create a “column” vector as ones(N,1). Another common usage is ones(N) creates a square matrix (i.e., same number of columns and rows) with N columns and rows. We have also used the built-in function zeros. It behaves just like ones, only every entry is assigned a value of 0. The built-in function rand takes the same form too. With rand, each element is a uniformly distributed pseudo-random number between 0 and 1; put simply, you can think of it as random number between 0 and 1. 257 258 Chapter 8 Systems of Equations Sometimes you may find you wish to create a matrix of ones or zeros which is the same size as a matrix which is already defined in your workspace. You can do this quite easily. Say matrix A is defined in your workspace. Then size(A) returns the size (or dimensions) of matrix A. You then need just pass this result to ones or zeros. Or combine the statements as ones(size(A)). Another of many ways to create a matrix is the magic function, which returns a “magic” square matrix (n by n) with the given size: >> M = magic(3) M = 8 1 3 5 4 9 6 7 2 If you don’t know the size of a matrix, you can use whos to display it: >> whos Name M Size 3x3 Bytes 72 Class double Attributes Or the size function, which returns a vector: >> V = size(M) V = 3 3 The first element is the number of rows, the second is the number of columns. To read an element of a matrix, you specify the row and column number(s): >> M(1,2) ans = 1 >> M(2,1) ans = 3 When you are working with matrices, it takes some effort to remember which index comes first, row or column. I find it useful to repeat “row, column” to myself, like a mantra. You might also find it helpful to remember “down, across,” or the abbreviation RC. Another way to create a matrix is to enclose the elements in brackets, with semi-colons between rows: >> D = [1,2,3 ; 4,5,6] D = 1 2 3 4 5 6 8.1 Matrices >> size(D) ans = 2 259 3 As you begin to work with row and column vectors, and matrices, remember our discussion of incremental development (Section 2.8) and unit testing (Section 2.9) from Chapter 2. Practice creating matrices and referring to specific elements in the Command Window to test your understanding. To be honest, this is still something I often do when writing code; sometimes there is too much to remember, so leverage the interactive abilities of MATLAB. Something we will find very useful in the future is colon notation. It will be given much more attention later, but I will introduce it here while we are discussing matrices. Many times we will want to isolate all of the elements of a specific row or column in a matrix. If I look at D like a table, perhaps row 1 contains data for a specific time at which a measurement was made. We can isolate this row using colon notation: >> D1 = D(1,:) D1 = 1 2 3 We can interpret (1,:) as row 1, all columns. You could likewise use it to isolate a specific column. To isolate just the second column, we will use (:,2) meaning all rows, second column: >> D2 = D(:,2) D2 = 2 5 which is a column vector, which we will discuss momentarily. You may also remember from our discussion of for loops that colon notation can be used to create a vector, and we then later saw how vectors of indices (think of the find command) can be used to isolate elements of a vector. Let’s put it together: >> Dr = D(:,2:3) Dr = 2 3 5 6 Is the result what you expected? While we could spend much more time on this, let me make just one more point. We see that the use of comma notation is very useful for isolating specific parts of a matrix. This can be useful to perform operations (+, –, .*, ./, etc.), logical comparisons, and to apply functions such as find only to a specific part or range of a matrix. This also gives us a way to use length, which only applies to vectors, to find the number of rows or columns. (But size is just as easy.) 260 Chapter 8 Systems of Equations 8.2 Row and column vectors Although it is useful to think in terms of scalars, vectors and matrices, from MATLAB’s point of view, everything is a matrix. A scalar is just a matrix that happens to have one row and one column: >> x = 5; >> size(x) ans = 1 1 And a vector is a matrix with only one row: >> R = 1:5; >> size(R) ans = 1 5 Well, some vectors, anyway. Actually, as we have already mentioned, there are two kinds of vectors. The ones we have seen so far are called row vectors, because the elements are arranged in a row; the other kind are column vectors, where the elements are in a single column. One way to create a column vector is to create a matrix with only one element per row: >> C = [1;2;3] C = 1 2 3 >> size(C) ans = 3 1 And since C is a vector, we can use the length command too: >> length(C) ans = 3 The difference between row and column vectors is important in linear algebra, but for most basic vector operations, it doesn’t matter. When you index the elements of a vector, you don’t have to know what kind it is: >> R(2) ans = 2 8.3 The transpose operator 261 >> C(2) ans = 2 8.3 The transpose operator The transpose operator, which looks remarkably like an apostrophe, computes the transpose of a matrix, which is a new matrix that has all of the elements of the original, but with each row transformed into a column (or you can think of it the other way around). In this example: >> D = [1,2,3 ; 4,5,6] D = 1 2 3 4 5 6 D has two rows, so its transpose has two columns: >> Dt = D' Dt = 1 4 2 5 3 6 What effect does the transpose operator have on row vectors, column vectors, and scalars? Let’s start with row vectors. We will define a row vector and then compute the transpose. >> X = 1:5 X = 1 2 3 >> Xt = X' Xt = 1 2 3 4 5 >> size(X) ans = 1 5 >> size(Xt) ans = 5 1 4 5 262 Chapter 8 Systems of Equations So the transpose of a row vector is a column vector. Likewise, the transpose of a column vector is a row vector. If we take the transpose of Xt, we will get back to X: >> Xt' ans = 1 2 3 4 5 As for a scalar, which is a 1 by 1 matrix, it is unchanged by the transpose operator. 8.4 Zero-finding: what we’ve done so far In Chapter 7 we learned to use fzero. fzero is a built-in MATLAB function that combines the best features of several efficient and robust numerical methods. Specifically, fzero employs a bracketing method; so long as the function is continuous and changes signs over the interval, then a zero exist and fzero will be successful. fzero is highly robust, and should always be your first choice for root finding. However, fzero has three limitations. 1) If a functions has more than one zero over the range of interest, MATLAB will return the location of only one of the zeroes. To overcome this, we saw how we could use fplot to improve the search range, and then we could call fzero multiple times with different search ranges. Alternatively, for the special case of a polynomial, we found we could use roots to obtain all of the roots of the polynomial. 2) Second is the restriction that the function needs to change signs over the interval. In general, this is not a problem. However, imagine the case of (x − 2)2 = 0. This equation has a zero at x = 2. However, fzero will not work. The problem is that for all values of x other than x = 2, the function is positive. The function therefore does not change signs over the interval. One way to overcome this shortcoming is to expand (x − 2)2 and then use roots. Another will be fsolve, which we will introduce momentarily. 3) Third and most important, at least as far as this chapter is concerned, is that fzero applies to a single equation containing a single unknown variable. Here we will learn about fsolve which can be applied to a system of equations. fsolve can be applied to a single equation with a single unknown, but for that case fzero should always be your first choice. 8.5 System of equations Imagine that you encounter the following equations: y = −x − 3 x 2 + y 2 = 17 8.5 System of equations 263 At first glance you might think you could solve these equations by calling fzero once to solve for x and once to solve for y. The problem is that each equation involves both variables, which is what makes this a system of equations and not just a list of unrelated equations. To solve a system, you have to solve the equations simultaneously. Fortunately, MATLAB has a built-in function fsolve that can handle systems of equations. Working with fsolve is analogous to fzero. Let us therefore start by finding the zeros of a single equation. For this case, the use of fsolve will look exactly like fzero. Imagine we would like to solve the equation x 2 − 2x = 3. We begin by writing an error function, such that the function evaluates to zero at a solution: x 2 − 2x − 3 = 0. Listing 8.1 error_func.m 1 function res = error_func(X) 2 res = X.^(2)-2.*X-3; 3 end Let’s assume we are searching for the root over the range 0 < x < 5. Solving using fzero: >> fzero(@error_func,[0,5]) ans = 3 With fzero we could alternatively provide an initial estimate of the zero location instead of a range to search. >> fzero(@error_func,4) ans = 3.0000 Solving using fsolve, the same error function is needed. The only difference for this case is you can only provide an initial estimate of the zero location (and not a range). >> fsolve(@error_func,4) ans = 3.0000 A perfect match! Note that with this call you would also receive the following information: Equation solved. fsolve completed because the vector of function values is near zero as measured by the default value of the function tolerance, and the problem appears regular as measured by the gradient. <stopping criteria details> What is most important is the top line, “Equation solved”. A discussion of the t Also, while not discussed here, fsolve can be applied to non-square systems of equations; that is, where the number of equations is greater than the number of unknowns, or when the number of unknowns is greater than the number of equations. For these cases, fsolve acts as a minimizer. I hope to add a chapter on the topic in the future. t Note that fsolve is part of MATLAB’s Optimization Toolbox. If you obtain an error message indicating that the command fsolve is unknown, you likely did not include the Optimization Toolbox in your initial MATLAB installation. 264 Chapter 8 Systems of Equations theory behind fsolve is beyond the scope of this class, so we will not worry about the rest of the message here. To learn more about fsolve, have a look at the MATLAB documentation. In Example 8.2 we will look at how the options used by fsolve can be updated, including changing the tolerance and suppressing the additional information displayed with the solution (i.e., “Equation solved”). The major reason for using fsolve over fzero is that it can be used to solve systems of equations. Using fsolve to solve a system of equations is very similar to solving a single equation. We will need to create an error function. Just as with fzero and the single equation case with fsolve, the error function will have just a single input and a single output variable. The input variable will be a vector containing the current estimate/value of the variable(s) you are solving for, and the output will be a vector containing the value of the error function for each equation in the system. Let’s consider the system of equations we started our discussion with. We first re-write our equations as a system of error functions. y +x +3 = 0 x 2 + y 2 − 17 = 0 Next, write our error function M-file. Just as before, fsolve expects that our function has a single input variable. Here the input variable will be a vector of length 2. The first element will be x and the second element will be y. Note that you can list the variables in whatever order you would like, you just need to make sure you are consistent throughout the problem. Listing 8.2 error_function_series.m 1 function res = error_function_series(X) 2 % Un-packing my input vector 3 x = X(1); 4 y = X(2); 5 % Our system of equations 6 res1 = y+x+3; 7 res2 = x^(2)+y^(2)-17; 8 % Packing up my result 9 res = [res1,res2]; 10 end The input variable is a vector with two elements, the first element is x and the second element is y. I gave it a capital letter to remind me that it is a vector. The body of the function includes three paragraphs, each explained by a comment. The first paragraph unpacks the vector by copying the elements into scalar variables. This isn’t necessary, but giving names to these values helps me remember what’s what. It also makes the second paragraph, where we compute the error functions, resemble the mathematical equations we were given, which helps prevent errors. 8.6 What can go wrong? The last paragraph packs the computed error functions back into a vector. When fsolve calls this function, it provides a vector as input and expects to get a vector as output. Calling fsolve from the Command Window will look identical as before. Only since we now have a system of equations, we need to provide an initial estimate of each variable. This problem can be solved analytically to find that x = −4 and y = 1 is a solution, as is x = 1 and y = −4. Solving with fsolve: >> x0 = 0; >> y0 = -5; >> X0 = [x0,y0] >> Sol = fsolve(@error_function_series,X0) Sol = 1.0000 -4.0000 Nice! The solution is a vector of length 2. The first element corresponds to x, and the second element corresponds to y. The order of this vector agrees exactly with the order of your vector with the initial guesses and the input vector to your error function M-file. >> x = Sol(1); >> y = Sol(2); Just as with fzero, we find that fsolve returns a single zero location. To find the other we need to start with a different set of initial guesses. >> x0 = -5; >> y0 = 0; >> X0 = [x0,y0] >> Sol = fsolve(@error_function_series,X0) Sol = -4.0000 1.0000 8.6 What can go wrong? Let’s re-visit our previous example but try an initial guess where x = y. >> x0 = 2; >> y0 = 2; >> X0 = [x0,y0]; >> Sol = fsolve(@error_function_series,X0) Sol = 2.7803 2.7803 and if were were to display the full output, we would see: 265 266 Chapter 8 Systems of Equations No solution found. fsolve stopped because the last step was ineffective. However, the vector of function values is not near zero, as measured by the default value of the function tolerance. <stopping criteria details> So MATLAB returns a solution, but it is not correct. You can confirm this by passing the result to the error function: >> error_function_series(Sol) ans = 8.5607 -1.5395 In this particular case, all we need to do is update the initial guess so that x 6= y. >> x0 = 1; >> y0 = 2; >> X0 = [x0,y0] >> Sol = fsolve(@error_function_series,X0) Sol = -4.0000 1.0000 So we find that the success of fsolve is sensitive to the initial estimate of the location of the zeroes. In general, the better the estimate, the better off you will be. If the system of equations corresponds to a physical problem, we can typically provide a reasonable initial estimate. For example, if I were solving for mole fraction compositions, I know they will be between 0 and 1. So 0.5 might be a good initial guess. This isn’t always the case, and we will see in the Examples how rand can be used. This also brings us back to the point of using fzero over fsolve. If you have a single equation with a single unknown that changes sign over the interval of interest, use fzero; it will be guaranteed to work. Additionally, this leads to the question if we were using fsolve as part of a larger script or function, how can we check that it worked without looking at the resulting message? In Section 6.4 we saw how we could write our functions to return multiple/optional output variables. This is how most of MATLAB’s built-in functions work. The first output variable provided by fsolve is the estimate of the solution to the system of equations. The second (optional) variable is the value of the system of error functions using the estimated solutions. And the third (optional) variable is the exit flag. A value of 1, 2, 3 or 4 all mean “Equation solved;” it worked! A value of 0 means it did not work because the number of iterations were exceeded. Increase the number of iterations and maybe you will be successful. And a negative value means... bad news, it didn’t work. Let’s revisit our last two calls with our new tricks. 8.6 What can go wrong? >> x0 = 2; >> y0 = 2; >> X0 = [x0,y0] >> [Sol,fun_val,flag] = fsolve(@error_function_series,X0) Sol = 2.7803 2.7803 fun_val = 8.5607 -1.5395 flag = -2 The value of fun_val agrees with what we computed previously, and since the exit flag is not –2, the result is not reliable. >> x0 = 1; >> y0 = 2; >> X0 = [x0,y0] >> [Sol,fun_val,flag] = fsolve(@error_function_series,X0) Sol = -4.0000 1.0000 fun_val = 1.0e-12 * 0.0018 0.3766 flag = 1 Nice! Remember MATLAB is using a default tolerance to determine if the solution is close enough to zero. While not exactly zero here, the values are very small. For more details on the working of fsolve and the values of the exit flag, have a look at the documentation page. Below is screenshot from the documentation page describing the three output variables just discussed. Figure 8.1 Snippet of the documentation page for fsolve that describes the first three output variables. 267 268 Chapter 8 Systems of Equations 8.7 Examples Example 8.1 Solve the following system of equations: 1 y = x −5 2 y = x 2 + 2x − 15 There are two unique solutions. Please find both. Solution: (Link to screen cast and accompanying M-file.) What fun, a system of non-linear equations. Let’s begin by solving by hand. Let’s subtract the first equation from the second. This kills y and results in: 3 x 2 + x − 10 = 0 2 We have an equation of the form ax 2 + bx + c = 0, so let’s go ahead and use the quadratic formula: x= p −b ± b 2 − 4ac 2a To help, I wrote a MATLAB function to perform the calculation for me. Listing 8.3 quadratic_eq.m 1 function res = quadratic_eq(a,b,c) 2 sqrtt = sqrt(b^(2)-4*a*c); 3 root1 = (-b+sqrtt)/(2*a); 4 root2 = (-b-sqrtt)/(2*a); 5 res = [root1,root2]; 6 end Running, and then using the result to solve for y: >> X = quadratic_eq(1,1.5,-10) X = 2.5000 -4.0000 >> Y = 0.5*X - 5 Y = -3.7500 -7.0000 In the interest of more practice, let’s go ahead and use roots and confirm we get the same answer: 8.7 Examples 269 >> C = [1, 3/2, -10]; >> X = roots(C) X = -4.0000 2.5000 >> Y = 0.5*X - 5 Y = -7.0000 -3.7500 Now let’s solve numerically using fsolve. We begin by writing our equations so that they evaluate to zero at the solution: 1 y − x +5 = 0 2 y − x 2 − 2x + 15 = 0 We can now write our error function. Listing 8.4 error_ex1.m 1 function res = error_ex1(X) 2 % Start by unpacking our input vector. I will take the first 3 % entry to be y and the second entry to be x. 4 y = X(1); 5 x = X(2); 6 7 % Our system of equations 8 res1 = y - 0.5*x + 5; 9 res2 = y - x^(2) - 2*x + 15; 10 11 % Pack-up it up 12 res = [res1,res2]; 13 end Next we can solve in the Command Window . Remember, we will need to provide an initial estimate of the solution. Rather than guess, let’s see what happens if we use the random number generator to generate them for us. I will generate initial guesses over the range –5 to 5. >> fsolve(@error_ex1,10*rand(1,2)-5) ans = -3.7500 2.5000 >> fsolve(@error_ex1,10*rand(1,2)-5) ans = -3.7500 2.5000 >> fsolve(@error_ex1,10*rand(1,2)-5) ans = -3.7500 2.5000 270 Chapter 8 Systems of Equations >> fsolve(@error_ex1,10*rand(1,2)-5) ans = -7.0000 -4.0000 There we have it! We were able to get both sets of solutions. Remember, the way I set-up my error function, the first value is y followed be x. The results agree perfectly with what we found by hand. Example 8.2 Solve the following system of equations: xy = 1 x+y =2 Solution: (Link to screen cast and accompanying M-file.) Here there is only one unique solution. By inspection, we know the solution will be x = y = 1. Let’s see how fsolve does. Let’s re-write our equations so that they equal zero, then create our error function. xy −1 = 0 x + y −2 = 0 Listing 8.5 error_ex2.m 1 function res = error_ex2(X) 2 % Unpack! Let the first entry be y and the second x 3 y = X(1); 4 x = X(2); 5 6 % Our system of equations 7 res1 = x*y-1; 8 res2 = x+y-2; 9 10 % Pack it up! 11 res = [res1,res2]; 12 end Just as in the last problem, I will solve by taking an initial guess of my solutions using the random number generator with values between –5 and 5. >> fsolve(@error_ex2,10*rand(1,2)-5) ans = 0.9994 1.0006 8.7 Examples 271 The numeric answer is not exactly x = y = 1. We can confirm by passing the result to our error function. >> error_ex2(ans) ans = 1.0e-06 * -0.3326 -0.0000 It’s not quite zero, but it is very close, and likely close enough for most applications. In the interest of having fun unit testing, if you wished to improve the accuracy of the calculation, this is a matter of the tolerance used by fsolve to determine if it is close enough. You can have a look at MATLAB’s documentation, but the default tolerances are 1 × 10−6 . Let’s make them smaller and see what happens. To change the options, have a look at the documentation page on “optimoptions” and the options table in the documentation page for fsolve. Since we will have an interest in changing the tolerance, also see the documentation page on “Tolerances and Stopping Criteria.” Let’s begin by displaying the current, default options: >> options = optimoptions('fsolve') options = fsolve options: Options used by current Algorithm ('trust-region-dogleg'): (Other available algorithms: 'levenberg-marquardt', 'trust-region') Set properties: No options set. Default properties: Algorithm: CheckGradients: Display: FiniteDifferenceStepSize: FiniteDifferenceType: FunctionTolerance: MaxFunctionEvaluations: MaxIterations: OptimalityTolerance: OutputFcn: PlotFcn: SpecifyObjectiveGradient: StepTolerance: TypicalX: UseParallel: 'trust-region-dogleg' 0 'final' 'sqrt(eps)' 'forward' 1.0000e-06 '100*numberOfVariables' 400 1.0000e-06 [] [] 0 1.0000e-06 'ones(numberOfVariables,1)' 0 Show options not used by current Algorithm ('trust-region-dogleg') You can match up all of the options under Default properties in the options 272 Chapter 8 Systems of Equations table in the fsolve documentation. Here, we update FunctionTolerance and OptimalityTolerance, and store the new set of options to a new variable, options2. While you can and normally would suppress the output, I will display it here so you can see what has changed: >> options2 = optimoptions('fsolve','FunctionTolerance',1e-12,... 'OptimalityTolerance',1e-12) options2 = fsolve options: Options used by current Algorithm ('trust-region-dogleg'): (Other available algorithms: 'levenberg-marquardt', 'trust-region') Set properties: FunctionTolerance: 1.0000e-12 OptimalityTolerance: 1.0000e-12 Default properties: Algorithm: CheckGradients: Display: FiniteDifferenceStepSize: FiniteDifferenceType: MaxFunctionEvaluations: MaxIterations: OutputFcn: PlotFcn: SpecifyObjectiveGradient: StepTolerance: TypicalX: UseParallel: 'trust-region-dogleg' 0 'final' 'sqrt(eps)' 'forward' '100*numberOfVariables' 400 [] [] 0 1.0000e-06 'ones(numberOfVariables,1)' 0 Show options not used by current Algorithm ('trust-region-dogleg') Now when we call fsolve we can provide an additional input argument corresponding to our set of options. I will use both the initial, default options we stored to options, and then our updated set in options2. >> sol = fsolve(@error_ex2,10*rand(1,2)-5,options) sol = 0.9994 1.0006 >> sol2 = fsolve(@error_ex2,10*rand(1,2)-5,options2) sol2 = 1.0000 1.0000 Nice! What if we wanted to update options2 to also not display the message with the solution? The relevant option is Display, and we can change its value as: 8.8 One final fsolve note 273 >> options2 = optimoptions(options2,'Display','none') options2 = fsolve options: Options used by current Algorithm ('trust-region-dogleg'): (Other available algorithms: 'levenberg-marquardt', 'trust-region') Set properties: Display: 'none' FunctionTolerance: 1.0000e-12 OptimalityTolerance: 1.0000e-12 Default properties: Algorithm: CheckGradients: FiniteDifferenceStepSize: FiniteDifferenceType: MaxFunctionEvaluations: MaxIterations: OutputFcn: PlotFcn: SpecifyObjectiveGradient: StepTolerance: TypicalX: UseParallel: 'trust-region-dogleg' 0 'sqrt(eps)' 'forward' '100*numberOfVariables' 400 [] [] 0 1.0000e-06 'ones(numberOfVariables,1)' 0 Show options not used by current Algorithm ('trust-region-dogleg') And last, if you wanted to reset options2 to contain the default values, we need just go back to our starting command: options2 = optimoptions(’fsolve’). If the options used here do not work for you, have a look at the documentation page “Current and Legacy Option Name Tables.” Many of the option names changed in MATLAB R2016a. If you are using GNU Octave, consult its documentation. t By later, these are topics I 8.8 One final fsolve note In this chapter we were introduced fsolve to solve systems of equations. We considered problems where the system of equation was square, i.e., the number of equations was equal to the number of unknowns. We will see later that fsolve also works when the system of equations is not square, where either the number of equations is greater than the number of unknowns (as is the case in data fitting) or when the number of equations is less than the number of unknowns (as in the case of optimization/minimization). Also, while fsolve can be used to solve systems of linear and non-linear equations, the case of linear equations is unique in that they can readily be solved exactly. We will consider this special case next. hope to add to the text in the near future. 274 Chapter 8 Systems of Equations 8.9 System of linear equations In Section 8.4 we found the roots of the equation x 2 − 2x = 3. This corresponds to a non-linear equation. This specific case corresponds to a quadratic equation, or a second-order polynomial, where the largest power of x is 2. A second-order polynomial will have two roots. Note that both roots need not be real, so we would say the equation could have up to two real roots. Likewise, when solving systems of non-linear equations, we found that we could have more than one unique set of roots. For the case of a single variable, a linear equation takes the form ax + b = 0, where a and b are constants. Any (single variable) equation that can not be put in this form is non-linear. For the case of a linear equation, there exist just a single root. Likewise, when we solve a system of linear equations, we will find that there exists only a single set of unique roots.1 As an undergraduate when I took my Circuit Analysis class, all I recall is setting up and solving systems of linear equations. Solving systems of linear equations was also a major component of your Mass and Energy Balance class. This is typically a major topic in a course in Linear Algebra. And as you might imagine, methods specifically exist for solving systems of linear equations. If you are interested, the “Open Textbook Library” contains a number of excellent, freely available linear algebra textbooks. 8.10 Gaussian elimination Let’s start by considering an example. Consider the following system of linear equations: 2x + 3y = 2 (8.1) x + 4z = 1 (8.2) y +z =4 (8.3) Let’s solve! First, we start by noticing that the system of equations is square. We have 3 equations and 3 unknowns. So it can be solved. I might start by adding −4×eq. 8.3 to eq. 8.2. This would result in: x − 4y = −15 (8.4) Next, add 43 eq. 8.4 to eq. 8.1. This results in: 2.75x = −9.25 (8.5) 1 This is true so long as the system of equations is square. That is, there is the same number of equations as unknowns. This is not a mathematics class, so I will leave this at that. 8.10 Gaussian elimination 275 We can then solve this equation and get x = −3.3636. Now that we have x, we can go to eq. 8.4 and eliminate x to solve for y = 2.9091. And then finally we could go back to eq. 8.3 to eliminate y and find that z = 1.0909. Nice! We just solved a system of of linear equations using an elimination strategy. We could also represent the system of equations as a matrix and solve in matrix form. We create a matrix by first re-writing the equations so that every variable is present in each equation, and I will order the variables from left to right as x, y, z. 2x + 3y + 0z = 2 (8.6) x + 0y + 4z = 1 (8.7) 0x + y + z = 4 (8.8) Where a variable was previously missing, I add zero times the variable. (Remember anything times zero is zero.) Next, we will construct a matrix containing the coefficients and the right-hand-side of the equality. Row 1 will be eq. 8.6, row 2 will be eq. 8.7, and row 3 will be eq. 8.8. Column 1 will be the coefficient of x, column 2 will be the coefficient of y, column 3 will be the coefficient of z, and column 4 will be the right-hand-side of the equality. 2 3 0 2 1 0 4 1 0 1 1 4 Just as we previously multiplied equations by a constant and then added equations together, we would do the same thing here with our rows. The goal would be for elements (1,1), (2,2), and (3,3) to be 1, while the other elements in rows 1–3 and column 1–3 are zero. If we were then to translate back to equation form, we would find that we just solved for x, y, and z. Solving in matrix form we would perform row reductions, where the three elementary row operations are: 1) replacement, 2) interchange, and 3) scaling. The final form of the matrix just described would correspond to reduced row echelon form. Algorithms exist to obtain the reduced row echelon form of a matrix. MATLAB has a built-in function to do just this called rref. Here is how we would use it to solve this system of equations: >> A = [2, 3, 0, 2; 1, 0, 4, 1; 0, 1, 1, 4]; >> Sol = rref(A) Sol = 1.0000 0 0 0 1.0000 0 0 0 1.0000 -3.3636 2.9091 1.0909 276 Chapter 8 Systems of Equations A perfect match! If you wished to filter out the roots, you could do so in a number of ways. We could create a vector: >> S = Sol(:,4) S = -3.3636 2.9091 1.0909 This is a column vector. If you preferred a row vector, you could take the transpose. This would give a result analogous if we had used fsolve. Know that you could use fsolve to solve a system of linear equation. Use of rref is preferred, as it avoids the need of initial conditions and you can be confident that it will be successful. 8.11 What could go wrong? The main issue that could cause rref to fail is if your system of equations is not square. Typically, this happens when you have more unknowns than equations. For this case, you can not solve for a unique set of solutions. The formal way to check if your system of equations is square is to compute the rank of the matrix. The rank of a matrix corresponds to the dimension of the column space of the matrix. For our purposes, the rank of the matrix tells us how many of the equations are linearly independent. If the rank of the matrix is equivalent to the number of variables, then the system of equations is square. >> r = rank(A) r = 3 How can we determine the number of unknowns (i.e., variables)? Simple: >> c = length(A(1,:))-1 c = 3 or equivalently using the end keyword: >> c = length(A(1,1:end-1)) c = 3 Here, A(1,:) corresponds to the first row of matrix A, all columns. So it pulls off the first row as a row vector. We want the length of this vector minus 1; minus 1 because the last column corresponds to the right-hand-side of the equality in the original equations. Equivalently, A(1,1:end-1) corresponds to the first row of matrix A, and columns 1 to the second last (i.e., the last column minus 1). You can 8.12 Left division 277 check to make sure that the number of variables is equivalent to the rank. If not, the system of equations is not square. So what would be an example of a non-square system of equations? First, you could have more equations than unknowns or more unknowns than equations. The more common scenario is the system of equations appears square, but two or more of the equations are linear combinations of each other. What if we slightly changed our system of equations to the following: 2x + 3y = 2 (8.9) 2x + 4y + z = 6 (8.10) y +z =4 (8.11) t This corresponds to having more unknowns than equations. Maybe a better way to phrase is would be more unknown than linearly independent equations. Upon inspection, we find that the system of equations is not square. Eq. 8.10 is equal to the sum of eqs. 8.9 and 8.11. Let’s see what happens if we attempt rref and rank. >> B = [2, 3, 0, 2; 2, 4, 1, 6; 0, 1, 1, 4]; >> Solb = rref(B) Solb = 1.0000 0 -1.5000 -5.0000 0 1.0000 1.0000 4.0000 0 0 0 0 >> rb = rank(B) rb = 2 Does the output make sense to you? A system of linear equations will have a single, unique solution if the system of equations is square. This is why you spent so much time learning how to set-up problems properly in your Mass and Energy Balance course. As always, have a look at the documentation pages for rref and rank to learn more. 8.12 Left division Before wrapping-up, in addition to using rref, another technique to solve a system of linear equations is left division. Let us again consider the system of equations given by eqs. 8.1, 8.2, and 8.3. We can re-write the system in the general form Ax = b, where A is a (square, n × n) matrix of the coefficients, b is a column vector of the values on the right-hand-side of the equality, and x is a column vector of the (unknown) variables we wish to solve for. For this problem this would take the form: t Let A and B correspond to matrices. Then (right division) A/B = AB −1 . On the other hand, (left division) A\B = A −1 B 278 Chapter 8 Systems of Equations 2 3 0 x 2 1 0 4 y = 1 0 1 1 z 4 We can solve as x = A −1 b, or equivalently using left division x = A\b. Let’s check that this gives the same answer as rref: >> A = [2,3,0; 1,0,4; 0,1,1]; >> b = [2;1;4]; >> Sol1 = inv(A)*b Sol1 = -3.3636 2.9091 1.0909 >> Sol2 = A^(-1)*b Sol2 = -3.3636 2.9091 1.0909 >> Sol3 = A\b Sol3 = -3.3636 2.9091 1.0909 We find that we get exactly the same solution as with rref using either notation, where inv is the MATLAB function to compute the inverse of matrix, where inv(A) and Aˆ(-1) are equivalent. You can use either solution strategy (and either notation) and obtain the same result. Know that in general I try to avoid left division. Why? Because too often I confuse myself and make a mistake. Left division is not an operation I am used to, and I suspect most of you have never heard of it before. What we commonly refer to simply as division, could just as well be called right division and is designated with a forwardslash (/). Left division is designated with a backslash (\). To understand the difference, let’s perform some unit testing in MATLAB. >> 8/4 ans = 2 >> 8\4 ans = 0.5 8.13 Application: Raoult’s Law 279 We find for this simple scalar case that left division essentially reverses the division. Again, this tends to confuse me, so I stick to using inv. 8.13 Application: Raoult’s Law The ability to model phase-equilibria is of central importance in the design of separation processes. Having discussed solving systems of equations, let’s consider the case of modeling binary vapor/liquid equilibrium using Raoult’s law. Raoult’s law assumes: 1. The liquid phase is an ideal solution 2. The vapor phase is an ideal gas 3. We are well removed from the critical point such that f i0 ≈ p isat If you do not know what this means, no worries, just be sure to sign-up for Chemical Engineering Thermodynamics before you graduate. A situation where Raoult’s law would be appropriate would be to model mixtures of alkanes at ambient pressures. A binary system at vapor/liquid equilibrium is governed by the following system of non-linear equations: y 1 p = x 1 p 1sat (8.12) y 2 p = x 2 p 2sat (8.13) where y 1 and y 2 are the vapor phase mole fractions of component 1 and 2, respectively, x 1 and x 2 are the liquid phase mole fractions of component 1 and 2, respectively, where y 1 + y 2 = 1 and x 1 + x 2 = 1. The pressure is given by p, and p 1sat and p 2sat are the pure component vapor pressure of component 1 and 2, respectively. The pure component vapor pressure is only a function of temperature, and is commonly computed using Antoine’s equation: log10 p sat = A − B T +C (8.14) If we add eqs. 8.12 and 8.13 together we obtain: p = x 1 p 1sat + x 2 p 2sat (8.15) This is referred to as a “bubble p” calculation, because it corresponds to the bubble point pressure for a given liquid composition. The expression additionally gives the important result that the bubble line of as system obeying Raoult’s law is a linear combination of the pure component saturation pressures. A system for which Raoult’s law is appropriate is hexane/cyclohexane. For this system, Antoine parameters and reference vapor/liquid equilibrium data is 280 Chapter 8 Systems of Equations available for free from the Dortmund Data Bank (DDBST). We find the following Antoine parameters, where the pressure is in mmHg and the temperature is in ◦ C 2 : species hexane cyclohexane cyclohexane A 7.01051 6.85146 7.09926 B 1246.33 1206.47 1380.54 C 232.988 223.136 246.526 Tmin –95 7 29 Tmax 235 81 280 For a binary system at two-phase equilibria, we have two degrees of freedom. This means if we specify two intensive properties, any two intensive properties, the thermodynamic state of our system is fixed. This could be T and p, T and x 1 , p and x 1 , or any other combination. This agrees with our system of equations; if we specify two intensive properties, we are left with two equations with two unknowns. We will consider the two very common cases of specifying T and x 1 , and specifying p and x 1 . 8.13.1 Specifying T and x 1 When we specify T and x 1 , we solve for p and y 1 . We can solve this problem a number of ways using MATLAB. First, we could use fsolve. Having specified T and x 1 , eqs. 8.12 and 8.13 (and using eq. 8.14) corresponds to a system of two equations with two unknowns. We can therefore set-up an error function M-file and solve using fsolve. This equation is perhaps the most straightforward. We do not need to manipulate the provided equations at all, we can just go directly the MATLAB. The main challenge that students often face is no matter how you decide to set-up your code and error function, you will inevitably need to use an anonymous function. 2 http://ddbonline.ddbst.com/AntoineCalculation/AntoineCalculationCGI.exe 8.13 Application: Raoult’s Law Listing 8.6 raoult_tx.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 % function res = raoult_tx(Ant,t_C,x1) % % Function to compute binary vapor liquid equilibrium using Raoult's law. % Temperature and liquid mole fraction are known, and we solve for % the vapor phase mole fraction and pressure. % % Inputs: Ant is a matrix containing the Antoine parameters for % component 1 and 2, respectively. The first row corresponds to % parameters for component 1, and the second row to parameters % for component 2, where there are three columns corresponding % to A, B, and C. The units are such that temperature is in C % and pressure is in mmHg. % t_C is the temperature of interest in degrees C % x1 is the liquid phase mole fraction % % Outputs: res is a vector of length 2, where the first element is % y1 and the second element is p. p will be in units of kPa function res = raoult_tx(Ant,t_C,x1) % Calculate the vapor pressure of component 1 and 2 in Pa psat_1 = antoine(Ant(1,:),t_C); psat_2 = antoine(Ant(2,:),t_C); % Next, we need to set-up our error functions for use with fsolve. % Remember fsolve expects an error function with a single input and % single output variable. Here I have written a general error function % for solving tx problems where the liquid mole fraction, x1, and the % vapor pressure of each component are also taken as input. Remember we % can pass this additional information using anonymous functions error_func = @(Y) raoult_error_tx(x1,psat_1,psat_2,Y); end % % % % % % % % Now we can use fsolve to solve for the vapor mole fraction and % pressure. The order of the variables must agree with that used in the % error function. We will also need to provide initial estimates. A % safe estimate would be that y1 = 0.5, or somewhere in between 0 an 1, % and for p, try x1*psat_1+x2*psat_2. You can play around with this. y10 = 0.5; p0 = x1*psat_1+(1-x1)*psat_2; % Note that is the solution for p for the special case of Raoult's law, % but not in general. So this will make a good initial guess for the % general case. res = fsolve(error_func,[y10,p0]); % % Converting the pressure to kPa before returning res(2) = res(2)/1000; function res = antoine(Ant,t_C) Helper function to compute vapor pressure using Antoine's equation. Temperature is in degrees C, and pressure is in mmHg Inputs: Ant is a vector of length 3 of Antoine parameters A, B and C t_C is the temperature in degrees C 281 282 Chapter 8 Systems of Equations 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 % Outputs: res is the vapor pressure in units of Pa. % function res = antoine(Ant,t_C) % un-packing the vector of Antoine parameters a = Ant(1); b = Ant(2); c = Ant(3); % Calculating the vapor pressure. Temperature is in C and pressure in % mmHg p_mmHg = 10^( a-b/(t_C+c) ); % Converting to units of Pa (SI units) and returning res = p_mmHg*(101.325e3/760); end % function res = raoult_error_tx(x1,psat_1,psat_2,Y) % % The error function for solving Raoult's law for binary vapor liquid % equilibrium. Everything is in SI units. % function res = raoult_error_tx(x1,psat_1,psat_2,Y) % un-packing Y y1 = Y(1); p = Y(2); % Relating y1 and y2, and x1 and x2. This will make my code % more readible y2 = 1-y1; x2 = 1-x1; % Our error functions res1 = y1*p - x1*psat_1; res2 = y2*p - x2*psat_2; % packing up our error functions res = [res1,res2]; end Let’s test our code for the binary system hexane(1)/cyclohexane(2) at 70 ◦ C (or 343.15 K) and x 1 = 0.125. At these conditions, experimental values from DDBST are y 1 = 0.179 and p = 77.327 kPa. 3 To facilitate the calculation, I will create another function specific to this system. 3 http://www.ddbst.com/en/EED/VLE/VLE%20Cyclohexane%3BHexane.php 8.13 Application: Raoult’s Law Listing 8.7 hexane_cyclohexane_tx.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 % % % % % % function res = hexane_cyclohexane_tx(t_C, x1) Function to compute p and y1 using Raoult's law for the binary system hexane(1)/cyclohexane(2) at a given temperature in C (t_C) and liquid phase mole fraction (x1) function res = hexane_cyclohexane_tx(t_C, x1) % Antoine parameters where T is in units C and p is in units of mmHg. % % Parameters for hexane(1) valid over the range -95 to 235 C Ant_1 = [7.01051,1246.33,232.988]; % Parameters for cyclohexane(2) valid over the range 29 to 280 C Ant_2 = [7.09926,1380.54,246.526]; % % Packing the Antoine parameters to a matrix as used as an input to % raoult_tx Ant = [Ant_1; Ant_2]; % Performing Raoult's law calculation res = raoult_tx(Ant,t_C,x1); end >> VLE = hexane_cyclohexane_tx(70,0.125) VLE = 0.1709 76.9205 This is in excellent agreement with the reference data. Nice! Note that I could have made raoult_tx a helper function within hexane_cyclohexane_tx, but I did not do so here in the interest of space. Alternatively, we could have solved the problem analytically. Remember that p 1sat and p 2sat are only a function of T . So by specifying T , p 1sat and p 2sat are fixed and can be computed directly using Antoine’s equation (eq. 8.15). Therefore, we could first solve directly for p via eq. 8.15, and then solve for y 1 directly as: y 1 = x 1 p 1sat /p. 283 284 Chapter 8 Systems of Equations Listing 8.8 raoult_tx_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 % function res = raoult_tx_2(Ant,t_C,x1) % % Function to compute binary vapor liquid equilibrium using Raoult's law. % Temperature and liquid mole fraction are known, and we solve for % the vapor phase mole fraction and pressure. In this version we solve the % system of equations analytically. % % Inputs: Ant is a matrix containing the Antoine parameters for % component 1 and 2, respectively. The first row corresponds to % parameters for component 1, and the second row to parameters % for component 2, where there are three columns corresponding % to A, B, and C. The units are such that temperature is in C % and pressure is in mmHg. % t_C is the temperature of interest in degrees C % x1 is the liquid phase mole fraction % % Outputs: res is a vector of length 2, where the first element is % y1 and the second element is p. p will be in units of kPa function res = raoult_tx_2(Ant,t_C,x1) % Calculate the vapor pressure of component 1 and 2 in Pa psat_1 = antoine(Ant(1,:),t_C); psat_2 = antoine(Ant(2,:),t_C); % Next, perform a bubble p calculation to find p in Pa % p_Pa = x1*psat_1 + (1-x1)*psat_2; % Then we can solve for y1 y1 = x1*psat_1/p_Pa; % Last, convert the pressure to kPa, then pack up y1 and p and return p_kPa = p_Pa/1000; res = [y1,p_kPa]; end % function res = antoine(Ant,t_C) % % Helper function to compute vapor pressure using Antoine's equation. % Temperature is in degrees C, and pressure is in mmHg % % Inputs: Ant is a vector of length 3 of Antoine parameters A, B and C % t_C is the temperature in degrees C % Outputs: res is the vapor pressure in units of Pa. % function res = antoine(Ant,t_C) % un-packing the vector of Antoine parameters a = Ant(1); b = Ant(2); c = Ant(3); % Calculating the vapor pressure. Temperature is in C and pressure in % mmHg p_mmHg = 10^( a-b/(t_C+c) ); 8.13 Application: Raoult’s Law 54 % Converting to units of Pa (SI units) and returning 55 res = p_mmHg*(101.325e3/760); 56 end Listing 8.9 hexane_cyclohexane_tx_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 % % % % % % function res = hexane_cyclohexane_tx_2(t_C, x1) Function to compute p and y1 using Raoult's law for the binary system hexane(1)/cyclohexane(2) at a given temperature in C (t_C) and liquid phase mole fraction (x1) function res = hexane_cyclohexane_tx_2(t_C, x1) % Antoine parameters where T is in units C and p is in units of mmHg. % % Parameters for hexane(1) valid over the range -95 to 235 C Ant_1 = [7.01051,1246.33,232.988]; % Parameters for cyclohexane(2) valid over the range 29 to 280 C Ant_2 = [7.09926,1380.54,246.526]; % % Packing the Antoine parameters to a matrix as used as an input to % raoult_tx Ant = [Ant_1; Ant_2]; % Performing Raoult's law calculation res = raoult_tx_2(Ant,t_C,x1); end >> VLE_2 = hexane_cyclohexane_tx_2(70,0.125) VLE_2 = 0.1709 76.9205 Solving analytically we obtain the same result. So which solution is preferred? Of course if you can solve a problem analytically it is preferred. However, if in the process of working up your expressions to solve analytically there is opportunity for you to make a mistake, then the numerical solution with fsolve may be preferred. The use of fsolve is additionally nice as the solution for the case of specifying p and x 1 is very similar. We will consider this case next 8.13.2 Specifying p and x 1 The other common scenario is p and x 1 are known, and we wish to solve for T and y 1 . Let’s start by saving a copy of our function raoult_tx as raoult_px and updating for this case. This case is a little more challenging. The reason is when T is unknown, so are p 1sat and p 2sat . Let’s test our code for the binary system hexane(1)/cyclohexane(2) at 101.33 kPa and x 1 = 0.3. At these conditions, experimental values from DDBST are y 1 = 0.384 and T = 349.55 K (76.4 ◦ C). 285 286 Chapter 8 Systems of Equations Listing 8.10 raoult_px.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 % function res = raoult_px(Ant,p_kPa,x1) % % Function to compute binary vapor liquid equilibrium using Raoult's law. % Pressure and liquid mole fraction are known, and we solve for % the vapor phase mole fraction and temperature. % % Inputs: Ant is a matrix containing the Antoine parameters for % component 1 and 2, respectively. The first row corresponds to % parameters for component 1, and the second row to parameters % for component 2, where there are three columns corresponding % to A, B, and C. The units are such that temperature is in C % and pressure is in mmHg. % p_kPa is the pressure of interest in kPa % x1 is the liquid phase mole fraction % % Outputs: res is a vector of length 2, where the first element is % y1 and the second element is T. T will be in units of C function res = raoult_px(Ant,p_kPa,x1) % Next, we need to set-up our error functions for use with fsolve. % Remember fsolve expects an error function with a single input and % single output variable. Here I have written a general error function % for solving px problems where the liquid mole fraction, x1, the pressure % and the Antoine parameters are also taken as input. % The Antoine parameters are essential because when we guess a value of % temperature each iteration, we will need to compute the % corresponding pure component vapor pressure. Remember we % can pass this additional information using anonymous functions error_func = @(Y) raoult_error_px(x1,p_kPa,Ant,Y); end % % % % % % % % Now we can use fsolve to solve for the vapor mole fraction and % temperatute. The order of the variables must agree with that used in the % error function. We will also need to provide initial estimates. A % safe estimate would be that y1 = 0.5, or somewhere in between 0 an 1, % and for t, try 0.5*(tsat_1+tsat_2). You can play around with this. % % How do we get tsat_1 and tsat_2? It is the temperature where % psat_1 = p and psat_2 = p. % y10 = 0.5; tsat_1 = antoine_t(Ant(1,:),p_kPa); tsat_2 = antoine_t(Ant(2,:),p_kPa); t0 = 0.5*(tsat_1+tsat_2); res = fsolve(error_func,[y10,t0]); function res = antoine(Ant,t_C) Helper function to compute vapor pressure using Antoine's equation. Temperature is in degrees C, and pressure is in mmHg Inputs: Ant is a vector of length 3 of Antoine parameters A, B and C t_C is the temperature in degrees C 8.13 Application: Raoult’s Law 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 % Outputs: res is the vapor pressure in units of Pa. % function res = antoine(Ant,t_C) % un-packing the vector of Antoine parameters a = Ant(1); b = Ant(2); c = Ant(3); % Calculating the vapor pressure. Temperature is in C and pressure in % mmHg p_mmHg = 10^( a-b/(t_C+c) ); % Converting to units of Pa (SI units) and returning res = p_mmHg*(101.325e3/760); end % function res = antoine_t(Ant,p_kPa) % % Helper function to compute saturation temperature using Antoine's equation. % Temperature is in degrees C, and pressure is in mmHg. The input % pressure is in kPa, so we will need to convert. % % Inputs: Ant is a vector of length 3 of Antoine parameters A, B and C % p_kPa is the pressure in kPa % Outputs: res is the temperature in C. % function res = antoine_t(Ant,p_kPa) % un-packing the vector of Antoine parameters a = Ant(1); b = Ant(2); c = Ant(3); % Convert pressure to mmHg p_mmHg = p_kPa*1000*(760/101.325e3); % Calculating the vapor pressure. Temperature is in C and pressure in % mmHg res = -b/(log10(p_mmHg)-a) - c; end % function res = raoult_error_tx(x1,psat_1,psat_2,Y) % % The error function for solving Raoult's law for binary vapor liquid % equilibrium. Everything is in SI units. % function res = raoult_error_px(x1,p_kPa,Ant,Y) % un-packing Y y1 = Y(1); t_C = Y(2); % with this T, calculate the vapor pressure % Calculate the vapor pressure of component 1 and 2 in Pa psat_1 = antoine(Ant(1,:),t_C); psat_2 = antoine(Ant(2,:),t_C); % Converting our pressure from kPa to Pa p_Pa = p_kPa*1000; % Relating y1 and y2, and x1 and x2. This will make my code % more readible y2 = 1-y1; x2 = 1-x1; % Our error functions 287 288 Chapter 8 Systems of Equations 109 110 111 112 113 end res1 = y1*p_Pa - x1*psat_1; res2 = y2*p_Pa - x2*psat_2; % packing up our error functions res = [res1,res2]; Listing 8.11 hexane_cyclohexane_px.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 % % % % % % function res = hexane_cyclohexane_px(p_kPa, x1) Function to compute T and y1 using Raoult's law for the binary system hexane(1)/cyclohexane(2) at a given pressure in kPa (p_kPa) and liquid phase mole fraction (x1). The resulting temperature will be in C. function res = hexane_cyclohexane_px(p_kPa, x1) % Antoine parameters where T is in units C and p is in units of mmHg. % % Parameters for hexane(1) valid over the range -95 to 235 C Ant_1 = [7.01051,1246.33,232.988]; % Parameters for cyclohexane(2) valid over the range 29 to 280 C Ant_2 = [7.09926,1380.54,246.526]; % % Packing the Antoine parameters to a matrix as used as an input to % raoult_tx Ant = [Ant_1; Ant_2]; % Performing Raoult's law calculation res = raoult_px(Ant,p_kPa,x1); end >> VLE = hexane_cyclohexane_px(Ant,101.33,0.3) VLE = 0.3814 76.6262 Once again this is in excellent agreement with the reference data. Just like the last problem, we could alternatively solve the system of equations sequentially. First, we perform a bubble p calculation (eq. 8.15). Eq. 8.15 again has a single unknown. However, this time around p is known while T is unknown. The complication is it is not clear if this can be solved analytically. However, a single equation with a single unknown can readily be solved using fzero. Then once we have solved for T , we can again directly solve for y 1 as y 1 = x 1 p 1sat /p. Remember in our expressions, p 1sat and p 2sat are only functions of T . 8.13 Application: Raoult’s Law Listing 8.12 raoult_px_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 % function res = raoult_px_2(Ant,p_kPa,x1) % % Function to compute binary vapor liquid equilibrium using Raoult's law. % Pressure and liquid mole fraction are known, and we solve for % the vapor phase mole fraction and temperature. In this version of the % code we will solve the system of equations sequentially. % % Inputs: Ant is a matrix containing the Antoine parameters for % component 1 and 2, respectively. The first row corresponds to % parameters for component 1, and the second row to parameters % for component 2, where there are three columns corresponding % to A, B, and C. The units are such that temperature is in C % and pressure is in mmHg. % p_kPa is the pressure of interest in kPa % x1 is the liquid phase mole fraction % % Outputs: res is a vector of length 2, where the first element is % y1 and the second element is T. T will be in units of C function res = raoult_px_2(Ant,p_kPa,x1) % Next, we need to set-up our error function for use with fzero. % Remember fzero expects an error function with a single input and % single output variable. Here I have written a general error function % for solving px problems where the liquid mole fraction, x1, the pressure % and the Antoine parameters are also taken as input. % The Antoine parameters are essential because when we guess a value of % temperature each iteration, we will need to compute the % corresponding pure component vapor pressure. Remember we % can pass this additional information using anonymous functions error_func = @(t) raoult_error_px(x1,p_kPa,Ant,t); % Now we can use fzero to solve for the temperature in C. % We will also need to provide initial estimate of T or a search range. % A tempting search range is between tsat_1 and tsat_2. However, I % avoid this as it could fail if in the future, when this function is % adapted for a non-ideal system, for an azeotropic system. I instead % use an inital estimate of 0.5*(tsat_1+tsat_2). You can play around % with this. % % How do we get tsat_1 and tsat_2? It is the temperature where % psat_1 = p and psat_2 = p. % tsat_1 = antoine_t(Ant(1,:),p_kPa); tsat_2 = antoine_t(Ant(2,:),p_kPa); t0 = 0.5*(tsat_1+tsat_2); t_C = fzero(error_func,t0); % Now that we have t_C, solve for psat_1 in Pa psat_1 = antoine(Ant(1,:),t_C); % And now directly solve for y1. We will need to convert p from kPa to % Pa y1 = x1*psat_1/(p_kPa*1000); 289 290 Chapter 8 Systems of Equations 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 end % Now packing up y1 and t_C to be returned res = [y1,t_C]; % function res = antoine(Ant,t_C) % % Helper function to compute vapor pressure using Antoine's equation. % Temperature is in degrees C, and pressure is in mmHg % % Inputs: Ant is a vector of length 3 of Antoine parameters A, B and C % t_C is the temperature in degrees C % Outputs: res is the vapor pressure in units of Pa. % function res = antoine(Ant,t_C) % un-packing the vector of Antoine parameters a = Ant(1); b = Ant(2); c = Ant(3); % Calculating the vapor pressure. Temperature is in C and pressure in % mmHg p_mmHg = 10^( a-b/(t_C+c) ); % Converting to units of Pa (SI units) and returning res = p_mmHg*(101.325e3/760); end % function res = antoine_t(Ant,p_kPa) % % Helper function to compute saturation temperature using Antoine's equation. % Temperature is in degrees C, and pressure is in mmHg. The input % pressure is in kPa, so we will need to convert. % % Inputs: Ant is a vector of length 3 of Antoine parameters A, B and C % p_kPa is the pressure in kPa % Outputs: res is the temperature in C. % function res = antoine_t(Ant,p_kPa) % un-packing the vector of Antoine parameters a = Ant(1); b = Ant(2); c = Ant(3); % Convert pressure to mmHg p_mmHg = p_kPa*1000*(760/101.325e3); % Calculating the vapor pressure. Temperature is in C and pressure in % mmHg res = -b/(log10(p_mmHg)-a) - c; end % function res = raoult_error_tx(x1,psat_1,psat_2,t_C) % % The error function for solving for T via bubble p calculation using % Raoult's law for binary vapor liquid equilibrium. Everything is in SI % units, except T which is in C. % function res = raoult_error_px(x1,p_kPa,Ant,t_C) % With this T, calculate the vapor pressure 8.13 Application: Raoult’s Law 109 110 111 112 113 114 115 116 end % Calculate the vapor pressure of component 1 and 2 in Pa psat_1 = antoine(Ant(1,:),t_C); psat_2 = antoine(Ant(2,:),t_C); % Converting our pressure from kPa to Pa p_Pa = p_kPa*1000; % Our bubble p error function res = p_Pa - (x1*psat_1+(1-x1)*psat_2); Listing 8.13 hexane_cyclohexane_px_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 % % % % % % function res = hexane_cyclohexane_px_2(p_kPa, x1) Function to compute T and y1 using Raoult's law for the binary system hexane(1)/cyclohexane(2) at a given pressure in kPa (p_kPa) and liquid phase mole fraction (x1). The resulting temperature will be in C. function res = hexane_cyclohexane_px_2(p_kPa, x1) % Antoine parameters where T is in units C and p is in units of mmHg. % % Parameters for hexane(1) valid over the range -95 to 235 C Ant_1 = [7.01051,1246.33,232.988]; % Parameters for cyclohexane(2) valid over the range 29 to 280 C Ant_2 = [7.09926,1380.54,246.526]; % % Packing the Antoine parameters to a matrix as used as an input to % raoult_tx Ant = [Ant_1; Ant_2]; % Performing Raoult's law calculation res = raoult_px_2(Ant,p_kPa,x1); end >> VLE_2 = hexane_cyclohexane_px_2(Ant,101.33,0.3) VLE_2 = 0.3814 76.6262 Again, with both methods we get the same answer. So which solution is preferred? Again, this is a difficult question to answer. It is nice to solve sequentially and use fzero. We know that so long as our error function is continuous and changes signs, then it is guaranteed to find a solution. Both of these criteria will be satisfied for this physical problem. But again, if working up the problem the allow you to solve sequentially creates the opportunity for mistakes, then use of fsolve might be preferred. I offer you both solution strategies to allow you to compare. 291 292 Chapter 8 Systems of Equations 8.14 Application: CSTR 8.14.1 CSTR Basics t Non-steady-state behavior is something that you need to worry about when starting-up or shuttingdown a process, or when there is a disturbance in your system. When I first took a course in Mass and Energy Balances, I never appreciated the usefulness of being able to perform a mass balance. We’ll use one here to derive the mole balance design equation for a continuous stirred tank reactor (CSTR). We will assume we have an ideal CSTR operating at steady-state; that is, we will assume the contents are perfectly mixed and their is no accumulation in our system. For this problem we will further consider the case of an isothermal reactor, so we can solve our mole balance independent of our energy balance. It is common to look at cases when this is not the case, which requires solving your mass and energy balance simultaneously. But that will be saved for your Kinetics and Reactor Design course. In the spirit of promoting free texts, when I taught Kinetics and Reactor Design I used the free text “A First Course on Kinetics and Reaction Engineering,” by Carl Lund4 . The text was just being developed when I was an undergraduate student taking the course with Professor Lund, and is the original motivation for this text. I would encourage you to have a look at the text and promote its use. I will also point out the text provides many good MATLAB resources for solving problems in that course. Below is an image of a CSTR which I took from Wikipedia. 4 http://wwwresearch.sens.buffalo.edu/karetext/title/title.shtml 8.14 Application: CSTR 293 Figure 8.2 Cartoon of a CSTR. In a CSTR, reagents are continuously fed into the reactor at a known flow rate and with known temperature and composition. In addition, a product stream is continuously removed from the reactor. If the total mass flowing into the reactor equals the total mass flowing out of the reactor, and if each of the flow rates, compositions, and temperatures does not change with time, the reactor is said to be operating at steady-state. At steady-state, there is no accumulation in our system, and for the isothermal case, the temperature of the inlet equals the temperature of the outlet. For this problem, we will add (if the reaction is endothermic) or remove (if the reaction is exothermic) heat so that the exit inlet and outlet temperature are the same. In our ideal case, we said that the reactor was perfectly mixed. The composition and temperature of the product stream is therefore identical to the contents of the reactor, which correspond to the conditions at which the reaction takes 294 Chapter 8 Systems of Equations place. Let’s write a mole balance for our system for component A: INPUT + GENERATION = OUTPUT + ACCUMULATION We are assuming steady-state operation, so there is no accumulation. Our expression therefore reduces to: INPUT + GENERATION = OUTPUT t This is a homogeneous, bulk phase reaction. The rate of reaction is therefore made intensive by dividing by the volume of the system. If we double the size of the system we expect that rate of generation/consumption to double, so volume is the natural scale factor. However, there are other conventions. A heterogeneous, catalytic reaction takes place at the surface of a solid catalyst. (It is heterogeneous because the catalyst is solid and in the presence of either a liquid or gas.) For this case, the rate of reaction is made intensive by dividing by the surface area of the catalyst. This is again the natural scale factor because if we double the surface area of the catalyst, we expect the rate of generation/consumption to double. Consider an arbitrary species A involved in a chemical reaction. We will assume that the only way a mole of A can be generated/consumed is via a chemical reaction. If we consider here just a single chemical reaction taking place: GENERATION = ν A V r where ν A is the stoichiometic coefficient of A, ν A r is the per unit volume rate of generation/consumption of A, and V is the volume of the system. With this our mole balance design equation becomes: INPUT + ν A V r = OUTPUT Last, we designate our inlet molar flow rate of component A as ṅ 0A and the outlet molar flow rate of component A as ṅ A . This leads to the final expression: ṅ 0A + ν A V r = ṅ A Or more commonly: ν A V r = ṅ A − ṅ 0A A common measure of the reaction progress is the fractional conversion. The fractional conversion of A may be computed as fA = ṅ 0A − ṅ A ṅ 0A It tells us what fraction of A in the inlet has been consumed. 8.14.2 A single reaction example Let’s consider an example problem involving a single reaction. The conversion of A to B (A → B ) takes place in an aqueous solution in an isothermal CSTR. The inlet to the reactor is a 2 M solution of A at 300 K at a total flow rate of 1200 L/hour. (Recall 1 M = 1 mol/L.) Calculate the volume of the reactor necessary to reach 0.8 fractional conversion of A. The reaction is first order in A and the rate coefficient obeys the Arrhenius expression with a preexponential term equal to 2.4 × 108 s−1 and an activation energy of 15.3 kcal/mol. Mathematically this means r = kC A 8.14 Application: CSTR 295 k = k 0 exp[−E / (RT )] k 0 = 2.4 × 108 s−1 E = 15.3kcal/mol Okay, let’s start with some simplifying assumptions. We have a reaction that takes place in the aqueous phase. The initial concentration of A is rather dilute, 2 moles per liter of water. There are approximately 55.35 moles of water per liter, so this corresponds to a mole fraction of just 0.035. And since it is a 1 to 1 reaction (for every mole of B created a mole of A is consumed), the total molar concentration of A and B will remain the same (a small value). Therefore, it is very reasonable to assume that the inlet and outlet volumetric flow rate (V̇ ) are constant and equal to 1200 L/hour. This is a very common approximation. This allows us to do a number of things. First, we can write our inlet and outlet molar flow rates in terms of molar concentrations and volumetric flow rates: ṅ 0A = C A0 V̇ ṅ A = C A V̇ Additionally, we can re-write the fractional conversion using molar concentrations: C 0 −C A fA = A 0 CA In the present problem I asked you to size the reactor. That is, for a given flow rate, find the total reactor volume needed. I just as well could have specified the total volume and asked you to find the inlet flow volume. The problem can be generalized by defining the space time: τ= V V̇ The space time has units of time, and can be thought of as the average time a fluid element remains inside the reactor. Solving with respect to space time is also useful because it will facilitate comparison to a batch reactor or plug flow reactor (PFR). Let us also pause and take note of the units. I have provided you with the general rate expression. If you want a rate expression for the consumption of A or generation of B, you multiply by the appropriate stoichiometric coefficient. Remember stoichiometric coefficients for reactants are negative (they are being consumed) and positive for reactants. So for the rate of consumption of A r A = ν A r = −kC A Likewise, the rate of generation of B would be r B = νB r = kC A t Also note here that having a dilute solution is a main reason we can assume the system is isothermal. There is an excess amount of water present that serves as a heat bath 296 Chapter 8 Systems of Equations Get the idea? The units of the rate will be moles per volume per time. We will need to make sure we are using the same units for moles, volume, and time throughout the problem. The rate constant (k) has the same units as the pre-exponential term (k 0 ) of 1/s. However, the flow rate given in the problem is in L/h. We will need to make sure our units are consistent when you solve. The term in the exponential, E /(RT ) is dimensionless. We are given E in kcal/mol. So unless we convert it, we should use R in units of kcal/(mol K) and T in units of K. We could alternatively convert E to say J/mol and use the only value of R that I remember, R = 8.314 J/(mol K). Additionally, in this problem we have set everything up to solve using A. As a future note, know that you do not have to use A if you do not want to. Likewise, we do not need to solve a mole balance for B in order to determine the molar flow rate of B. The moles of A and B are related by stoichiometry. For every mole of A that is consumed, a mole of B is created in our reaction. Therefore, ṅ 0A − ṅ A = ṅ B − ṅ B0 and since we started with no moles of B ṅ 0A − ṅ A = ṅ B C A0 −C A = C B My very last note is on the rate expression itself. Here we have a relatively simple expression. This is generally not the case. Depending on the proposed mechanism or if we have a reversible reaction, the expression will likely be much more complicated. (No worries, MATLAB does not care!) But the point I would like to make is that you can’t (and never should be expected to) just look at a reaction and know what the form of the rate of reaction should be. The only way is to propose a model (in various ways) and test it by fitting to experimentally generated data. Example 8.3 The conversion of A to B (A → B ) takes place in an aqueous solution in an isothermal CSTR. The inlet to the reactor is a 2 M solution of A at 300 K at a total flow rate of 1200 L/hour. (Recall 1 M = 1 mol/L.) Calculate the volume of the reactor necessary to reach 0.8 fractional conversion of A. The reaction is first order in A and the rate coefficient obeys the Arrhenius expression with a pre-exponential term equal to 2.4 × 108 s−1 and an activation energy of 15.3 kcal/mol. Mathematically this means r = kC A k = k 0 exp[−E / (RT )] k 0 = 2.4 × 108 s−1 E = 15.3kcal/mol 8.14 Application: CSTR 297 Solution: We begin with an isothermal CSTR in which we have as single reaction occurring, A → B . For this system our mole balance design equation is: ν A V r = ṅ A − ṅ 0A We are told that A and B are dilute such that: ṅ A = V̇ C A ṅ 0A = V̇ C A0 where V̇ is the volumetric flow rate which can be assumed constant. Plugging into our mole balance design equation we have: ¡ ¢ ν A V r = V̇ C A −C A0 (8.16) Additionally, we are told that the rate is first order in C A r = kC A where we are provided with the parameters to compute k. In the problem statement we are additionally provided with values of C A0 and V̇ . Plugging the rate expression into eq. (8.16), we find we have a single equation with two unknowns: C A and V . To solve, we need another equation. We are provided with the constraint that the fractional conversion is 0.8: fA = ṅ 0A − ṅ A ṅ 0A = C A0 −C A C A0 (8.17) Equation (8.16) and 8.17 corresponds to a system of two equation with two unknowns which can be solved. Note that eq. (8.17) could be solved first to find C A , and then used in eq. (8.16) to computed V . This would involve an fzero call for each case or you could solve analytically. Alternatively, we can just use fsolve to solve the system of equations. Both work and will yield the same result. I will use the latter approach. Before solving, a few comments on about units. The unit of volume used by V , V̇ , C A and C A0 will be liter (L). The parameters provided in the problem statement use a unit of time of seconds for k and hours for V̇ . A unit conversion will be necessary to make sure both use the same unit of time. Last, the term E / (RT ) is dimensionless and T must be in absolute units. I will convert E to J/mol and use Kelvin for T . Let’s do it! 298 Chapter 8 Systems of Equations Listing 8.14 cstr_single_v.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 % Function to solve for the volume of a CSTR to acheive a fractional % conversion of 0.8. I will hardcode everything for the specific problem % at hannd. You need just call the function to solve. % function res = cstr_single_v() % First we will list the provided parameters % Pre-exponential term, 1/s k0 = 2.4e8; % Activation energy, kcal/mol ea_kcal = 15.3; % Converting the activation energy to J/mol ea_j = ea_kcal*1000*4.184; % Molar gas constant in J/(mol K) rg = 8.314; % Temperature in K t_K = 300; % volumetric flow rate, L/hour v_Lh = 1200; % Converting the volumetric flow rate to L/s v_Ls = v_Lh/3600; % Initial concentration of A, mol/L cA0 = 2; % Fractional conversion fA = 0.8; % Stoichiometric coefficient of A nuA = -1; % Compute the rate constant k = k0*exp(-ea_j/(rg*t_K)); % We need to create an anonymous function to pass to our error % function: v_Ls, cA0, fA, nuA, k cstr_error_func = @(CV) cstr_mole_balance(CV,v_Ls,cA0,fA,nuA,k); % Using fsolve to solve for cA and V. I will use an initial estimate of % cA = 1 M and V = 10 L. If fsolve fails we can try and tweak these. res = fsolve(cstr_error_func,[1,10]); end % Error function corresponding to our mole balance design equation % and fractional conversion. The first argument will be a vector, CV, where % the first element is cA and the second element is v, the two variables we % wish to solve for using fsolve. The others are parameters that will be % passed using an anonymous function. function res = cstr_mole_balance(CV,v_Ls,cA0,fA,nuA,k) % Un-packing cA = CV(1); v = CV(2); % The rate expressions 8.14 Application: CSTR 54 55 56 57 58 59 60 61 62 63 64 end 299 r = k*cA; % Now computing the error functions. % The mole balance re-written as an error function res1 = nuA*v*r-v_Ls*(cA-cA0); res2 = fA - (cA0-cA)/cA0; % Packing up the rate functions res = [res1,res2]; >> sol = cstr_single_v sol = 0.4000 778.3349 We find we need a volume of 778.3349 L. Note that I tried other initial estimates of V , but the final value seemed relatively insensitive to the initial guess. Note, I created a function with no inputs to solve the problem. You will find that I do this a lot. You need just run the function to obtain the solution. However, note that beginning with MATLAB R2016b, you can now add functions to scripts to achieve the same effect. Example 8.4 Next, generalize your solution to the previous example. Assume that the inlet volumetric flow rate was not specified. (But do assume it is large enough to assume the volumetric flow rate is constant.) Solve for and create a plot of the fractional conversion of A as a function of the space time. Solve for the space time required to achieve a fractional conversion of 0.8. In general, as space time increases does fractional conversion increase or decrease? So if one wished to increase the fractional conversion of a CSTR, what should they do to the inlet volumetric flow rate (or the reactor volume if possible)? Solution: Next, we generalize eq. (8.16) to use space time, where τ = V /V̇ . This gives: ν A τr = C A −C A0 (8.18) We have two tasks. First, knowing ν A , k, and C A0 , solve for the required τ to achieve a fractional conversion of 0.8. The solution is virtually the same as the previous example. Second, loop over values of τ, and for each value of τ solve for and plot f A . To accomplish this, with a specified value of τ, we can solve eq. (8.18) for C A , which can then be used to compute the fractional conversion. For this case, we will have a single equation with a single unknown, so the use of fzero will be preferred over fsolve. Note that we could alternatively solve the expression analytically for C A for this simple case. But it is easy enough to solve numerically, and the solution would be the same no matter how complicated the rate expression ever 300 Chapter 8 Systems of Equations became. Following our previous note on units, τ and k will use the same units of time. I will use seconds to solve. However, the resulting time to achieve a fractional conversion of 0.8 is very large in seconds, so I will convert the output to minutes to make the output more readable. Listing 8.15 cstr_single_tau.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 % Function to solve for the space time of a CSTR to acheive a fractional % conversion of 0.8. Also, create a plot of the fractional version versus % space time. I will hardcode everything for the specific problem % at hannd. You need just call the function to solve. % function res = cstr_single_tau() % First we will list the provided parameters % Pre-exponential term, 1/s k0 = 2.4e8; % Activation energy, kcal/mol ea_kcal = 15.3; % Converting the activation energy to J/mol ea_j = ea_kcal*1000*4.184; % Molar gas constant in J/(mol K) rg = 8.314; % Temperature in K t_K = 300; % Initial concentration of A, mol/L cA0 = 2; % Stoichiometric coefficient of A nuA = -1; % Compute the rate constant k = k0*exp(-ea_j/(rg*t_K)); % Using fsolve to solve for cA and tau to acheive a fractional % conversion of 0.8. We will using an anonymous function to pass cA0, % nuA, and k to the error function. cstr_error_func_08 = @(CT) cstr_mole_balance_08(CT,cA0,nuA,k); res = fsolve(cstr_error_func_08,[1,10]); % Converting the computed space time to minutes. res(2) = res(2)/60; % Next, creating the desired plot of fractional conversion versus space % time. % Space time in minutes Tau = linspace(0,240); % Vector to store computed concentrations of A CA = zeros(1,length(Tau)); for i = 1:length(Tau) % Each iteration define an anonymous function to pass Tau(i), k, % nuA, and cA0 to the error function. cstr_error_func = @(cA) cstr_mole_balance(cA,Tau(i),k,nuA,cA0); % For fzero, I will set bounds from 0 to cA0. 8.14 Application: CSTR 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 CA(i) = fzero(cstr_error_func,[0,cA0]); end % Computing the fractional conversion FA = (cA0-CA)./cA0; % Plotting hold on plot(Tau,FA,'-k') % Just for fun, plot our point corresponding to a fractional conversion % of 0.8. plot(res(2),0.8,'ko') xlabel('\tau [min]') ylabel('f_{A}') print('-depsc','cstr_single_tau.eps') end % Error function corresponding to our mole balance design equation % and fractional conversion. The first input variable is a vector of length % 2 containing cA and tau which we will solve for using fsolve. The % remaining variables are parameters that will be passed via an anonymous % function. % function res = cstr_mole_balance_08(CT,cA0,nuA,k) % Un-packing cA = CT(1); tau = CT(2); % The desired fractional conversion fA = 0.8; % The rate expressions r = k*cA; % Now computing the error functions. % The mole balance re-written as an error function res1 = nuA*tau*r-(cA-cA0); res2 = fA - (cA0-cA)/cA0; end % Packing up the rate functions res = [res1,res2]; % Error function corresponding to our mole balance design equation. % In this version, tau will be specified and we will just solve for % cA. The first input variable is cA which we will solve for using fzero, % and the remaining variables are parameters that will be passed via an % anonymous function. % function res = cstr_mole_balance(cA,tau,k,nuA,cA0) % Converting the space time from minutes to seconds t = tau*60; 301 302 Chapter 8 Systems of Equations 103 104 105 106 107 108 109 end % The rate expressions r = k*cA; % Now computing the error functions. % The mole balance re-written as an error function res = nuA*t*r-(cA-cA0); >> sol = cstr_single_tau sol = 0.4000 38.9167 1 0.9 0.8 0.7 fA 0.6 0.5 0.4 0.3 0.2 0.1 0 0 50 100 150 200 250 [min] Figure 8.3 cstr_single_tau We find that as space time increases, fractional conversion increases. The rate of increase is greatest initially and then slows. If one wished to increase the fractional conversion of a CSTR, they can decrease the volumetric flow rate, keeping the volume of reactor constant, to increase the space time. 8.15 Glossary 8.15 Glossary row vector: In MATLAB, a matrix that has only one row. column vector: A matrix that has only one column. transpose: An operation that transforms the rows of a matrix into columns (or the other way around, if you prefer). system of equations: A set of equations written in terms of a set of variables such that the equations are inter-tangled. paragraph: A chunk of code that makes up part of a function, usually with an explanatory comment. unpack: To copy the elements of a vector into a set of variables. pack: To copy values from a set of variables into a vector. rank: An operation that returns the number of linearly independent equations. 303 304 Chapter 8 Systems of Equations 8.16 Exercises Exercise 8.1 A mixture contains n-pentane (1), n-hexane (2), and n-heptane (3) at equal mole fractions (z 1 = z 2 = z 3 ), where the term in parentheses corresponds to the component index we will use. The temperature is 55 ◦ C. The pressure is adjusted so that 75% of the mixture is vapor (V /F = 0.75) while the temperature remains constant at 55 ◦ C. Determine the pressure and compositions of the two phases. At 55 ◦ C the vapor pressure of n-pentane, n-hexane, and n-heptane are P 1sat = 1.903 bar, P 2sat = 0.644 bar, and P 3sat = 0.231 bar, respectively. Figure 8.4 Illustration of our system. To solve, we will use the illustration of the flash drum above to set-up our system of equations to solve. A few notes first. F , L, and V correspond to the inlet flow rate, and outlet liquid and vapor flow rates, respectively. We will solve using the fraction vaporized (V /F ) and the fraction remaining liquid (L/F ). The inlet, liquid- and vapor-phase mole fractions of component i are given by z i , x i , and y i , respectively. Finally, note that specifying the temperature is equivalent to specifying P isat . With that, we are ready to set-up our system of equations. We will end up with a total of 8 equations with 8 unknowns. For our 3 component system, we can write 3 iso-fugacity (equilibrium) expressions, 3 mass balances (or equivalently mole balances since we have no reactions), and two constraints that our liquid and vapor mole fractions sum to 1. Since we have a system of linear alkanes, we can assume Raoult’s law is appropriate for our iso-fugacity expressions. This leads to the following system of equations: y 1 P = x 1 P 1sat y 2 P = x 2 P 2sat y 3 P = x 3 P 3sat z1 F = y 1V + x1 L z2 F = y 2V + x2 L z3 F = y 3V + x3 L 1 = y1 + y2 + y3 1 = x1 + x2 + x3 8.16 Exercises 305 Then to get our final expression, we divide through our mole balances by F , giving: y 1 P = x 1 P 1sat y 2 P = x 2 P 2sat y 3 P = x 3 P 3sat z 1 = y 1 V /F + x 1 L/F z 2 = y 2 V /F + x 2 L/F z 3 = y 3 V /F + x 3 L/F 1 = y1 + y2 + y3 1 = x1 + x2 + x3 This results in a system of 8 equation with 8 unknowns which we can solve. Our 8 unknowns are: y 1 , y 2 , y 3 , x 1 , x 2 , x 3 , L/F and P . When you solve, note that all of the unknowns except P that we will solve for will be between 0 and 1. Also, using our values of P isat in bar, our computed P will also be in bar. Solving, I obtain: y 1 = 0.4055, y 2 = 0.3463, y 3 = 0.2482, x 1 = 0.1167, x 2 = 0.2946, x 3 = 0.5887, L/F = 0.25, and P = 0.5479 bar. Now just think, with this code you can become a design engineer. Exercise 8.2 Reactants A and B can react irreversibly to produce either a desired product, D, or an undesired product, U, as shown in the equations below: A +B → D (1) A +B →U (2) The corresponding rate expressions are given by: ¾ ½ ¡ ¢ −15300J/mol CA r 1 = 1.12 × 102 min−1 exp RT ½ ¾ ¡ ¢ −23700J/mol r 2 = 1.87 × 102 min−1 exp CB RT A liquid feed mixture containing 10 mol A per gallon and 12 mol B per gallon at 350 K is fed to a 25 gallon isothermal CSTR (operating at 350 K) at a rate of 12.5 gallons per minute. What is the conversion of the limiting reagent? What is the outlet selectivity (in mol D per mol U)? The limiting reagent will be the reactant that will be consumed first. Here, for each mol of A consumed a mol of B is consumed. The limiting reagent will therefore be the species with the smallest concentration in the feed. Why would the conversion with respect the limiting reagent be preferred? For this set of reactions, the following general system of mole balance design equations will apply: ¡ ¢ τ ν A,1 r 1 + ν A,2 r 2 = C A −C A0 ¡ ¢ τ νB,1 r 1 + νB,2 r 2 = C B −C B0 ¡ ¢ 0 τ νD,1 r 1 + νD,2 r 2 = C D −C D 306 Chapter 8 Systems of Equations ¡ ¢ 0 τ νU ,1 r 1 + νU ,2 r 2 = CU −CU Exercise 8.3 Please solve the following system of 10 equations with 10 unknowns: 7x 1 + 4x 2 + 3x 3 + 8x 4 + 8x 5 + 4x 6 + 1x 7 + 2x 8 + 5x 9 + 1x 10 =4 4x 2 + 7x 3 + 3x 4 + 3x 5 + 8x 6 + 1x 7 + 8x 8 + 1x 9 + 10x 10 =9 3x 1 + 8x 2 + 7x 3 + 5x 4 + 8x 5 + 6x 6 + 5x 7 + 3x 8 + 2x 9 =2 8x 2 + 2x 3 + 7x 4 + 2x 5 + 5x 6 + 8x 7 + 5x 8 + 9x 9 + 8x 10 =3 1x 1 + 2x 2 + 1x 3 + 9x 4 + 9x 5 + 9x 6 + 9x 7 + 2x 8 + 2x 9 + 8x 10 =1 8x 1 + 5x 2 + 5x 3 + 10x 4 + 3x 5 + 3x 6 + 1x 7 + 6x 8 + 8x 9 + 9x 10 =1 7x 1 + 4x 2 + 10x 3 + 5x 4 + 2x 5 + 8x 6 + 6x 7 + 3x 8 + 5x 9 + 1x 10 =9 3x 1 + 6x 2 + 3x 3 + 1x 4 + 3x 5 + 8x 6 + 5x 7 + 7x 8 + 10x 9 + 4x 10 =6 10x 1 + 7x 2 + 6x 3 + 1x 4 + 6x 5 + 4x 6 + 7x 8 + 1x 9 + 3x 10 =5 8x 2 + 2x 3 + 3x 4 + 5x 5 + 6x 6 + 3x 7 + 7x 8 + 4x 9 + 8x 10 =1 Solving, I obtain the following solution: x 1 = −0.8120 x 2 = −2.3182 x 3 = −8.7617 x 4 = 13.8025 x 5 = −9.2603 x 6 = 4.7159 x 7 = 2.1875 x 8 = 22.3778 x 9 = −8.4912 x 10 = −14.4467 Exercise 8.4 We have seen how we can use fsolve to model binary vapor/liquid equilibrium. In Listing 8.6 we specified temperature and the liquid-phase mole fraction, and solved for the pressure and vapor-phase mole fraction. This was applied to the binary system of hexane(1)/cyclohexane(2). Here, let’s go a step further and construct a px y phase diagram. What do we mean by this? Well, temperature is known and constant. x 1 spans the range 0 to 1. So loop over values of x 1 over the range 0 to 1, and solve for the corresponding y 1 and pressure. Then plot p vs x 1 and p vs y 1 on the same plot. You should connect the points with a line to create a phase-envelope. The more points you use, the smoother your curve. In the table below, I provide reference data from DDBST at 343.15 K. Perform your calculations at this temperature. Then on the same plot, add the reference data as symbols. How do the results compare? To your plot, please add a title with the system name (hexane(1)/cyclohexane(2)) and temperature, axis labels, and a legend to distinguish between your Raoult’s law predictions and the reference data. 8.16 Exercises p/kPa 77.327 81.607 85.673 90.033 94.019 97.965 100.818 307 x1 0.12500 0.25000 0.37500 0.50000 0.62500 0.74200 0.87500 y1 0.17900 0.33600 0.46500 0.59000 0.70800 0.80600 0.90100 Here I have copied our table of Antoine parameters, where T is in ◦ C and p is in mmHg. species hexane cyclohexane cyclohexane A 7.01051 6.85146 7.09926 B 1246.33 1206.47 1380.54 C 232.988 223.136 246.526 Tmin –95 7 29 Tmax 235 81 280 Exercise 8.5 Continuing from the previous exercise, in Listing 8.10 we specified pressure and the liquid-phase mole fraction, and solved for the temperature and vapor-phase mole fraction. This was applied to the binary system of hexane(1)/cyclohexane(2). Here, let’s go a step further and construct a T x y phase diagram. What do we mean by this? Well, pressure is known and constant. x 1 spans the range 0 to 1. So loop over values of x 1 over the range 0 to 1, and solve for the corresponding y 1 and temperature. Then plot T vs x 1 and T vs y 1 on the same plot. You should connect the points with a line to create a phase-envelope. The more points you use, the smoother your curve. In the table below, I provide reference data from DDBST at 101.33 kPa. Perform your calculations at this pressure. Then on the same plot, add the reference data as symbols. How do the results compare? To your plot, please add a title with the system name (hexane(1)/cyclohexane(2)) and pressure, axis labels, and a legend to distinguish between your Raoult’s law predictions and the reference data. A table of Antoine parameters is provided in the previous exercise. T /K 353.87 352.65 352.15 351.35 350.50 349.90 349.55 348.85 348.55 347.85 346.85 346.05 345.20 344.55 344.00 343.55 343.20 341.85 x1 0.00000 0.08200 0.12100 0.16700 0.22600 0.27000 0.30000 0.35300 0.38400 0.43800 0.52300 0.59100 0.67100 0.74000 0.79700 0.84000 0.87700 1.00000 y1 0.00000 0.12100 0.16700 0.22600 0.30000 0.35300 0.38400 0.43800 0.46800 0.52300 0.60200 0.67100 0.74000 0.79200 0.84000 0.87700 0.90400 1.00000 308 Chapter 8 Systems of Equations T x y phase-diagrams are very important for the design of distillation columns. Columns are typically operated at a constant pressure (typically near atmospheric), and a temperature gradient is present from the top to the bottom of the column. It can be extremely useful to visualize such processes using what is called a McCabe-Thiele diagram. In a McCabe-Thiele diagram, we take all of our data from our T x y, and plot y vs x. Connect the points to create a loci of equilibrium compositions. Then last, as a reference, construct a y = x line from x = y = 0 to x = y = 1. Give it a try and label your plot. Exercise 8.6 In Exercises 8.4 and 8.5 we constructed px y and T x y phase diagrams for a system obeying Raoult’s law. Raoult’s law makes three important assumptions: 1. The liquid phase is an ideal solution 2. The vapor phase is an ideal gas 3. We are well removed from the critical point such that f i0 ≈ p isat Again, if you do not know what this means, no worries, just be sure to sign-up for Chemical Engineering Thermodynamics before you graduate. The second and third assumptions are generally very good so long as we are at low pressures. The first assumption is generally where Raoult’s law breaks down. We account for solution phase non-ideality using activity coefficients (Lewis/Randall or Raoult’s law normalized), which leads to the “modified” Raoult’s law expressions. A binary system at vapor/liquid equilibrium is governed by the following system of non-linear equations: y 1 p = γ1 x 1 p 1sat y 2 p = γ2 x 2 p 2sat where γ1 and γ2 are the activity coefficient of component 1 and 2, respectively. The pure component vapor pressure is only a function of temperature, and is commonly computed using Antoine’s equation: log10 p sat = A − B T +C The activity coefficient is, in theory, a function of temperature, pressure, and composition. However, for condensed phase systems (liquids) the pressure dependence can typically be safely ignored. For this problem, this means that γ1 and γ2 are functions only temperature and composition. As used in Exercise 7.4, one way to model the activity coefficient is using the NRTL (non-random two-liquid) equation: · µ ln γ1 = x 22 τ21 G 21 x 1 + x 2G 21 ¶2 · µ τ12 G 12 x 2 + x 1G 12 ¶2 ln γ2 = x 12 + + τ12G 12 (x 2 + x 1G 12 )2 τ21G 21 (x 1 + x 2G 21 )2 where τ12 = and ¸ g 12 − g 22 g 21 − g 11 τ21 = RT RT ¸ 8.16 Exercises 309 G 12 = exp (−α12 τ12 ) G 21 = exp (−α21 τ21 ) where R is the molar gas constant. Note that the terms τ12 and τ21 are dimensionless. In theory, the parameter g i j is an energy parameter characteristic of the i - j molecular interaction, and αi j is the non-randomness parameter corresponding to the inverse of the coordination number. For this problem, we will build-upon our code from Exercises 8.4 and 8.5 to construct px y and T x y phased diagrams for a binary system using the modified Raoult’s law expressions. We will apply this to the binary system of ethyl acetate(1)/ethanol(2) studied in Exercise 7.4. For this system The vapor pressure of ethyl acetate (p 1sat ) and ethanol (p 2sat ) can be computed using Antoine’s equation with parameters provided in the table below from DDBST. With these parameters, the vapor pressure in units of mmHg, and T is the temperature in ◦ C. compound ethyl acetate ethanol A 7.10179 8.20417 B 1244.95 1642.89 C 217.9 230.3 Tmin [◦ C] 16 –57 Tmax [◦ C] 76 80 As in Exercise 7.4 we will use the NRTL equation to model the activity coefficients with the following set of parameters: α12 = α21 = 0.296 g 12 − g 22 = 72.347 J/mol g 21 − g 11 = 3682.7378 J/mol 1. Construct a px y phase diagram for the binary system ethyl acetate(1)/ethanol(2) at a temperature of 313.15 K. That is, plot p vs x 1 and p vs y 1 in the same plot. In your plot compare to the reference data freely available in the Dortmund Data Bank. 5 2. Construct a T x y phase diagram for the binary system ethyl acetate(1)/ethanol(2) at a pressure of 101.33 kPa. That is, plot T vs x 1 and T vs y 1 in the same plot. In your plot compare to the reference data freely available in the Dortmund Data Bank. 6 You should additionally construct a McCabe-Thiele (y 1 vs x 1 ) diagram. 5 http://www.ddbst.com/en/EED/VLE/VLE%20Ethanol%3BEthyl%20acetate.php 6 Note that the NRTL parameters we are using were regressed to reference data at 313.15 K. Using them at a different temperature is an extrapolation. While NRTL does in theory account for temperature dependence, you should in general proceed with caution. 310 Chapter 8 Systems of Equations Exercise 8.7 (Two attempts of Professor Paluch discussing how to set-up the problem can be found here and here.) Now that we have a grasp of CSTR basics, let’s consider the case of multiple reactions. We will consider two scenarios. First we will have the case of parallel reactions. For this case, the reactant (or reactants) is consumed by two or more different reactions. Let’s consider the case: A→B (P.1) A →C (P.2) The second case will be series reactions, in which the reactant (or reactants) form an intermediate product which reacts further to form another product. For our exercise we will consider the case: A→B (S.1) B →C (S.2) In both cases, B will be our desired product and C will be an undesired product. When quantifying reaction progress for the case of series and parallel reactions, a common reaction progress variable is the selectivity of the desired relative to the undesired product. Here we define the selectivity of B relative to C (or desired product relative to undesired product) as nB C B S B /C = = nC C C How does our mole balance design equation change? Well, before when we had a single reaction we had r A = νA r Now, we just need to sum over each reaction. We will use a subscript 1 to correspond to reaction 1, and a subscript 2 to correspond to reaction 2 in both the parallel (P) and series (S) case. (Note that both the rate r and stoichiometic coefficient ν A will depend on the reaction number. Also note that it is possible to have a stoichiometric coefficient of 0.) This gives the net rate of reaction for the parallel and series case of: r A = ν A,1 r 1 + ν A,2 r 2 For this problem, we need to know both n B and nC (or C B and CC ). Remember for a single reaction the number of moles of each species is related by stoichiometry. This will not be the case here with our multiple reactions. We will therefore have a system of equations; a mole balance design equation for component B and C, and since our rate expression is dependent on A, solve for A too. For both the parallel and series reaction case we have: r A = ν A,1 r 1 + ν A,2 r 2 r B = νB,1 r 1 + νB,2 r 2 rC = νC ,1 r 1 + νC ,2 r 2 For reaction 1, use the same parameters as before, namely, r 1 = k 1C A k 1 = k 0,1 exp[−E 1 /(RT )] k 0,1 = 2.4 × 108 s−1 8.16 Exercises 311 E 1 = 15.3kcal/mol For reaction 2 for the parallel reaction case (P.2), r 2 = k 2C A k 2 = k 0,2 exp[−E 2 /(RT )] E 2 = 15.3kcal/mol And for reaction 2 for the series reaction case (S.2), r 2 = k 2C B k 2 = k 0,2 exp[−E 2 /(RT )] E 2 = 15.3kcal/mol For the parallel and series reactions described, I would like you to plot the composition of each species versus the space time, the fractional conversion of A versus space time, and the selectivity of B relative to C versus time. Consider the limits of very large and very small space times when you think about selectivity. For both the parallel and series case, I would like you to consider: k 0,2 = 0.1k 0,1 , k 0,2 = k 0,1 , and k 0,2 = 10k 0,1 . Compare the behavior of parallel and series reactions, and discuss how you might “optimize” your reactor in both cases. Please summarize your findings in a short report supported with plots. Please note that you need to be careful when plotting selectivity. Component C is a product, where initially only A is present. For the series case, B is the intermediate product. Therefore, at very small times, you will have a very small amount of B present, and a very very very small amount of C present. This can cause the value of the selectivity to be very large. (Remember in the limit that the moles of C goes to zero, the selectivity goes to infinity!) Once you get a feel for the range of values, you might consider setting the y-axis limits. Or alternatively, you might consider excluding values are very small space times. Chapter 9 Ordinary Differential Equations In Chapter 9 we learn how to numerically solve first order initial value ordinary differential equations. Then in Chapter 10 we will solve systems of first order ordinary differential equations, and in Chapter 11 higher order differential equations. By the end of this chapter you will be able to: • Provide a basic explanation of how to numerically solve initial value ODEs (Euler) • Exhibit ability to construct “rate” functions • Apply ode45 to solve initial value ODEs If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 9.1 Differential equations A differential equation (DE) is an equation that describes the derivatives of an unknown function. “Solving a DE” means finding a function whose derivatives satisfy the equation. For example, when bacteria grow in particularly bacteria-friendly conditions, the rate of growth at any point in time is proportional to the current population. What we might like to know is the population as a function of time. Toward that end, let’s define f to be a function that maps from time, t , to population y. We don’t know what it is, but we can write a differential equation that describes it: df =af (9.1) dt where a is a constant that characterizes how quickly the population increases. Notice that both sides of the equation are functions. To say that two functions are equal is to say that their values are equal at all times. In other words: 313 314 Chapter 9 Ordinary Differential Equations ∀t : df (t ) = a f (t ) dt This is an ordinary differential equation (ODE) because all the derivatives involved are taken with respect to the same variable. If the equation related derivatives with respect to different variables (partial derivatives), it would be a partial differential equation. This equation is first order because it involves only first derivatives. If it involved second derivatives, it would be second order, and so on. This equation is linear because each term involves t , f or d f /d t raised to the first power; if any of the terms involved products or powers of t , f and d f /d t it would be nonlinear. Linear, first order ODEs can be solved analytically; that is, we can express the solution as a function of t . This particular ODE has an infinite number of solutions, but they all have this form: f (t ) = be at For any value of b, this function satisfies the ODE. If you don’t believe me, take its derivative and check. If we know the population of bacteria at a particular point in time, we can use that additional information to determine which of the infinite solutions is the (unique) one that describes a particular population over time. For example, if we know that f ((0) = 5 billion cells, then we can write f (0) = 5 = be a0 and solve for b, which is 5. That determines what we wanted to know: f (t ) = 5e at The extra bit of information that determines b is called the initial condition (although it isn’t always specified at t = 0). So to put it all together, we just solved a linear, first order, initial value ODE. Unfortunately, most interesting physical systems are described by nonlinear DEs, most of which can’t be solved analytically. The alternative is to solve them numerically. In this chapter we will solve first order, initial value ODEs. Then in a couple of chapter we will solve second (and higher) order, initial value ODEs. If you would like to learn more about DEs, the “Open Textbook Library” contains a number of excellent, freely available textbooks on the subject.1 For a quick introduction or refresher geared toward engineering students, I would recommend “Notes on Diffy Qs: Differential Equations for Engineers”, by Jirí Lebl which is actively updated and under development, and is freely available. 9.2 Euler’s method 315 9.2 Euler’s method The simplest numerical method for ODEs is Euler’s method, named after the Swiss mathematician Leonhard Paul Euler (1707–1783). If you need help getting excited about Euler’s method, watch the clip from the motion picture “Hidden Figures” at this link. Here’s a test to see if you are as smart as Euler. Let’s say that you arrive at time t and measure the current population, y, and the rate of change, r . What do you think the population will be after some period of time ∆t has elapsed? If you said y + r ∆t , congratulations! You just invented Euler’s method. This estimate is based on the assumption that r is constant, but in general it’s not, so we only expect the estimate to be good if r changes slowly and ∆t is small. That is, we are assuming y changes linearly over the range t 0 to t 0 + ∆t so that its rate of change (i.e., its derivative) is constant. But let’s assume (for now) that the ODE we are interested in can be written so that df (t ) = g (t , y) dt where g is some function that maps (t , y) onto r ; that is, given the time and current population, it computes the rate of change. Then we can advance from one point in time to the next using these equations: Tn+1 = Tn + ∆t (9.2) F n+1 = F n + g (t , y) ∆t (9.3) Here {Ti } is a sequence of times where we estimate the value of f , and {F i } is the sequence of estimates. For each index i , F i is an estimate of f (Ti ). The interval ∆t is called the time step. Assuming that we start at t = 0 and we have an initial condition f (0) = y 0 (where y 0 denotes a particular, known value), we set T1 = 0 and F 1 = y 0 , and then use Equations 9.2 and 9.3 to compute values of Ti and F i until Ti gets to the value of t we are interested in. If the rate doesn’t change too fast and the time step isn’t too big, Euler’s method is accurate enough for most purposes. One way to check is to run it once with time step ∆t and then run it again with time step ∆t /2. If the results are the same, they are probably accurate; otherwise, cut the time step again. Euler’s method is first order, which means that each time you cut the time step in half, you expect the estimation error to drop by half. With a second-order method, you expect the error to drop by a factor of 4; third-order drops by 8, etc. The price of higher order methods is that they have to evaluate g more times per time step. After we have solved a few ODE’s using MATLAB’s built-in functionality, we will revisit Euler’s method and see it in action in Section 9.10. 1 https://open.umn.edu/opentextbooks/subjects/mathematics t If you think of our linear interpolation example (or a first order Taylor series expansion), this is accurate if the function is linear over the range ∆t . As ∆t becomes smaller and smaller, all functions will look more and more linear over the range. 316 Chapter 9 Ordinary Differential Equations 9.3 Another note on notation There’s a lot of math notation in this chapter so I want to pause to review what we have so far. Here are the variables, their meanings, and their types: Name Meaning Type t ∆t y r f time time step population rate of change The unknown function specified, implicitly, by an ODE. The first time derivative of f A “rate function,” derived from the ODE, that computes rate of change for any t , y. a sequence of times, t , where we estimate f (t ) a sequence of estimates for f (t ) scalar variable scalar constant scalar variable scalar variable function t → y d f /d t g T F function t → r function t , y → r sequence sequence So f is a function that computes the population as a function of time, f (t ) is the function evaluated at a particular time, and if we assign f (t ) to a variable, we usually call that variable y. Similarly, g is a “rate function” that computes the rate of change as a function ¡ ¢ of time and population. If we assign g t , y to a variable, we call it r . d f /d t is the first derivative of f , and it maps from t to a rate. If we assign d f /d t (t ) to a variable, we call it r . It is easy to get d f /d t confused with g , but notice that they are not even the same type. g is more general: it can compute the rate of change for any (hypothetical) population at any time; d f /d t is more specific: it is the actual rate of change at time t , given that the population is f (t ). This still might be a little confusing. No worries, when we start to work examples, this should all get resolved. 9.4 ode45 t If you are using GNU Octave, you will need to install the “ode” package. You will then need to add pkg load ode to your scripts or execute it from the Command Window before using ode45. If you are unsure if you currently have the ode package installed, execute pkg list from the Command Window . A limitation of Euler’s method is that the time step is constant from one iteration to the next. But some parts of the solution are harder to estimate than others; if the time step is small enough to get the hard parts right, it is doing more work than necessary on the easy parts. The ideal solution is to adjust the time step as you go along. Methods that do that are called adaptive, and one of the best adaptive methods is the Dormand-Prince pair of Runge-Kutta formulas. You don’t have to know what that means, because the nice people at Mathworks have implemented it in a function called ode45. The ode stands for “ordinary differential equation [solver];” the 45 indicates that it uses a combination of 4th and 5th order formulas. 9.4 ode45 317 In order to use ode45, you have to write a MATLAB function that evaluates g as a function of t and y. As an example, suppose that the rate of population growth for rats depends on the current population and the availability of food, which varies over the course of the year. The governing equation might be something like df ((t ) = a f (t ) [1 + sin (ωt )] dt (9.4) where t is time in days and f (t ) is the population at time t . a and ω are parameters. A parameter is a value that quantifies a physical aspect of the scenario being modeled. For example, in Example 7.2 we used parameters rho and r to quantify the density and radius of a duck. Parameters are often constants, but in some models they vary in time. In this example, a characterizes the reproductive rate, and ω is the frequency of a periodic function that describes the effect of varying food supply on reproduction. This equation specifies a relationship between a function and its derivative. In order to estimate values of f numerically, we have to transform it into a rate function. The first step is to introduce a variable, y, as another name for f (t ) df (t ) = a y [1 + sin (ωt )] dt This equation means that if we are given t and y, we can compute d f /d t (t ), which is the rate of change of f . The next step is to express that computation as a function called g : ¡ ¢ g t , y = a y [1 + sin (ωt )] Writing the function this way is useful because we can use it with Euler’s method or ode45 to estimate values of f . All we have to do is write a MATLAB function that evaluates g . Here’s what that looks like using the values a = 0.01 and ω = 2π/365 (one cycle per year): Listing 9.1 rats.m 1 2 3 4 5 function res = rats(t, y) a = 0.01; omega = 2 * pi / 365; res = a * y * (1 + sin(omega * t)); end You can test this function from the Command Window by calling it with different values of t and y; the result is the rate of change (in units of rats per day): >> r = rats(0, 2) r = 0.0200 t Our bacteria growth equation (eq. 9.1) and rat growth equation (eq. 9.4) should look very similar. In the rat problem we multiply a f by a term that oscillates with t to model the change in food supply with seasons. 318 Chapter 9 Ordinary Differential Equations So if there are two rats on January 1, we expect them to reproduce at a rate that would produce 2 more rats per hundred days. But if we come back in April, the rate has almost doubled: >> r = rats(120, 2) r = 0.0376 Since the rate is constantly changing, it is not easy to predict the future rat population, but that is exactly what ode45 does. Here’s how you would use it: t Something we will do in >> ode45(@rats, [0, 365], 2) The first argument is a handle for the function that computes g , the rate. The second argument is the interval we are interested in, one year. The third argument is the initial population, f (0) = 2. When you call ode45 without assigning the result to a variable, MATLAB displays the result in a figure. Giving the figure a title and labeling the axis: >> title('rats') >> xlabel('day') >> ylabel('Number of rats') rats 80 70 60 Number of rats later examples, you might store the time interval and the initial condition to variables, say t_range and ic, to clarify your code. 50 40 30 20 10 0 0 50 100 150 200 250 300 350 400 day Figure 9.1 ode45(@rats,[0,365],2) The x-axis shows time in days, with our data spanning the range 0 to 365 days; the y-axis shows the rat population, which starts at 2 and grows to almost 80. The 9.5 Multiple output variables rate of growth is slow in the winter and summer, and faster in the spring and fall, but it also accelerates as the population grows. Within this chapter I will define parameters within the rate function M-file. However, remember that this is a case where the use of anonymous functions may be advantageous. If we wanted to look at the effect of the parameters a or ω on the solution, we would update their value in the rate function, save, and then re-run. We could alternatively create a general rate function that takes as input variables the parameters a and ω: Listing 9.2 rats_general.m 1 2 3 4 5 function res = rats_general(t, y, a, omega) %a = 0.01; %omega = 2 * pi / 365; res = a * y * (1 + sin(omega * t)); end This rate function can not be used directly with ode45 since it expects a (rate) function with input variables of just t and y. We can overcome this limitation using an anonymous function: >> >> >> >> a = 0.01; omega = 2 * pi / 365; rats_gen = @(t,y) rats_general(t,y,a,omega); ode45(rats_gen, [0, 365], 2) In doing so we find we obtain the same result. Now if you wished to change the value of one of the parameters, we could update its value in the Command Window , re-define rats_gen, and then re-solve. 9.5 Multiple output variables ode45 is one of many MATLAB functions that return more than one output variable. The syntax for calling it and saving the results is >> [T, Y] = ode45(@rats, [0, 365], 2); The first return value is assigned to T; the second is assigned to Y. Each element of T is a time, t , where ode45 estimated the population; each element of Y is an estimate of f (t ). The variables T and Y are column vectors. If you assign the output values to variables, ode45 doesn’t draw the figure; you have to do it yourself: >> plot(T, Y, 'bo-') 319 320 Chapter 9 Ordinary Differential Equations >> title('rats') >> xlabel('day') >> ylabel('Number of rats') The resulting plot is the same as fig. 9.1. In the plot you will notice that the space between the points is not quite even. They are closer together at the beginning of the interval and farther apart at the end. To see the population at the end of the year, you can display the last element from each vector: >> [T(end), Y(end)] ans = 365.0000 76.9530 end is a special word in MATLAB; when it appears as an index, it means “the index of the last element.” You can use it in an expression, so Y(end-1) is the second-to-last element of Y. How much does the final population change if you double the initial population? How much does it change if you double the interval to two years? How much does it change if you double the value of a? 9.6 Analytic or numerical? When you solve an ODE analytically, the result is a function, f , that allows you to compute the population, f (t ), for any value of t . When you solve an ODE numerically, you get two vectors. You can think of these vectors as a discrete approximation of the continuous function f : “discrete” because it is only defined for certain values of t , and “approximate” because each value F i is only an estimate of the true value f (t ). So those are the limitations of numerical solutions. The primary advantage is that you can compute numerical solutions to ODEs that don’t have analytic solutions, which is the vast majority of nonlinear ODEs. If you are curious to know more about how ode45 works, you can modify rats to display the points, (t , y), where ode45 evaluates g . Here is a simple version: t Later in this chapter we will see how we can make our numerical solution behave like an analytic solution, and how we can use fzero to find the time when we have a specific value. Later, we will additionally see how we can use MATLAB’s interpolating function, interp1, along with ode45 too. Armed with these tricks, you might Listing 9.3 rats_2.m forget about analytical solutions all together. 1 function res = rats_2(t, y) 2 3 4 5 6 end plot(t, y, 'rx') a = 0.01; omega = 2 * pi / 365; res = a * y * (1 + sin(omega * t)); Each time rats_2 is called, it plots one data point; in order to see all of the data points, you have to use hold on. To make the resulting plot clear and readable, 9.7 What can go wrong? 321 we will consider the shorter range of 0 to 70 days. >> >> >> >> clf hold on [T, Y] = ode45(@rats_2, [0, 70], 2); plot(T,Y,'bo') 6 5.5 5 4.5 4 3.5 3 2.5 2 0 10 20 30 40 50 60 70 Figure 9.2 ode45(@rats_2,[0, 70],2) The red × show the points where ode45 called rats. The blue ◦ show where ode45 estimates f (t ). Notice that ode45 typically evaluates g several times for each estimate. This allows it to improve the estimates, for one thing, but also to detect places where the errors are increasing so it can decrease the time step (or the other way around). 9.7 What can go wrong? Don’t forget the @ on the function handle. If you leave it out, MATLAB treats the first argument as a function call, and calls rats without providing arguments. >> ode45(rats, [0,365], 2) Error using rats (line 4) Not enough input arguments. Again, the error message is confusing, because it looks like the problem is in rats. You’ve been warned! The exception to this is if you use an anonymous function t This is similar to the restriction of the number of input variables that can be passed that we saw previously with fzero and fsolve. We have already seen that we can get around this limitation using anonymous functions. Later we will see how we could use nested functions too. 322 Chapter 9 Ordinary Differential Equations to pass additional parameters, where an anonymous function is already of type function handle. Also, remember that the function you write will be called by ode45, which means it has to have the signature ode45 expects: it should take two input variables, t and y, in that order, and return one output variable, r. If you are working with a rate function like this for our bacteria growth problem (eq. 9.1): g (t , y) = a y You might be tempted to write this: 1 2 3 4 function res = rate_func(y) a = 0.1; res = a * y; end % WRONG But that would be wrong. So very wrong. Why? Because when ode45 calls rate_func, it provides two arguments. If you only take one input variable, you’ll get an error. So you have to write a function that takes t as an input variable, even if you don’t use it. 1 2 3 4 function res = rate_func(t, y) a = 0.1; res = a * y; end % RIGHT Another common error is to write a function that doesn’t make an assignment to the output variable. If you write something like this: 1 2 3 4 5 function res = rats(t, y) a = 0.01; omega = 2 * pi / 365; r = a * y * (1 + sin(omega * t)); end And then call it from ode45, you get % WRONG 9.8 Stiffness 323 >> ode45(@rats, [0,365], 2) Error using feval Undefined function 'rats' for input arguments of type 'double'. Error in odearguments (line 87) f0 = feval(ode,t0,y0,args{:}); % ODE15I sets args{1} to yp0. Error in ode45 (line 113) [neq, tspan, ntspan, next, t0, tfinal, tdir, y0, f0, odeArgs, odeFcn, ... This might be a scary message, but if you read the first line and ignore the rest, you’ll get the idea. Yet another mistake that people make with ode45 is leaving out the brackets on the second argument. In that case, MATLAB thinks there are four arguments, and you get >> ode45(@rats, 0, 365, 2) Error using odearguments (line 21) When the first argument to ode45 is a function handle, the tspan argument must have at least two elements. Error in ode45 (line 113) [neq, tspan, ntspan, next, t0, tfinal, tdir, y0, f0, odeArgs, odeFcn, ... Again, if you read the first line, you should be able to figure out the problem (tspan stands for “time span”, which we have been calling the interval). 9.8 Stiffness There is yet another problem you might encounter, but if it makes you feel better, it might not be your fault: the problem you are trying to solve might be stiff.2 I won’t give a technical explanation of stiffness here, except to say that for some problems (over some intervals with some initial conditions) the time step needed to control the error is very small, which means that the computation takes a long time. Here’s one example: df = f 2− f 3 dt If you solve this ODE with the initial condition f (0) = δ over the interval from 0 to 2/δ, with δ = 0.01, you should see something like this: 2 The following discussion is based partly on an article from Mathworks available at https: //www.mathworks.com/company/newsletters/articles/stiff-differential-equations.html t Computing speed and the efficiency of MATLAB keeps improving, so this example might not be too bad on your computer. Note, when MATLAB is struggling, a Stop icon will show up in the bottom left corner of the plot window. You can click it to kill the process. You can also click Ctrl + c from the command line to kill a process. You may notice Stop buttons in a few plots in this chapter where we try computationally intensive processes. 324 Chapter 9 Ordinary Differential Equations Listing 9.4 stiff.m 1 2 3 function res = stiff(t, y) res = y^(2)-y^(3); end >> delta = 0.01; >> ode45(@stiff, [0,2/delta], delta) 1.2 1 0.8 0.6 0.4 0.2 0 0 50 100 150 200 Figure 9.3 ode45(@stiff,[0, 2/delta] delta) t Again, when I first taught this course in Spring 2017 the calculation was very slow. Now it is not and ode45 currently works perfectly fine. After the transition from 0 to 1, the time step is very small and the computation goes slowly. For smaller values of δ, the situation is even worse. In this case, the problem is easy to fix: instead of ode45 you can use ode23s, an ODE solver that tends to perform well on stiff problems (that’s what the “s” stands for). In general, if you find that ode45 is taking a long time, you might want to try one of the stiff solvers. It won’t always solve the problem, but if the problem is stiffness, the improvement can be striking. Additional information about solving ODEs with MATLAB and the different available ODE solvers can be found in the MATLAB article “Choose an ODE Solver”. But I emphasize that ode45 should work for most of the problems you will encounter, and should be your default ODE solver. You may also find the the MATLAB articles “Solve Nonstiff ODEs” and “Solve Stiff ODEs” useful. 9.9 Examples 325 9.9 Examples The best way to get comfortable using ode45 is to solve some problems. And the best problems to practice on are ones that you know the answer to. In addition to the examples here, for more practice I would encourage you to solve some of the example problems with analytical solutions provided in “Notes on Diffy Qs: Differential Equations for Engineers”. Example 9.1 (Link to screen cast with accompanying M-files.) Pretend you dropped an object from a high stationary platform (or from an airplane). Let’s model the downward velocity of the object as a function of time. If frictional resistance is ignored, we have ma(t ) = m dv (t ) = mg dt (9.5) where a is the acceleration of the object, v is the downward velocity, t is time, g is the acceleration due to gravity, and m is the mass. If we account for the frictional force on the object due to air, which acts opposite in direction to g , and we assume that the frictional force is proportional to v, performing a force balance we have ma(t ) = m dv (t ) = mg − γv(t ) dt (9.6) where γ is our coefficient of friction. (We previously included m in our expression to facilitate the writing of our force balance.) Dividing through by m and letting k = γ/m, we have dv (t ) = g − kv(t ) dt (9.7) subject to the initial condition v(0) = 0 (9.8) We could solve this expression using separation of variables and obtain the analytic result v(t ) = ´ g³ 1 − e −kt k (9.9) Let’s solve the differential equation numerically and compare to the analytic solution. To solve, let g = 9.81 m/s2 and k = 0.5 s−1 . To solve, let’s first create our function. We start by re-writing the expression replacing v(t ) with y 326 Chapter 9 Ordinary Differential Equations dv (t ) = g − k y dt Next, replace dv d t (t ) with (9.10) g (t , y) g (t , y) = g − k y (9.11) Careful not to confuse g (t , y) with parameter g . We are now set to write our function and solve over the range 0 to 20 seconds. Listing 9.5 free_fall.m 2 3 4 5 function res = free_fall(t, y) k=0.5; g=9.81; res = g-k*y; end >> >> >> >> >> [T,Y] = ode45(@free_fall, [0,20], 0); hold on plot(T,Y,'ko') xlabel('t/s') ylabel('position/m') 20 18 16 14 position/m 1 12 10 8 6 4 2 0 0 5 10 15 t/s Figure 9.4 ode45(@free_fall, [0,20], 0) 20 9.9 Examples 327 Okay, and now let’s compare to the analytic solution. Listing 9.6 free_fall_soln.m 2 3 4 5 function res = free_fall_soln(t) k = 0.5; g = 9.81; res = (g/k)*(1-exp(-k*t)); end >> fplot(@free_fall_soln,[0,20],'-r') 20 18 16 14 position/m 1 12 10 8 6 4 2 0 0 5 10 15 20 t/s Figure 9.5 Comparing the numerical and analytic solution. A perfect match! This is also a cool problem because after about 10 s we see the velocity appears to plateau out at a constant value. When the velocity becomes constant, the acceleration is 0 and we call this the terminal velocity. This is what makes skydiving so much fun; if the acceleration is 0, the net force is also 0. As an aside, while I created a separate M-file for free_fall_soln, this is a perfect case when using an anonymous function could be convenient. >> >> >> >> k = 0.5; g = 9.81; free_fall_soln = @(t) (g/k)*(1-exp(-k*t)); fplot(free_fall_soln,[0,20],'-r'); 328 Chapter 9 Ordinary Differential Equations Example 9.2 (Link to screen cast with accompanying M-file.) The first differential equation I ever remember solving was the draining of a tank. I also remember solving this problem when I taught Fluid Mechanics. Imagine we have a cylindrical tank filled with a liquid. Then at time t = 0 a valve is opened at the bottom of the tank allowing the liquid to drain. We can perform a mass balance on the tank just like in your Mass and Energy Balance course. rate of accumulation − rate of consumption = flow in − flow out (9.12) Which for our case reduces to rate of accumulation = −flow out (9.13) dm (t ) = −ṁ(t ) dt (9.14) where m is the total mass of fluid in the tank, t is time, and ṁ is the mass flow rate of fluid out of the tank. Assuming constant uniform density (ρ) ¡ ¢ d ρV dt (t ) = −ρQ(t ) dV (t ) = −Q = −av(t ) dt (9.15) (9.16) where in the second expression we have canceled out ρ, and Q is the volumetric flow rate which we can relate to the velocity of the fluid at the exit (v) and the area of the hole through which the fluid is draining. Performing an energy balance, we can relate the kinetic energy of the fluid at the exit to the potential energy of the fluid at the top of the tank 1 mg h = mv 2 2 (9.17) where g is the acceleration due to gravity and h is the height of the fluid in the tank. Solving for v ¡ ¢1/2 v = 2g h (9.18) Before substituting back in, let’s relate V to h. For a cylindrical tank of constant cross sectional area, V = Ah, where A is the cross sectional area. Since A is constant, it can be taken out of the differential. This results in 9.9 Examples 329 A ¡ ¢1/2 ¡ ¢1/2 dh h(t )1/2 = −a 2g (t ) = −a 2g h(t ) dt ¡ ¢1/2 −a 2g dh (t ) = h(t )1/2 dt A (9.19) (9.20) If we were to solve analytically using separation of variables, subject to the initial condition h(0) = h 0 , the result is h(t ) = µp h0 − k t 2 ¶2 (9.21) ¡ ¢1/2 where k = a 2g /A. Let’s solve numerically. Start by replacing h(t ) with y ¡ ¢1/2 −a 2g dh (t ) = y 1/2 dt A Next, replace dh d t (t ) with (9.22) g (t , y) g (t , y) = ¡ ¢1/2 −a 2g A y 1/2 (9.23) We can now write our function Listing 9.7 tank_drain.m 1 2 3 4 5 6 7 8 function res = tank_drain(t,y) %a = 10/(100^(2)); % 10 cm^2 %A = 5; % 5 m^2 %g = 9.81; %k = a*sqrt(2*g)/A; k = 0.4; res = -k*sqrt(y); end In writing the function, I have lumped all of the parameters into k and have set its value to 0.4 for the purpose of this problem. Solving >> >> >> >> >> hold on [T,Y] = ode45(@tank_drain, [0,20], 20); plot(T,Y,'ko') xlabel('time') ylabel('height') 330 Chapter 9 Ordinary Differential Equations 20 18 16 14 height 12 10 8 6 4 2 0 0 5 10 15 20 time Figure 9.6 ode45(@tank_drain, [0,20], 20) Where the last input to ode45 is our initial condition h(0) = 20 m which I have arbitrarily chosen for our solution. Okay, and now let’s compare to the analytic solution. Listing 9.8 tank_drain_soln.m 1 2 3 4 5 function res = tank_drain_soln(t) k = 0.4; h0 = 20; res = (sqrt(h0)-k*t./2).^(2); end >> fplot(@tank_drain_soln,[0,20],'-r') 9.10 Euler in action! 331 20 18 16 14 height 12 10 8 6 4 2 0 0 5 10 15 20 time Figure 9.7 Comparing the numerical and analytic solution. A perfect match! Once again, I could just as well use an anonymous function for this last part. >> >> >> >> k = 0.4; h0 = 20; tank_drain_soln = @(t) (sqrt(h0)-k*t./2).^(2); fplot(tank_drain_soln,[0,20],'-r'); 9.10 Euler in action! Now that we have solved a few examples using MATLAB’s built-in function ode45, let’s revisit Euler’s method. The purpose of this is the same as when we considered the bisection method in Section 7.17.1. When available, you should use MATLAB’s built-in functions. They are bug free and optimized for efficiency. But it is good to look at some “simple” numerical methods to gain an understanding of what is going on under the hood, and to appreciate what MATLAB is doing for us. We already described Euler’s method in Section 9.2. Writing a MATLAB function to perform Euler’s method we have: 332 Chapter 9 Ordinary Differential Equations Listing 9.9 euler_ode.m 1 function [T,Y] = euler_ode(fcn_handle, Trange, dt, y0) 2 % 3 % Un-packing Trange. The first element is t0 and the second is Tfinal 4 t0 = Trange(1); 5 tfinal = Trange(2); 6 7 % With a fixed timestep, we can create a vector of times at which we 8 % will evaluate our function. I will then take the transpose to get a 9 % column vector like ode45. 10 T = t0:dt:tfinal; 11 T = T'; 12 13 % Pre-size our Y vector, f(t). I will make it a column vector too just 14 % like ode45. 15 Y = zeros(length(T),1); 16 % 17 % Store the initial condition to Y(1) 18 Y(1) = y0; 19 20 % Looping over all times. We will start with i=2 since i=1 just 21 % corresponds to our initial conditions. I could start at i=1, but then 22 % need to shift my indices. 23 for i=2:length(T) 24 dy = fcn_handle(T(i-1),Y(i-1)); 25 Y(i) = dy*dt+Y(i-1); 26 end 27 28 end Now let’s apply it to solve the rat growth problem (Listing 9.1). >> >> >> >> hold on t0=0; tfinal=365; dt=1; y0=2; [T1,Y1]=euler_ode(@rats,[t0,tfinal],dt,y0); plot(T1,Y1,'r-') We are using a time step of 1 day, and are plotting the solution as a red line. Note that in euler_ode we are returning two vectors, T and Y; if you wish to return multiple variables from a function, we use the bracket notation as done here. Let’s also try a time step of 0.1 days and plot the result as a green line. >> dt=0.1; >> [T2,Y2]=euler_ode(@rats,[t0,tfinal],dt,y0); >> plot(T2,Y2,'g-') Lastly, let’s compare our answer to that using ode45, where I will plot the solution as blue circles. 9.10 Euler in action! 333 >> [T3,Y3] = ode45(@rats, [t0, tfinal], y0); >> plot(T3,Y3,'bo') Notice that with ode45 we do not need to specify a time step, it is an adaptive method. All of the other required inputs are the same. The plot comparing our results is shown below. We find that using Euler’s method with a timestep of 1 day, we are in good agreement with ode45 at short times, but then the disagreement increases as time increases. Initially, the error is very small and not noticeable. The error then grows because F n+1 is dependent on F n , the value at the previous integration step. This causes errors to propagate and grow. When we decrease the time step to 0.1 days, the agreement is excellent. 80 70 60 50 40 30 20 10 0 0 50 100 150 200 250 300 350 400 Figure 9.8 Comparing Euler’s method and ode45 to solve the rat growth problem. The error in Euler’s method is proportional to the time step size. So we can always decrease the value of the time step and improve its accuracy. But this comes at a greater computational cost. >> [T3,Y3] = ode45(@rats, [t0, tfinal], y0); >> length(Y3) ans = 45 Using ode45 we have the initial condition plus 44 integration steps, but remember these higher order schemes are evaluating g more times per step than this number reveals. Using Euler’s method with a timestep of 1 day we have the initial condition plus 365 integration steps, and with a time step of 0.1 days we have the initial 334 Chapter 9 Ordinary Differential Equations condition plus 3,650 integration steps! Be that as it may, Euler’s method is rather robust and efficient, so at times you may find it a good choice to use. To highlight the speed and robustness of Euler’s method, let’s look at our “stiff” problem (Listing 9.4). Let’s consider time steps of size delta and delta/10. And we can compare to ode23s for completeness. >> >> >> >> >> >> >> >> >> >> hold on delta = 0.01; t0=0; tfinal=200; y0=delta; dt = delta; [T1,Y1] = euler_ode(@stiff,[t0,tfinal],dt,y0); plot(T1,Y1,'r-') dt = delta/10; [T2,Y2] = euler_ode(@stiff,[t0,tfinal],dt,y0); plot(T2,Y2,'g-') [T3,Y3] = ode23s(@stiff, [t0,tfinal], y0); plot(T3,Y3,'bo') 1.2 1 0.8 0.6 0.4 0.2 0 0 50 100 150 200 Figure 9.9 Comparing Euler’s method and ode23s to solve our “stiff” ODE. A perfect match! And both sets of Euler calculations computed very quickly. As a final point, let me just mention that methods like Euler’s method with fixed time steps have some real applicability, and is something my research group uses routinely. My group frequently uses a technique called molecular dynamics, where we are literally solving Newton’s equations of motion at the molecular level. However, a key requirement of our integration scheme (among others) is that it is time reversible. That is, from a given point we can rewind back to the beginning. Fixed time step methods satisfy this condition, while adaptive methods do not. 9.11 Return of fzero There are many other cases where reversibility is important. I guess that’s enough Euler fun... for now! 9.11 Return of fzero Within this chapter we solved three first order ODEs corresponding to physical processes. We have modeled the population or rats, the velocity of a falling object, and the height of fluid draining from a tank. For our solution, we have plotted the value of the function over a period of time, and we have seen how to store the numerical results to vectors. When solving physical problems, we often want to solve for the time at which the function is equal to a particular value. Before moving on to the next chapter, let’s discuss how to accomplish this. We will start by using a logical vector to identify elements satisfying a given condition. (Note, a for loop would be possible too.) We will then see how we can use fzero to obtain a precise result. In this later approach we will also see how we can compute the value of the function at exact values of t . This gives us the ability to use our numerical solution much like we might use an analytic solution. Let’s return to our rat example discussed in Section 9.4. At what time do we have 30 rats? From our plot, it looks like we have 30 rats at around 150 days. But I would like a precise result. Solving as: >> >> >> >> t0 = 0; tfinal = 365; y0 = 2; [T,Y] = ode45(@rats, [t0, tfinal], y0); we obtain two vectors, T and Y. T is a vector containing the times at which our function was evaluated, where the first entry is t0 and the final entry is tfinal. Y is a vector containing the values of the function at the corresponding time in vector T. We know that the population is increasing from a value of y0 at time t0. The first approach we could therefore apply to find when the population is equal to 30 would be to perform a logical comparison to see when Y is greater than or equal to 30. The resulting logical vector could then be used to identify all of the elements of Y that are greater than or equal to 30. We would then take the first element of the resulting vector as an estimate of the solution. The first element because the population is increasing with time. Note, we do not perform a logical comparison to see when Y is equivalent to 30 since ode45 is solving for the population at discrete times, so an exact value of 30 between t0 and tfinal is unlikely. Let’s do it! >> Ysol = Y(Y>=30); >> yans = Ysol(1) yans = 32.8834 335 336 Chapter 9 Ordinary Differential Equations >> TSol = T(Y>=30); >> tans = TSol(1) tans = 166.0951 We see that the population is not exactly 30, but rather 32.8834. We could of course find a more precise result. One way would be to perform a linear interpolation between this point and the previous point that would have been just less than 30. We will return to this point in a couple of chapters when we look at MATLAB’s built-in interpolating functions. Here, we will use what we know and use fzero. Think back to Chapter 7 and the use of fzero and our bisection code to find the zero of a (linear or) non-linear function over a given range. How did these methods work? Assume that t is our independent variable. We first need to know two values of t that bracket when the function (or dependent variable) is equal to 0. We evaluate the function at these two points, and then guess a value of t (between our other two t ’s that bracket the solution) where the zero occurs, and evaluate the function at this point. We then check to see which of the original t ’s bracket the zero with this new guessed value of t . We keep repeating and shrinking the range until we converge on our answer. So in order to be able to use fzero, we need a function that we pass a value of t , and it returns the value of the function. We can do this. If I use ode45 to solve our ODE over the range 0 to t , and store the results to a vector, the final value of the vector is the value of the function at t . Let’s create a new function rat_time to do just that. Listing 9.10 rat_time.m 1 2 3 4 5 6 7 8 function res = rat_time(t) t0 = 0; % initial time tfinal = t; % final time y0 = 2; % initial population [T,Y] = ode45(@rats, [t0, tfinal], y0); % now let's return the the population at t res = Y(end); end And testing its functionality >> rat_time(150) ans = 26.2171 Nice! So now we have a method to compute the value of the function at an exact time. For fzero, we can only search for where a function is equal to zero. If we wish to find where the population is equal to 30, we should therefore subtract 30 from the value of the population so that when the population is equal to 30, 9.11 Return of fzero 337 the result of our function is 0. Since the population is increasing, we can be sure it satisfies the conditions required by fzero. (Think of the bisection method.) Updating our function and then testing: Listing 9.11 rat_time_2.m 1 2 3 4 5 6 7 8 function res = rat_time_2(t) t0 = 0; % initial time tfinal = t; % final time y0 = 2; % initial population [T,Y] = ode45(@rats, [t0, tfinal], y0); % now let's return the the population at res = Y(end)-30; end - 30 >> rat_time_2(150) ans = -3.7829 Okay. So now let’s use fzero to search over the range 0.1 to 365 for when the function is 0. Note that we can not begin our search at 0 because this would result in calling ode45 with an initial and final time of 0, which results in an error. (There would be nothing to integrate!) >> fzero(@rat_time_2,[0.1,365]) ans = 159.2232 We get a precise answer of 159.2232 days. We can check by computing the population at this time. >> rat_time(ans) ans = 30.0000 Beautiful! We find that the answer found from our simple search of 166.0951 was a reasonable estimate, but it is in error. When solving, note that I created rat_time_2 as its own M-file. This is another case where it may be attractive to instead use an anonymous function. >> rat_time_2 = @(t) rat_time(t) - 30; >> fzero(rat_time_2,[0.1,365]) ans = 159.2232 Nice! Then if we wanted to look at the time to reach a different population, we could just re-define rat_time_2 from the Command Window . Having seen how we can use fzero with ode45, let’s apply our new skill to 338 Chapter 9 Ordinary Differential Equations Examples 9.1 and 9.2. We will also consider a new example, modeling the cooling of a cup of coffee. Example 9.3 (Link to screen cast with accompanying M-file.) Let’s revisit our free fall example, Example 9.1. What is the terminal velocity. Additionally, at what time is the velocity equal to 15 m/s? Let’s start by writing a function to compute the velocity at a specified time. Listing 9.12 free_fall_time.m 1 2 3 4 5 6 7 8 function res = free_fall_time(t) t0 = 0; % initial time tfinal = t; % final time y0 = 0; % initial velocity % solving our ODE [T,Y]=ode45(@free_fall,[t0,tfinal],y0); res = Y(end); end From our graph, we see that after about 15 s the velocity reaches a constant value. Put differently, the derivative of the velocity (and hence the acceleration) is equal to zero. When this occurs we have reached our terminal velocity. >> free_fall_time(15) ans = 19.6091 >> free_fall_time(20) ans = 19.6191 >> free_fall_time(30) ans = 19.6200 >> free_fall_time(40) ans = 19.6200 >> free_fall_time(400) ans = 19.6164 >> free_fall_time(500) ans = 19.6160 >> free_fall_time(1000) ans = 19.6136 >> free_fall_time(10000) ans = 19.6113 >> free_fall_time(100000) ans = 19.6113 >> free_fall_time(1e6) 9.11 Return of fzero 339 ans = 19.6157 >> free_fall_time(1e7) ans = 19.6118 It appears to take a very long time to reach our terminal velocity, although this small fluctuation may be attributed to numerical error. Nonetheless, the rate of change and hence acceleration is very small (if not 0), so if this was a skydiving problem, it would still be tons of fun. Note that if we were to analytically solve for the terminal velocity as the point when d v/d t = 0, we have v = 19.6200. Two more notes on this, is d v/d t is only a function of v. We could therefore plug values of v into our rate function and find when the rate (d v/d t = 0) is zero. Taking it a step further, we could use fzero on a modified rate function where we do not take t as an input. Now let’s find the time when the velocity is equal to 15 m/s. We need to update free_fall_time so that it returns a value of zero when the velocity is equal to 15 m/s. Listing 9.13 free_fall_time_2.m 1 2 3 4 5 6 7 8 function res = free_fall_time_2(t) t0 = 0; % initial time tfinal = t; % final time y0 = 0; % initial velocity % solving our ODE [T,Y]=ode45(@free_fall,[t0,tfinal],y0); res = Y(end)-15; end Let’s search for our solution over the range 0.1 to 30 s. Remember, we can not have a lower-bound of 0 because this would result in a call to ode45 with an initial and final time of 0. >> fzero(@free_fall_time_2,[0.1,30]) ans = 2.8923 So the velocity reaches 15 m/s at 2.8923 s. If we had instead wished to use an anonymous function: >> free_fall_time_2 = @(t) free_fall_time(t) - 15; >> fzero(free_fall_time_2,[0.1,30]) ans = 2.8923 340 Chapter 9 Ordinary Differential Equations Example 9.4 Now let’s revisit our tank draining example, Example 9.2. Find the time at which the height of the fluid in the tank is 15, 10, 5 and 1 m. Let’s start by creating a function that computes the height of the fluid in the take at a specified t . We then need to modify the function so that it evaluates to zero at these heights. Since we wish to look at 4 different heights, I will write a general form of the function in which I can also pass the variable yfinal which corresponds to the desired final height. I will then use an anonymous function for each case to pass the the desired value of yfinal. Listing 9.14 tank_drain_time.m 1 2 3 4 5 6 7 function res = tank_drain_time(t,yfinal) t0 = 0; % initial time tfinal = t; % final time y0 = 20; % the initial heigh [T,Y]=ode45(@tank_drain,[t0,tfinal],y0); res = Y(end)-yfinal; end And now solving: >> tank_drain_time_15 = @(t) tank_drain_time(t,15); >> t15 = fzero(tank_drain_time_15,[0.1,20]) t15 = 2.9958 >> tank_drain_time_10 = @(t) tank_drain_time(t,10); >> t10 = fzero(tank_drain_time_10,[0.1,20]) t10 = 6.5493 >> tank_drain_time_5 = @(t) tank_drain_time(t,5); >> t5 = fzero(tank_drain_time_5,[0.1,20]) t5 = 11.1803 >> tank_drain_time_1 = @(t) tank_drain_time(t,1); >> t1 = fzero(tank_drain_time_1,[0.1,20]) t1 = 17.3607 And so on. 9.11 Return of fzero 341 Example 9.5 Suppose that you are given an 8 ounce cup of coffee at 90 ◦ C and a 1 ounce container of cream at room temperature, which is 20 ◦ C. You have learned from bitter experience that the hottest coffee you can drink comfortably is 60 ◦ C. Assuming that you take cream in your coffee, and that you would like to start drinking as soon as possible, are you better off adding the cream immediately or waiting? And if you should wait, then how long? To answer this question, you have to model the cooling process of a hot liquid in air. Hot coffee transfers heat to the environment by conduction, radiation, and evaporative cooling. Quantifying these effects individually would be challenging and unnecessary to answer the question as posed. As a simplification, we can use Newton’s Law of Cooling: df = −r ( f − e) dt where f is the temperature of the coffee as a function of time and d f /d t is its time derivative; e is the temperature of the environment (assume 20 ◦ C), which is a constant in this case, and r is a parameter (also constant) that characterizes the rate of heat transfer. It would be easy to estimate r for a given coffee cup by making a few measurements over time. Let’s assume that that has been done and r has been found to be 0.001 in units of inverse seconds, 1/s. In what follows, I will give detailed instructions for solving this problem. The intention of this is to help you build-up your code (incremental development style) for what may be the most complex problem we have solved so far. What we ultimately would like to know is if there is an optimal time when the cream should be added to the coffee to minimize the required cooling time. To answer this, you should solve for the required cooling time for three cases. First, just let the coffee (black, no cream) cool to the desired temperature. Second, add the cream at t = 0 and then let the coffee cool. And third, add the cream at the end so that once it is added, we are at the final, desired temperature. Keep this in mind as you read through the detailed instructions that follow. • Using mathematical notation, write the rate function, g , as a function of y, where y is the temperature of the coffee at a particular point in time. • Create an M-file named coffee and write a function called coffee that takes no input variables and returns no output value. Put a simple statement like x=5 in the body of the function and invoke coffee() from the Command Window . • Add a function called rate_func that takes t and y and computes g (t , y). Notice that in this case g does not actually depend on t ; nevertheless, your function has to take t as the first input argument in order to work with ode45. Test your function by adding a line like rate_func(0,90) to coffee, then call coffee from the Command Window . • Once you get rate_func(0,90) working, modify coffee to use ode45 to compute the temperature of the coffee (ignoring the cream) for 60 minutes. 342 Chapter 9 Ordinary Differential Equations Confirm that the coffee cools quickly at first, then more slowly, and reaches room temperature (approximately) after about an hour. • Write a function called mix_func that computes the final temperature of a mixture of two liquids. It should take the volumes and temperatures of the liquids as parameters. In general, the final temperature of a mixture depends on the specific heat of the two substances. But if we make the simplifying assumption that coffee and cream have the same density and specific heat, then the final temperature is (v 1 y 1 + v 2 y 2 )/(v 1 + v 2 ), where v 1 and v 2 are the volumes of the liquids, and y 1 and y 2 are their temperatures. Add code to coffee to test mix_func. • Use mix_func and ode45 to compute the time until the coffee is drinkable if you add the cream immediately. • Modify coffee so it takes an input variable t that determines how many seconds the coffee is allowed to cool before adding the cream, and returns the temperature of the coffee after mixing. • Use fzero to find the time t that causes the temperature of the coffee after mixing to be 60 ◦ C. • What do these results tell you about the answer to the original question? Is the answer what you expected? What simplifying assumptions does this answer depend on? Which of them do you think has the biggest effect? Do you think it is big enough to affect the outcome? Overall, how confident are you that this model can give a definitive answer to this question? What might you do to improve it? Solution: What a fun problem! The problem statement is very detailed to help you build-up your code. I will take you through a number of versions of my code as I build it up. While I will not show any plots, from the rate expression we see that the hotter the temperature of our beverage, the greater the rate of heat transfer. So when we add cream we have a step change (decrease) in our temperature, which then results in a lower rate of heat transfer. Then the question is, is there an optimal time to add the cream. We will consider three cases. First, we will just let our coffee cool to the desired temperature. Second, we can add the cream and then let our coffee cool. And third, we will add the cream at the end so that once it is added, we are at the final, desired temperature. The first series of steps are a matter of incremental development steps to get the code working. I maintained a copy of the function coffee which I used for these debugging steps. Here is that final version of my debugged code: 9.11 Return of fzero 343 Listing 9.15 coffee.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function res = coffee() % %x = 5 % teesting that coffee works % %rate_func(0,90) % testing that rate_func works % %t0 = 0; % initial time %tfinal = 60*60; % final time is 60 minutes %f0 =90; % initial temperature of the coffee %ode45(@rate_func,[t0,tfinal],f0) % mix_func(90,8,20,1) % testing that mix_func works end function res = rate_func(t,y) r = 0.001;% units of 1/s et = 20; % room temperature in oC res = -r*(y-et); end function res = mix_func(y1,v1,y2,v2) res = (y1*v1+y2*v2)/(v1+v2); end Before I add cream, first let’s calculate the time required to cool black coffee (that is, coffee with no cream). I will accomplish this using fzero. To do this, in coffee_0_fzero the top function is a function with no inputs that simply calls fzero. Following it we have a new helper function, coffee_0_error_func, which is our error function to be used with fzero. It takes an argument of time and returns the temperature of the coffee at that time minus 60. I subtract 60 from the temperature so that when we have our desired temperature of 60 ◦ C, the function will be equal to (or return a value of) 0. This will allow me to use fzero to solve. 344 Chapter 9 Ordinary Differential Equations Listing 9.16 coffee_0_fzero.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 % Case of just cooling coffee (no cream) % % For the purpose of this exercise, I will return two optional variables, % the time when the coffee is safe to drink and the temperature at this % time. I will make them optional so that if you only wish % to return time and not the temperature, which you expect to be 60. % function [tsafe,ysafe] = coffee_0_fzero() % Remember, my lower bracket needs to be greater than 0. % The first thing MATLAB will do when I call fzero with brackets % is evaluate my function at the end points. 0 is not allowed because % it would lead to an ode45 call with an intial and final time of % zero. tsafe = fzero(@coffee_0_error_func,[0.1,700]); % Calculating the temperature at this time to see how close fzero % get's us to the actual value. I will just call our error function, % adding a value of 60 back to the final result to get the temperature. ysafe = coffee_0_error_func(tsafe)+60; end % Our error function function res = coffee_0_error_func(t) % the initial temp of the coffee in oC y0 = 90; % Now let's use ode45 to compute the temperature % of our beverage over a period of time t. Remember the only % times we know exactly when our function will be evaluate is at % t0 and tfinal. So if we return the last element of our vector, it will % be the value of our function at t. t0 = 0; tfinal = t; [T,Y] = ode45(@rate_func,[t0,tfinal],y0); % We want to choose t such that the final % temperature is 60 oC. Remember fzero is looking % for zeroes. Our solution is when we find t such % that our error function returns a value of 0. % So we therefore need to subtract 60 from our final % temperature. res = Y(end)-60; end function res = rate_func(t,y) r = 0.001;% units of 1/s et = 20; % room temperature in oC res = -r*(y-et); end function res = mix_func(y1,v1,y2,v2) res = (y1*v1+y2*v2)/(v1+v2); end 9.11 Return of fzero And now let’s use it: >> [tsafe,ysafe] = coffee_0_fzero tsafe = 559.6158 ysafe = 60.0000 Using fzero we obtain a precise answer of 559.6158 s (9.3269 minutes). We can pass this value to our error function to confirm that the temperature minus 60 is 0 at this time. In the next iteration of the code, we will add the cream immediately to our coffee. So effectively by mixing the cream first, we are decreasing our initial temperature at t = 0. I will again use fzero as before in the updated function coffee_1_fzero. 345 346 Chapter 9 Ordinary Differential Equations Listing 9.17 coffee_1_fzero.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 % Case of adding cream immediately % % For the purpose of this exercise, I will return two optional variables, % the time when the coffee is safe to drink and the temperature at this % time, so we can see how close our solution get's us to the desired % temperature of 60 oC so that we can compare our numerical solution % strategies. I will make them optional so that if you only wish % to return time and not the temperature, which you expect to be 60, % you can. % function [tsafe,ysafe] = coffee_1_fzero() % Remember, my lower bracket needs to be greater than 0. % The first thing MATLAB will do when I call fzero with brackets % is evaluate my function at the end points. 0 is not allowed because % it would lead to an ode45 call with an intial and final time of % zero. tsafe = fzero(@coffee_1_error_func,[0.1,700]); end % Calculating the temperature at this time to see how close fzero % get's us to the actual value. I will just call our error function, % adding a value of 60 back to the final result to get the temperature. ysafe = coffee_1_error_func(tsafe)+60; % Our error function function res = coffee_1_error_func(t) y1 = 90; % initial temp of coffee in oC y2 = 20; % initial temp of cream in oC v1 = 8; % initial volume of coffee in ounces v2 = 1; % intiial volume of cream in ounces % Now mixing the coffee and cream and computing % the initial temp of the mixture y0 = mix_func(y1,v1,y2,v2); % Now let's use ode45 to compute the temperature % of our beverage over a period of 60 minutes. % I will store the results to a vector that we % can search through for our solution. t0 = 0; tfinal = t; [T,Y] = ode45(@rate_func,[t0,tfinal],y0); % We want to choose t such that the final % temperature is 60 oC. If we use fzero we % need to look for a value of zero, so % subtract 60. res = Y(end)-60; end function res = rate_func(t,y) r = 0.001;% units of 1/s et = 20; % room temperature in oC res = -r*(y-et); end 9.11 Return of fzero 54 function res = mix_func(y1,v1,y2,v2) 55 res = (y1*v1+y2*v2)/(v1+v2); 56 end And now let’s use it: >> [tsafe,ysafe] = coffee_1_fzero tsafe = 441.8328 ysafe = 60 Using fzero we obtain a precise answer of 441.8328 s (7.3639 minutes). We can pass this value to the error function to confirm that the temperature minus 60 is 0 at this time. By adding creamer first our coffee takes 1.9630 minutes less to cool to 60 ◦ C. From our rate expression, we see that the rate of cooling will be greatest when the temperature of the coffee is greatest. For the last part then, we ask if we can speed up the cooling process by first letting the coffee cool for some time t , before we add the creamer to bring the coffee down to a temperature of 60 ◦ C. For this case again I will use fzero. The function file will look very much the same as the last case. 347 348 Chapter 9 Ordinary Differential Equations Listing 9.18 coffee_2_fzero.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 % Case of adding creat at time t function [tsafe,ysafe] = coffee_2_fzero() tsafe = fzero(@coffee_2,[0.1,3600]); ysafe = coffee_2(tsafe)+60; end % Our error function function res = coffee_2(t) % Let the coffee (no cream) cool for t % seconds y0 = 90; t0 = 0; tfinal = t; [T,Y] = ode45(@rate_func,[t0,tfinal],y0); % % Now add the cream to the coffee y2 = 20; % temperature of the cream v2 = 1; % volume of the cream in ounces y1 = Y(end); % temperature ofthe coffee v1 = 8; % volume of the coffee in ounces yfinal = mix_func(y1,v1,y2,v2); % % Since fzero looks for zeroes, lets shift % the temperature by 60 C res = yfinal-60; end function res = rate_func(t,y) r = 0.001;% units of 1/s et = 20; % room temperature in oC res = -r*(y-et); end function res = mix_func(y1,v1,y2,v2) res = (y1*v1+y2*v2)/(v1+v2); end >> [tsafe,ysafe] = coffee_2_fzero tsafe = 441.8328 ysafe = 60 That is exactly how long it took to cool the coffee when we added the creamer first! The solution is confirmed by calling coffee_2, where we additionally see that the coffee (without creamer) is first cooled to 65 ◦ C, and then the mixing process reduces the temperature an additional 5 ◦ C. So what is going on? From the rate expression, we know that the rate of cooling is decreasing as the temperature of the coffee decreases (or equivalently as time increases). However, what is more subtle, is the temperature drop upon the addition of cream is also decreasing as the temperature of the coffee decreases. When the coffee is at 90, 80, and 70 ◦ C, the decrease in temperature upon adding creamer is 9.12 Isothermal Batch Reactors 7.7778, 6.6667, and 5.5556 ◦ C, respectively. 9.12 Isothermal Batch Reactors In Section 8.14 we analyzed an isothermal continuous stirred tank reactor (CSTR). Here we will look at an isothermal batch reactor. Our analysis of isothermal batch reactors will continue into the next chapter. I will again take this opportunity to encourage you to look at the free text “A First Course on Kinetics and Reaction Engineering,” by Carl Lund3 . 9.12.1 Batch Reactor Basics Just as when “deriving” the design equation for a CSTR, we will again use a mass balance here to derive the design equation for a batch reactor. For this problem we will consider an isothermal reactor, so we can solve our mole balance independent of our energy balance. It is common to look at cases when this is not the case, which requires solving your mass and energy balance simultaneously. First we will look at the isothermal case, and then we will add this extra layer later. Below is an image of a batch reactor which I took from Wikipedia. Figure 9.10 Cartoon of a batch reactor. While it probably did not look like this, when you carry out reactions in your general chemistry and organic chemistry classes, they were carried out in batch. At t = 0 you charge the reactor with a solution containing your reactants. Then 3 http://wwwresearch.sens.buffalo.edu/karetext/title/title.shtml 349 350 Chapter 9 Ordinary Differential Equations the reaction proceeds over some period of time. During this time the system is perfectly mixed, and no additional material is added or removed. For this problem, we will add (if the reaction is endothermic) or remove (if the reaction is exothermic) heat at the same rate it is consumed or generated so that the reaction proceeds isothermally. Let’s write a mole balance for our system for component A: INPUT + GENERATION = OUTPUT + ACCUMULATION In our problem we are not adding or removing anything, so our expression reduces to: GENERATION = ACCUMULATION The accumulation term is something you may have seen in your Mass and Energy Balance course if you looked at non-steady state balances, and would take the form here of: dnA ACCUMULATION = dt It is the time rate of change of the moles of A in the system. We will assume that the only way moles of A can be generated/consumed is via a chemical reaction. If we consider here just a single chemical reaction taking place: GENERATION = ν A V r where ν A is the stoichiometic coefficient of A, ν A r is the per unit volume rate of generation/consumption of A, and V is the volume of the system. With this our mole balance design equation becomes: dnA = νAV r dt We will next look at the problem statement for exercise 9.1 that will appear at the end of the chapter to aid in our discussion of some common notes about modeling batch reactors. You will then solve exercise 9.1 on your own. The conversion of A to B (A → B ) takes place in an aqueous solution in an isothermal batch reactor. The reactor is charged with 1200 L of a 2 M solution of A at 300 K. (Recall 1 M = 1 mol/L.) Calculate the time required to reach 0.8 fractional conversion of A. The fractional conversion of A may be computed as fA = n 0A − n A n 0A where n 0A is the initial number of moles of A (at t = 0) and n A is the number of moles at a given time. 9.12 Isothermal Batch Reactors 351 The reaction is first order in A and the rate coefficient obeys the Arrhenius expression with a pre-exponential term equal to 2.4 × 108 s−1 and an activation energy of 15.3 kcal/mol. Mathematically this means r = kC A k = k 0 exp[−E /(RT )] k 0 = 2.4 × 108 s−1 E = 15.3kcal/mol 9.12.2 Useful Assumption We have a reaction that takes place in the aqueous phase. The initial concentration of A is rather dilute, 2 moles per liter of water. There are approximately 55.35 moles of water per liter, so this corresponds to a mole fraction of just 0.035. And since it is a 1 to 1 reaction (for every mole of B created a mole of A is consumed), the total concentration of A and B will remain the same. Therefore, it is very reasonable to assume that volume is constant and equal to 1200 L. 9.12.3 Note on Units I have provided you with the general rate expression. If you want a rate expression for the consumption of A or generation of B, you multiply by the appropriate stoichiometric coefficient. Remember stoichiometric coefficients for reactants are negative (they are being consumed) and positive for reactants. So for the rate of consumption of A r A = −kC A The term C A is a measure of concentration, moles/volume. What units do we use? Well, the rate is defined as 1 dnA = r A = −kC A V dt The volume, V , is referred to as a normalization factor, and is a very common choice for liquid phase reactions. It makes the rate an intrinsic property. So the units of C A just need to agree with n A /V . Note too that since we are assuming V is constant, we can bring it into the differential and write 1 d n A d (n A /V ) dC A = = V dt dt dt Likewise the fractional conversion can be written in terms of C A by multiplying the top and bottom of the expression by 1/V to give fA = C A0 −C A C A0 352 Chapter 9 Ordinary Differential Equations So if you would prefer to use units of concentrations, you can. The rate coefficient (k) has the same units as the pre-exponential term (k 0 ) of 1/s, the same units as d /d t . So if you solve as is, your unit of time will be seconds. The term in the exponential, E /(RT ) is dimensionless. You are given E in kcal/mol. So unless you convert it, you should use R in units of cal/(mol K) and T in units of K. 9.12.4 Final Note In this problem we have set everything up to solve using A. As a future note, know that you do not have to use A if you do not want to. Likewise, we do not need to solve a differential mole balance for B in order to determine how many moles of B we have. The moles of A and B are related by stoichiometry. For every mole of A that is consumed, a mole of B is created in our reaction. Therefore n 0A − n A = n B − n B0 and since we started with no moles of B n 0A − n A = n B C A0 −C A = C B My very last note is on the rate expression itself. Here we have a relatively simple expression. This is generally not the case. Depending on the proposed mechanism or if we have a reversible reaction, the expression is much more complicated. (No worries, MATLAB does not care!) But the point I would like to make is that you can’t (and never should be expected to) just look at a reaction and know what the form of the rate of reaction should be. The only way is to propose a model (in various ways) and test it by fitting to experimentally generated data. 9.13 Glossary 9.13 Glossary differential equation (DE): An equation that relates the derivatives of an unknown function. ordinary DE: A DE in which all derivatives are taken with respect to the same variable. partial DE: A DE that includes derivatives with respect to more than one variable first order (ODE): A DE that includes only first derivatives. linear: A DE that includes no products or powers of the function and its derivatives. time step: The interval in time between successive estimates in the numerical solution of a DE. first order (numerical method): A method whose error is expected to halve when the time step is halved. adaptive: A method that adjusts the time step to control error. stiffness: A characteristic of some ODEs that makes some ODE solvers run slowly (or generate bad estimates). Some ODE solvers, like ode23s, are designed to work on stiff problems. parameter: A value that appears in a model to quantify some physical aspect of the scenario being modeled. 353 354 Chapter 9 Ordinary Differential Equations 9.14 Exercises Exercise 9.1 The conversion of A to B (A → B ) takes place in an aqueous solution in an isothermal batch reactor. The reactor is charged with 1200 L of a 2 M solution of A at 300 K. (Recall 1 M = 1 mol/L.) Calculate the time required to reach 0.8 fractional conversion of A. The fractional conversion of A may be computed as fA = n 0A − n A n 0A where n 0A is the initial number of moles of A (at t = 0) and n A is the number of moles at a given time. The reaction is first order in A and the rate coefficient obeys the Arrhenius expression with a pre-exponential term equal to 2.4×108 s−1 and an activation energy of 15.3 kcal/mol. Mathematically this means r = kC A k = k 0 exp[−E /(RT )] k 0 = 2.4 × 108 s−1 E = 15.3kcal/mol Chapter 10 Systems of ODEs Last chapter we learned how to numerically solve first order initial value ordinary differential equations. We will build upon this in Chapter 10 and will solve systems of first order ordinary differential equations. By the end of this chapter you will be able to: • Demonstrate the ability to construct vectorized “rate” functions • Apply ode45 to solve systems of initial value ODEs • Analyze ecosystem dynamics by setting up and solving intricate physical models requiring the use of ode45 If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 10.1 Lotka-Voltera The Lotka-Voltera model describes the interactions between two species in an ecosystem, a predator and its prey. A common example is rabbits and foxes. The model is governed by the following system of differential equations: R 0 = aR − bRF F 0 = ebRF − cF where • R is the population of rabbits, • F is the population of foxes, • a is the natural growth rate of rabbits in the absence of predation, 355 t Notice here I am using Lagrange’s notation for compactness when writing systems of differential equations; R 0 = ddRt and F 0 = ddFt . (The notation on the righthand side corresponds to Leibniz’s notation. Newton’s dot notation also exists, but is not as common in the undergraduate curriculum; it is used to designate flow rates, but there is never a mention of Newton.) Note that Lagrange’s notation can also take the form: R t = ddRt and F t = ddFt . 356 Chapter 10 Systems of ODEs • c is the natural death rate of foxes in the absence of prey, • b is the death rate of rabbits per interaction with a fox, • e is the efficiency of turning eaten rabbits into foxes. At first glance you might think you could solve these equations by calling ode45 once to solve for R as a function of time and once to solve for F . The problem is that each equation involves both variables, which is what makes this a system of equations and not just a list of unrelated equations. To solve a system, you have to solve the equations simultaneously. Fortunately, ode45 can handle systems of equations. The difference is that the initial condition is a vector that contains initial values R(0) and F (0), and the output is a matrix that contains one column for R and one for F . And here’s what the rate function looks like with the parameters a = 0.1, b = 0.01, c = 0.1 and e = 0.2: Listing 10.1 lotka.m 1 2 3 4 function res = lotka(t, V) % unpack the elements of V r = V(1); f = V(2); 5 % a b c e 6 7 8 9 10 set the parameters = 0.1; = 0.01; = 0.1; = 0.2; 11 % compute the derivatives drdt = a*r - b*r*f; dfdt = e*b*r*f - c*f; 12 13 14 15 16 17 18 end % pack the derivatives into a vector res = [drdt; dfdt]; As usual, the first input variable is time. The second input variable is a vector with two elements, R(t ) and F (t ). I gave it a capital letter to remind me that it is a vector. The body of the function includes four paragraphs, each explained by a comment. The first paragraph unpacks the vector by copying the elements into scalar variables. This isn’t necessary, but giving names to these values helps me remember what’s what. It also makes the third paragraph, where we compute the 10.1 Lotka-Voltera derivatives, resemble the mathematical equations we were given, which helps prevent errors. The second paragraph sets the parameters that describe the reproductive rates of rabbits and foxes, and the characteristics of their interactions. If we were studying a real system, these values would come from observations of real animals, but for this example I chose values that yield interesting results. The last paragraph packs the computed derivatives back into a vector. When ode45 calls this function, it provides a vector as input and expects to get a vector as output. Sharp-eyed readers will notice something different about this line: res = [drdt; dfdt]; The semi-colon between the elements of the vector is not an error. It is necessary in this case because ode45 requires the result of this function to be a column vector. Now we can run ode45 like this: >> ode45(@lotka, [0, 365], [100, 10]) >> xlabel('Time in days') >> ylabel('Population') As always, the first argument is a function handle, the second is the time interval, and the third is the initial condition. The initial condition is a vector: the first element is the number of rabbits at t = 0, the second element is the number of foxes. The order of these elements (rabbits and foxes) is up to you, but you have to be consistent. That is, the initial conditions you provide when you call ode45 have to be the same as the order, inside lotka, where you unpack the input vector and repack the output vector. MATLAB doesn’t know what these values mean; it is up to you as the programmer to keep track. But if you get the order right, you should see something like this: 357 t As we have seen previously, we could alternatively create a general rate function M-file which takes the parameters as inputs, and then define an anonymous function in the Command Window in which we pass this information. 358 Chapter 10 Systems of ODEs 120 100 Population 80 60 40 20 0 0 50 100 150 200 250 300 350 400 Time in days Figure 10.1 ode45(@lotka,[0,365],[100,10]) The x-axis is time in days; the y-axis is population. The top curve shows the population of rabbits; the bottom curve shows foxes. This result is one of several patterns this system can fall into, depending on the starting conditions and the parameters. As an exercise, try experimenting with different values. 10.2 What can go wrong? The output vector from the rate function has to be a column vector. If I modify Listing 10.1 (lotka) to change res = [drdt; dfdt]; to res = [drdt, dfdt]; I get the following error: >> ode45(@lotka, [0, 365], [100, 10]) Error using odearguments (line 90) LOTKA must return a column vector. Error in ode45 (line 113) [neq, tspan, ntspan, next, t0, tfinal, tdir, y0, f0, odeArgs, odeFcn, ... 10.3 Output matrices Which is pretty good as error messages go. It’s not clear to me why it needs to be a column vector, but that’s not our problem. Another possible error is reversing the order of the elements in the initial conditions, or in the vectors inside lotka. Again, MATLAB doesn’t know what the elements are supposed to mean, so it can’t catch errors like this; it will just produce incorrect results. 10.3 Output matrices As we saw before, if you call ode45 without assigning the results to variables, it plots the results. If you assign the results to variables, it suppresses the figure. Here’s what that looks like: >> [T, M] = ode45(@lotka, [0, 365], [100, 10]); As in previous examples, T is a vector of time values where ode45 made estimates of the variables. But unlike previous examples, the second output variable is a matrix containing one column for each variable (in this case, R and F ) and one row for each time value. >> size(M) ans = 185 2 This structure—one column per variable—is a common way to use matrices. plot understands this structure, so if you do this: >> plot(T, M) MATLAB understands that it should plot each column from M versus T. You can also copy the columns of M into other variables like this: >> R = M(:, 1); >> F = M(:, 2); In this context, the colon represents the range from 1 to end, so M(:, 1) means “all the rows, column 1” and M(:, 2) means “all the rows, column 2.” >> size(R) ans = 185 1 >> size(F) ans = 185 1 So R and F are column vectors. 359 360 Chapter 10 Systems of ODEs If you plot these vectors against each other, like this >> plot(R, F) You get a phase plot that looks like this: 22 20 18 16 14 12 10 8 6 4 20 30 40 50 60 70 80 90 100 110 Figure 10.2 plot(R,F) Each point on this plot represents a certain number of rabbits (on the x-axis) and a certain number of foxes (on the y-axis). Since these are the only two variables in the system, each point in this plane describes the complete state of the system. Over time, the state moves around the plane; this figure shows the path traced by the state during the time interval. This path is called a trajectory. Since the behavior of this system is periodic, the resulting trajectory is a loop. If there are 3 variables in the system, we need 3 dimensions to show the state of the system, so the trajectory is a 3-D curve. You can use plot3 to trace trajectories in 3 dimensions, but for 4 or more variables, you are on your own. 10.4 Examples The best way to get comfortable using ode45 is to solve some problems. And the best problems to practice on are ones that you know the answer to. In addition to the examples here, for more practice I would encourage you to solve some of the example problems with analytical solutions provided in “Notes on Diffy Qs: Differential Equations for Engineers”. 10.4 Examples 361 Example 10.1 (Link to screen cast with accompanying M-file.) Let’s solve the following system of differential equations x 0 = −y y 0 = (1.01)x − (0.2)y subject to the initial conditions x(0) = 0 y(0) = −1 Let’s start by solving this problem analytically, because we can, and hopefully it will help you appreciate how awesome numerically solving using MATLAB is. We start by noticing £ ¤ x 00 = −y 0 = (1.01)x − (0.2)y = (−1.01)x − (0.2)x 0 We now have a single linear second-order differential equation to solve x 00 = (−1.01)x − (0.2)x 0 Since it is second-order, we need two initial conditions. No problem, we have them x(0) = 0 0 x (0) = −y(0) = 1 To solve, we start by writing the characteristic equation r 2 + (0.2)r + 1.01 = 0 We can factor and solve r 2 + (0.2)r + 1.01 = (r + 0.1)2 + 1 = 0 (r + 0.1)2 = −1 p r = −0.1 ± ( − 1) = −0.1 ± i 362 Chapter 10 Systems of ODEs This leads to the general solution x(t ) = e −t /10 (A cos t + B sin t ) Applying our first initial condition x(0) = A = 0 Leaving us with x(t ) = B e −t /10 sin t Before we can apply our second initial condition, we need to differentiate x with respect to t . Applying the product rule x0 = − 1 B e −t /10 sin t + B e −t /10 cos t 10 Now applying our second initial condition x 0 (0) = B = 1 Leading to the final solution, where recall y = −x 0 x(t ) = e −t /10 sin t 1 y(t ) = e −t /10 (sin t − 10 cos t ) 10 Now let’s solve numerically using MATLAB. Let’s start by writing a function to compute the rate of change of x and y with respect to t (i.e., x 0 and y 0 ). Listing 10.2 example_10_1.m 1 2 3 4 function res = example_10_1(t, V) % unpack the elements of V x = V(1); y = V(2); 5 % compute the derivatives dxdt = -y; dydt = 1.01*x-0.2*y; 6 7 8 9 10 11 12 end % pack the derivatives into a vector res = [dxdt; dydt]; 10.4 Examples 363 We will solve subject to the initial conditions x(0) = 0 and y(0) = −1, and let’s solve over the range 0 < t < 50. Note that the way I set-up the function the first equation is our equation for x and the second is our equation for y. Let’s solve! I will solve by storing the solution to vector T (time) and matrix M (first column will be the solution for x and the second column will be the solution for y). I will plot the solution, compare to our analytic result, and then plot the phase plot. >> >> >> >> >> >> >> >> >> [T,M] = ode45(@example_10_1,[0,50],[0,-1]); X = M(:,1); Y = M(:,2); hold on plot(T,X,'-b',T,Y,'-r') xlabel('t') ylabel('x or y') title('Solution to Example 10.1') legend('x','y') Here is the resulting plot Solution to Example 10.1 1 x y 0.8 0.6 0.4 x or y 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 0 10 20 30 40 50 t Figure 10.3 ode45(@example_10_1,[0,50],[0,-1]) Let’s compare to our analytic solution. Keeping the plot open and then >> Ta = linspace(0,50); >> Xa = exp(-Ta./10).*sin(Ta); >> Ya = (1/10)*exp(-Ta./10).*(sin(Ta)-10*cos(Ta)); 364 Chapter 10 Systems of ODEs >> plot(Ta,Xa,'bo',Ta,Ya,'ro') >> legend('x','y','x ref','y ref') This results in the following plot, where the analytic solution for x is drawn as blue circles and the analytic solution for y as red circles Solution to Example 10.1 1 x y x ref y ref 0.8 0.6 0.4 x or y 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 0 10 20 30 40 50 t Figure 10.4 Comparing our analytic and numeric solutions. A perfect match! Now let’s close the plot and plot our phase plot. We will use our numeric solution. >> plot(X,Y,'-k') >> xlabel('x') >> ylabel('y') And here is the result: 10.4 Examples 365 0.8 0.6 0.4 0.2 y 0 -0.2 -0.4 -0.6 -0.8 -1 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1 x Figure 10.5 Our phase plot for Example 10.1 Cool! A spiral trajectory. Example 10.2 (Link to screen cast with accompanying M-file.) Solve the following system of differential equations x 0 = 4x − 3y y 0 = 6x − 7y subject to the initial conditions x(0) = 2 y(0) = −1 We wont work through the analytic solution to this one, but if you would like to compare the final answer is x(t ) = 3e 2t − e −5t y(t ) = 2e 2t − 3e −5t 366 Chapter 10 Systems of ODEs Now let’s solve numerically using MATLAB. Let’s start by writing a function to compute the rate of change of x and y with respect to t (i.e., x 0 and y 0 ). Listing 10.3 example_10_2.m 1 2 3 4 function res = example_10_2(t, V) % unpack the elements of V x = V(1); y = V(2); 5 % compute the derivatives dxdt = 4*x-3*y; dydt = 6*x-7*y; 6 7 8 9 10 11 12 end % pack the derivatives into a vector res = [dxdt; dydt]; We will solve subject to the initial conditions x(0) = 2 and y(0) = −1, and let’s solve over the range 0 < t < 1. Note that the way I set-up the function the first equation is our equation for x and the second is our equation for y. Let’s solve! I will solve by storing the solution to vector T (time) and matrix M (first column will be the solution for x and the second column will be the solution for y). I will plot the solution and then plot the phase plot. >> >> >> >> >> >> [T,M] = ode45(@example_10_2,[0,1],[2,-1]); plot(T,M) xlabel('t') ylabel('x or y') title('Solution to Example 10.2') legend('x','y') Here is the resulting plot 10.4 Examples 367 Solution to Example 10.2 25 x y 20 x or y 15 10 5 0 -5 0 0.2 0.4 0.6 0.8 t Figure 10.6 ode45(@example_10_2,[0,1],[2,-1]) And comparing to the analytic solutions: >> >> >> >> >> >> Ta = linspace(0,1); Xa = 3*exp(2*Ta)-exp(-5*Ta); Ya = 2*exp(2*Ta)-3*exp(-5*Ta); hold on plot(Ta,Xa,'bo',Ta,Ya,'ro') legend('x','y','x ref','y ref') 1 368 Chapter 10 Systems of ODEs Solution to Example 10.2 25 x y x ref y ref 20 x or y 15 10 5 0 -5 0 0.2 0.4 0.6 0.8 1 t Figure 10.7 Comparing our analytic and numerical solutions. Now let’s close the plot and plot our phase plot. Or to plot in a separate, new figure window, begin with the command figure(2). >> >> >> >> >> >> figure(2) X = M(:,1); Y = M(:,2); plot(X,Y) xlabel('x') ylabel('y') And here is the result: 10.4 Examples 369 16 14 12 10 y 8 6 4 2 0 -2 0 5 10 15 20 25 x Figure 10.8 Our phase plot for Example 10.2 Notice that when you typed figure(2), “Figure 2” became active. If you wanted to return to “Figure 1” and update it, you would switch back to “Figure 1” as the active figure using the command figure(1). And then if you wanted to return back to “Figure 2”, use figure(2). Example 10.3 (Link to screen cast with accompanying M-files.) According to Wikipedia, “The Lorenz attractor, introduced by Edward Lorenz in 1963, is a non-linear three-dimensional deterministic dynamical system derived from the simplified equations of convection rolls arising in the dynamical equations of the atmosphere. For a certain set of parameters the system exhibits chaotic behavior and displays what is today called a strange attractor...” The system is described by this system of differential equations: x 0 = σ(y − x) y 0 = x(r − z) − y z 0 = x y − bz Common values for the parameters are σ = 10, b = 8/3 and r = 28. Use ode45 to estimate a solution to this system of equations. 370 Chapter 10 Systems of ODEs (a) The first step is to write a function named lorenz that takes t and V as input variables, where the components of V are understood to be the current values of x, y and z. It should compute the corresponding derivatives and return them in a single column vector. (b) The next step is to test your function by calling it from the command line with values like t = 0, x = 1, y = 2 and z = 3? Once you get your function working, you should make it a silent function before calling ode45. (c) Assuming that Step 2 works, you can use ode45 to estimate the solution for the time interval t 0 = 0, t e = 30 with the initial condition x = 1, y = 2 and z = 3. (d) Use plot3 to plot the trajectory of x, y and z. Solution: Let’s start by writing our rate of change function, here called lorenz. Listing 10.4 lorenz.m 1 function res = lorenz (t,V) 2 % Unpact vector V 3 x = V(1); 4 y = V(2); 5 z = V(3); 6 7 % Parameter list 8 sigma = 10; 9 b = 8/3; 10 r = 28; 11 12 % Compute the rates of change 13 dxdt = sigma*(y-x); 14 dydt = x*(r-z)-y; 15 dzdt = x*y-b*z; 16 17 % Return the rates as a column vector 18 res = [dxdt; dydt; dzdt]; 19 end Before moving on, we can test its use with t = 0, x = 1, y = 2, and z = 3. Remember that lorenz only takes two arguments, so the values of the function will need to be passed as a single vector. (You can use either column or row since a single index is needed in either case.) >> lorenz(0,[1;2;3]) ans = 10 23 -6 10.4 Examples 371 Now let’s solve the system of ODEs over the time interval t 0 = 0 to t e = 30 subject to the initial conditions x(0) = 1, y(0) = 2, and z(0) = 3. I will store the results to a vector and matrix. I will then separate the matrix that contains the values of the functions x, y and z as a function of time into separate column vectors, which I can use to make a plot of our solution and of our phase plot. >> >> >> >> >> >> >> >> [T,M]= ode45(@lorenz,[0,30],[1,2,3]); X = M(:,1); Y= M(:,2); Z = M(:,3); plot(T,X,'r-',T,Y,'b-',T,Z,'g-') xlabel('t') ylabel('x, y, or z') legend('x','y','z') And here is the resulting plot: 50 x y z 40 30 x, y, or z 20 10 0 -10 -20 -30 0 5 10 15 20 25 t Figure 10.9 ode45(@lorenz,[0,30],[1,2,3]) And now for the phase plot: >> >> >> >> plot3(X,Y,Z,'b-') xlabel('x') ylabel('y') zlabel('z') With the resulting plot: 30 372 Chapter 10 Systems of ODEs 50 40 z 30 20 10 0 50 20 10 0 0 -10 y -50 -20 x Figure 10.10 Lorenz phase plot In Spring 2018 I had a student discover on accident that MATLAB actually has a built-in lorenz function. The built-in function is actually a MATLAB GUI, that solves this exact problem. You need just call the function lorenz without any inputs. There is actually a lotka function too, but MATLAB does not provide enough information with regards to how to run it or what exactly it does. 10.5 Euler returns! In Chapter 9 we used Euler’s method to solve first order ordinary differential equations (ODEs). We can use Euler’s equation here too for systems of ODEs. The key is that given values of our functions at t 0 , we can propagate forward in time to t 0 + ∆t . You will notice the function is written using vectors and matrices. While to someone new to MATLAB this may appear less readable, this makes the code highly generalized. It can be used to solve a system of an arbitrary number of equations. Without further delay, here it is: 10.5 Euler returns! 373 Listing 10.5 euler_ode_system.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 % function [T,Y] = euler_ode_system(fcn_handle, Trange, dt, IC) % Function to use Euler's method to solve a system of % differential equations. % % Required input: % fcn_handle: The function handle to the function to % compute the rate of change of our % functions. Rates can be returned as % either a column or row vector. % Trange: Time vector where Trange = [t0, tfinal] % dt: the time step % IC: Intial condition vector. If we have functions % X and Y, this is IC = [X0, Y0]. However, it % is designed for an arbitrary number of % of equations. Note that I assume it is a row vector. % The effects my assigment of the initial conditions to % the first row of the Y matrix. % function [T,Y] = euler_ode_system(fcn_handle, Trange, dt, IC) % % Un-packing Trange. The first element is t0 and the second is Tfinal t0 = Trange(1); tfinal = Trange(2); % % % T T With a fixed timestep, we can create a vector of times at which we will evaluate our function. I will then take the transpose to get a column vector like ode45. = t0:dt:tfinal; = T'; % Pre-size our Y matrix. It will be setup like a matrix just like ode45 % would use. The number of rows will be the same as the length of T. % The number of columns will be the same as the number of functions, % which is the length of IC Y = zeros(length(T),length(IC)); % % Store the initial conditions row 1 of Y. Y(1,:) = IC; % Looping over all times. We will start with i=2 since i=1 just % corresponds to our initial conditions. I could start at i=1, but then % need to shift my indices. for i=2:length(T) DY = fcn_handle(T(i-1),Y(i-1,:)); % Note I need to transpose DY. It is a column vector as returned by % the rate function. I will add to it the i-1 row of my Y vector, % which is row vector. Y(i,:) = DY'*dt+Y(i-1,:); end end 374 Chapter 10 Systems of ODEs Using Euler’s method to solve our Lotka-Voltera model example with a step size of 0.1 days: >> [T,M]=euler_ode_system(@lotka,[0,365],0.1,[100,10]); >> plot(T,M) 120 100 80 60 40 20 0 0 50 100 150 200 250 300 350 400 Figure 10.11 Using Euler’s method to solve our Lotka-Voltera model example. Which appears to be in good agreement with ode45. Nice! But to be sure, let’s compare to our results using ode45. The peaks seem to be growing, which isn’t exactly right. >> hold on >> [Tr,Mr]=ode45(@lotka,[0,365],[100,10]); >> plot(Tr,Mr(:,1),'bo',Tr,Mr(:,2),'ro') 10.6 Isothermal Batch Reactors: Multiple Reactions 375 120 100 80 60 40 20 0 0 50 100 150 200 250 300 350 400 Figure 10.12 Comparing Euler to ode45 with a step size of 0.1 days. Oh no! The results do differ and appear to get worse with time. Remember, the errors in the calculation get propagated with time as each step is dependent on the previous. Let’s try a smaller timestep. >> [T2,M2]=euler_ode_system(@lotka,[0,365],0.01,[100,10]); >> plot(T2,M2,'-k') Perfect! Now that we have spent some time working with Euler’s method, have you thought of any ways to improve it? The main limitation is that we evaluate the rate of change at t 0 , and assume that it is constant over the range t 0 to t 0 + ∆t . This is correct in the limit that ∆t → 0. (Put differently, we are assuming the function is linear over this range.) One scheme to improve Euler’s method is to first use Euler’s method to estimate the value of the function and it’s derivative forward in time. Then, return to t 0 (or an earlier time) and use these predictions to improve our estimate of the rate of the function. While I certainly do not give it justice here, this is the general idea behind the predictor-corrector scheme of numerically solving ODEs. 10.6 Isothermal Batch Reactors: Multiple Reactions In Section 8.14 we analyzed an isothermal continuous stirred tank reactor (CSTR), and then in exercise 8.7 you analyzed parallel and series reactions in an isothermal CSTR. In Section 9.12 we analayzed an isothermal batch reactor with a single reaction. Here will build upon this and consider parallel and series reactions in 376 Chapter 10 Systems of ODEs an isothermal batch reactor. We will discuss in detail the system here (which is very similar to exercise 8.7, and then you will perform the analysis on your own for exercise 10.7. Okay, so now that we have a grasp of batch reactor basics, let’s consider the case of multiple reactions. We will have two scenarios. First we will have the case of parallel reactions. For this case, the reactant (or reactants) is consumed by two or more different reactions. For our exercise we will consider the case: A→B (P.1) A →C (P.2) The second case will be series reactions, in which the reactant (or reactants) form an intermediate product which reacts further to form another product. For our exercise we will consider the case: A→B (S.1) B →C (S.2) When labeling the equations I use the pre-fix “P” and “S” just to remind myself that the equations correspond to parallel and series reactions, receptively. In our expressions I will just use the number of the equation. In both cases, B will be our desired product and C will be an undesired product. When quantifying reaction progress, we define the selectivity of B relative to C (or desired product relative to undesired product) as S B /C = nB nC How does our mole balance design equation change? Well, before when we had a single reaction we had dnA = νAV r dt Now, we just need to sum over each reaction. We will use a subscript 1 to correspond to reaction 1 and a subscript 2 to correspond to reaction 2. (Note that both the rate r and stoichiometic coefficient ν A will depend on the reaction number. Also note that it is possible to have a stoichiometric coefficient of 0.) This gives the net rate of reaction for the parallel case: dnA = ν A,1V r 1 + ν A,2V r 2 dt For this problem, we need to know both n B and nC . Remember for a single reaction the number of moles of each species is related by stoichiometry. This will not be the case here with our multiple reaction. We will therefore have a system of ODEs to solve; a mole balance design equation for component B and C, and since 10.6 Isothermal Batch Reactors: Multiple Reactions our rate expression is dependent on A, solve for A too. For the parallel reaction case we have: dnA = ν A,1V r 1 + ν A,2V r 2 dt d nB = νB,1V r 1 + νB,2V r 2 dt d nC = νC ,1V r 1 + νC ,2V r 2 dt And for the series reaction case we have the same set of equations. What will change is the numerical values of the stoichiometric coefficients and the expression for r 2 . dnA = ν A,1V r 1 + ν A,2V r 2 dt d nB = νB,1V r 1 + νB,2V r 2 dt d nC = νC ,1V r 1 + νC ,2V r 2 dt Since we are starting with just A, our initial conditions will be: n 0A = 2400 n B0 = 0 nC0 = 0 For reaction 1 (both parallel and series), use the same parameters as before, namely, r 1 = k 1C A k 1 = k 0,1 exp[−E 1 /(RT )] k 0,1 = 2.4 × 108 s−1 E 1 = 15.3kcal/mol For reaction 2 for the parallel reactions (P.2), r 2 = k 2C A k 2 = k 0,2 exp[−E 2 /(RT )] E 2 = 15.3kcal/mol And for reaction 2 for the series reactions (S.2), r 2 = k 2C B k 2 = k 0,2 exp[−E 2 /(RT )] E 2 = 15.3kcal/mol 377 378 Chapter 10 Systems of ODEs 10.7 Glossary state: If a system can be described by a set of variables, the values of those variables are called the state of the system. phase plot: A plot that shows the state of a system as point in the space of possible states. trajectory: A path in a phase plot that shows how the state of a system changes over time. 10.8 Exercises The first 5 questions are actually one long exercise. However, I have broken it up into small parts in hopes of guiding you to the final solution. As I have mentioned previously, I view MATLAB as powerful educational tool. We can set-up a model for a physical system of interest, and then perform virtual experiments looking at the effect of perturbing variables. And since we are not concerned about solving problems analytically, we can hypothesize about how to make our model more realistic, and then readily test our hypothesis. Exercise 10.1 We started the chapter by considering a Lotka-Voltera predator-prey model. The Lotka-Voltera model describes the interactions between two species in an ecosystem, a predator and its prey. A common example is rabbits and foxes. The model is governed by the following system of differential equations: R 0 = aR − bRF F 0 = ebRF − cF where • R is the population or rabbits • F is the population of foxes • a is the natural growth rate of rabbits in the absence of predation • c is the natural death rate of foxes in the absence of prey • b is the death rate of rabbits per interaction with a fox • e is the efficiency of turning eaten rabbits into foxes Let’s begin by making sure we have our model set-up correctly. Use the following values of the parameters: a = 0.1, b = 0.01, c = 0.1, and e = 0.2. Also, assume we initially have 100 rabbits and 10 foxes (R(0) = 100 and F (0) = 10). Solve for the population of rabbits over the course of 365 days. (Note that day is the units of time used in the model.) While in reality we can not have a fractional number of rabbits and foxes, on day 365 I find the model predicts we have 22.6552 rabbits and 6.7519 foxes. Use these numbers to confirm your code is working correctly before moving on. 10.8 Exercises 379 Exercise 10.2 Plotting the rabbit and fox population over the period of 365 days, we find they exhibit oscillatory behavior. With this set of parameters, the maximum rabbit population is the same as the initial population. You will notice that in our model (a) the growth rate of rabbits in the absence of predation and (c) the death rate of foxes in the absence of prey are equivalent. What happens if the growth rate of rabbits were to double, a = 0.2, keeping everything else the same? Plot your results. Comment on the how the maximum value of the rabbit and fox population change, and also on how the oscillatory behavior changes. Does this make physical sense? Restore a back to its original value of a = 0.1. Now modify the death rate of foxes, c. If we make c larger, then foxes have a shorter life expectancy. And if we make c smaller, then the foxes live longer. What is the effect of changing c? Does this make physical sense? Exercise 10.3 A limitation of the Lotka-Voltera model is the rabbit growth term in the absence of predation. Specifically, if there are no foxes, then R 0 = aR Again using a = 0.1 and an initial rabbit population of 100, solve for the population of rabbits over the course of 365 days. You will find this corresponds to an exponential growth of rabbits, and after 365 days we have 7.1095 × 1017 rabbits. Wow! The predicted behavior is not physical. While the population may grow exponentially initially, the population must be restricted by resource availability. Exercise 10.4 One way to fix the unreasonable exponential growth of rabbits is to instead use a two-term logistic growth model of the form R 0 = aR − a 2 R k where k is the carrying capacity of the system. That is, the maximum number of rabbits that the system can support. Again use a = 0.1 and an initial rabbit population of 100. Try using k = 150 (a carrying capacity greater than the initial population) and k = 50 (a carrying capacity less than the initial population). Solve for the population of rabbits over the course of 365 days. What do the results look like? Does this model seem more appropriate to you? Exercise 10.5 Let’s return to our original system of equations, but now let’s replace the exponential growth term with the two-term logistic growth model: ³ a ´ R 0 = aR − R 2 − bRF k F 0 = ebRF − cF Let’s continue to use the same set of parameters: a = 0.1, b = 0.01, c = 0.1, and e = 0.2. Also, assume again we initially have 100 rabbits and 10 foxes (R(0) = 100 and F (0) = 10). Let’s look at the effect of the carrying capacity on the system. In the introduction and Problem 1, we were effectively looking at the case of k → ∞. For that case we observed oscillatory behavior. Now use k = 150. Solve for the population of rabbits over the course of 365 days. Plot the results. How does the behavior of the system change? As a reference to make sure your code is working correctly, at 365 days I find the population of rabbits and foxes to be 50.0189 and 6.6493, respectively. To further understand what is going on, solve for 730 days (2 years) or maybe even 10 years. How cool! 380 Chapter 10 Systems of ODEs What I find interesting is the effect of the carrying capacity on the long-time population of the system. By long-time, let’s solve for a period of 3,650 days (10 years). Try using value of k of 50, 100, 200, 300, 400, 1000. Plot the population and comment on how the population after 10 years changes with k. Exercise 10.6 Compartmental models simplify the mathematical modelling of infectious diseases. Here we will consider the simplified SIR model wherein the population is assigned with labels Susceptible, Infectious, or Recovered. The SIR model may be used to model the dynamics of an epidemic such as the flu or more recently COVID-19, which was crucial for guiding public policy decisions. If you are interested in more details, please have a look at the report “Modeling epidemics with differential equations.” See also the excellent (and free during the pandemic) article “Use of a Modified SIRD Model to Analyze COVID-19 Data.” In that work the authors improve upon the SIR model similar to how we improved upon the standard Lotka-Voltera model. Here we will stick with the standard SIR model for simplicity, but you can experiment for fun after the course is complete. SARS-COV-2 is the virus that causes COVID-19. How does SARS-COV-2 spread? From the news we have learned that SARS-COV-2 spreads between people in close contact (the reason for social distancing), and through respiratory droplets produced when an infected person coughs, sneezes, or talks (the reason for wearing masks). To model the spread, We will apply three labels to the population. 1. S- people are healthy but susceptible to getting infected 2. I- people are infected 3. R- people recover We will assume that: 1. At any time the total population is conserved (i.e., everyone recovers and no one dies in our model) 2. Susceptible people get sick by interacting with infected people The resulting SIR model takes the form: S 0 = −r SI 0 I = r SI − γI R 0 = γI where • S is the fraction of the population that is susceptible, • I is the fraction of the population that is infectious, • R is the fraction of the population that is recovered, • r is the rate constant representative of how quickly people are leaving the susceptible group and getting infected, • γ is the rate constant representative of how quickly infected people are recovering. 10.8 Exercises 381 Initially, at t = 0, assume R = 0, I = 1×10−6 , and R + I +S = 1. Assuming a value of r = 0.05 and γ = 0.03, solve for the value of S, I , and R over the range t = 0 to 1000 days. The goal of social distancing and wearing masks is to decrease the value of r . What happens as r decreases? By developing treatments for COVID-19, such as antivirals, the goal is to decrease the value of γ. What happens as γ decreases? Exercise 10.7 For the parallel and series reactions described in section 10.6, I would like you to plot the composition of each species versus time and the selectivity of B relative to C versus time. For the parallel case, I would like you to consider: k 0,2 = 0.1k 0,1 , k 0,2 = k 0,1 , and k 0,2 = 10k 0,1 . For the series case I would like you to consider: k 0,2 = 0.1k 0,1 , k 0,2 = k 0,1 , and k 0,2 = 10k 0,1 . Compare the behavior of parallel and series reactions, and discuss how you might “optimize” your reactor in both cases. Please summarize your findings in a short report supported with plots. How do your results compare to that of a CSTR operated with the same space time? (That is, compare to your results from Exercise 8.7.) Note: at t = 0, n B = CC = 0. You should therefore only plot the selectivity of B relative to C versus time for t > 0. Exercise 10.8 In the last exercise, we reconsidered our systems of reactions from Exercise 8.7, but now used a batch reactor instead of a CSTR. Here we will do the same and revisit our system of reactions from Exercise 8.2, but again using a batch reactor instead of a CSTR. Reactants A and B can react irreversibly to produce either a desired product, D, or an undesired product, U, as shown in the equations below: A +B → D (1) A +B →U (2) The corresponding rate expressions are given by: ½ ¾ ¡ ¢ −15300J/mol r 1 = 1.12 × 102 min−1 exp CA RT ¾ ½ ¡ ¢ −23700J/mol 2 −1 CB r 2 = 1.87 × 10 min exp RT At t = 0 a 25 gallon batch reactor is charged with a liquid solution containing 10 mol A per gallon and 12 mol B per gallon at 350 K. The batch reactor is operated isothermally for 2 minutes. What is the conversion of the limiting reagent? What is the selectivity (in mol D per mol U)? The limiting reagent will be the reactant that will be consumed first. Here, for each mol of A consumed a mol of B is consumed. The limiting reagent will therefore be the species with the smallest concentration in the feed. Why would the conversion with respect the limiting reagent be preferred? Again, compare your results to Exercise 8.2, where the CSTR was operated for an equivalent space time. 11 Chapter Second-order systems Last chapter we learned how to numerically solve systems of first order initial value ordinary differential equations. We will build upon this in Chapter 11 and will solve higher order differential equations. The key will be to re-write our higher order differential equation as a series of first order differential equations. By the end of this chapter you will be able to: • Explain how to take higher order ODEs and re-write them as a system of first order ODEs • Apply ode45 to model projectile trajectories • Demonstrate ability to update ode45 call to specify an “Event” • Analyze the role of drag on projectile trajectories If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 11.1 Nested functions In the Section 6.1 (on page 173), in listing 6.1 we saw an example of an M-file with more than one function. I will copy listing 6.1 here but will remove the comments to save space. 383 384 Chapter 11 Second-order systems t When we first discussed functions, I recommended the MATLAB documentation page “Share Data Between Workspaces.” The best practice for sharing data is by passing arguments, which includes the use of anonymous functions. GNU Octave makes a similar recommendation. Now that we have extensive experience working with functions, I introduce nested functions, which once they understand how they work, many students find to be a very convenient alternative to using anonymous functions. Listing 11.1 v_virial.m 1 2 3 4 function res = v_virial( t,p_bar,tc,pc_bar,omega ) p = p_bar*1e5; pc = pc_bar*1e5; r = 8.314; 5 tr = t/tc; 6 7 b0 = 0.083-0.422/(tr^(1.6)); b1 = 0.139-0.172/(tr^(4.2)); b = r*tc/pc*(b0+omega*b1); 8 9 10 11 z = 1 + b*p/(r*t); 12 13 vig = v_ideal_gas(t,p_bar); 14 15 16 17 end res = z*vig; 18 19 20 21 22 23 24 function res = v_ideal_gas( t,p_bar ) p = p_bar*1e5; r = 8.314; v = r*t/p; res = v*(100^(3)); end Because the first function ends before the second begins, they are at the same level of indentation. Functions like these are parallel, as opposed to nested. A nested function is defined inside another, like this: 11.1 Nested functions 385 Listing 11.2 v_virial_2.m 1 2 3 4 function res = v_virial_2( t,p_bar,tc,pc_bar,omega ) p = p_bar*1e5; pc = pc_bar*1e5; r = 8.314; 5 tr = t/tc; 6 7 b0 = 0.083-0.422/(tr^(1.6)); b1 = 0.139-0.172/(tr^(4.2)); b = r*tc/pc*(b0+omega*b1); 8 9 10 11 z = 1 + b*p/(r*t); 12 13 vig = v_ideal_gas(t,p_bar); 14 15 res = z*vig; 16 17 function res = v_ideal_gas( t,p_bar ) p = p_bar*1e5; r = 8.314; v = r*t/p; res = v*(100^(3)); end 18 19 20 21 22 23 24 25 end The top-level function, v_virial_2, is the outer function and v_ideal_gas is an inner function. Nesting functions is useful because the variables of the outer function can be accessed from the inner function, and likewise the variables of the inner function can be accessed by the outer function. This is not possible with parallel functions. In this example, using a nested function makes it so that we no longer need to pass the temperature and pressure to v_ideal_gas, and we additionally no longer need to redefine the gas constant. t Note that in the MATLAB documentation, the outer function is the parent function, and the inner function (or functions) is the nested function 386 Chapter 11 Second-order systems Listing 11.3 v_virial_3.m 1 2 3 4 function res = v_virial_3( t,p_bar,tc,pc_bar,omega ) p = p_bar*1e5; pc = pc_bar*1e5; r = 8.314; 5 tr = t/tc; 6 7 b0 = 0.083-0.422/(tr^(1.6)); b1 = 0.139-0.172/(tr^(4.2)); b = r*tc/pc*(b0+omega*b1); 8 9 10 11 z = 1 + b*p/(r*t); 12 13 vig = v_ideal_gas(); 14 15 res = z*vig; 16 17 function res = v_ideal_gas() v = r*t/p; res = v*(100^(3)); end 18 19 20 21 22 23 end The function v_ideal_gas can access the necessary variables r, t, and p from v_virial_3. This obviates the need to pass any variables to v_ideal_gas, making it easier to test and debug v_virial_3. You will notice that when the scope of a variable spans multiple functions, the MATLAB Editor’s default behavior is to color the variable cyan. That is, if a variable here is mutual to both v_virial_3 and v_ideal_gas, it is colored cyan. Before moving on, let’s repeat the calculation of Section 6.1 to ensure we get the same result in all cases. >> v_virial(440,60,425.125,37.960,0.201) ans = 313.2382 >> v_virial_2(440,60,425.125,37.960,0.201) ans = 313.2382 >> v_virial_3(440,60,425.125,37.960,0.201) ans = 313.2382 11.1 Nested functions 387 11.1.1 Application fzero As another application, let’s revisit our duck buoyancy problem, Example 7.2. Specifically, let’s look at Listing 7.7 on page 228 which I have copied below: Listing 11.4 duck2.m 1 2 3 4 5 6 7 8 % res = duck2(x,r,rho) % Function to find the required depth of a duck to remain % buoyant in water. duck2 requires that we pass parameters % r (radius of duck) and rho (density of the liquid), in % addition to the depth (x) which is what we will solve for. function res = duck2( x,r,rho ) res = 0.3.*4.*r.^(3)-rho.*(3.*r.*x.^(2)-x.^(3)); end In the problem we were solving for the depth (d ) to which a duck of radius r was submerged in water of density ρ using fzero. fzero expects a function to which we pass only a single variable. We therefore specified r and ρ in the function to solve. In order to generalize the function to allow us to pass r and ρ too, we had to use an anonymous function (see Section 7.16 on page 227) Here, let’s instead use a nested function to simplify the task, and to use an unmodified call to fzero. To accomplish this, I will create a top function duck_zero to which I will pass r and ρ, and it will return the depth d . I will then nest duck2. In this way, duck2 can “see” the value of r and ρ in the top function, so that d is the only variable that now needs to be passed. To solve for d , I will add the call to fzero to the top function, and save the result to res to be returned. Let’s do it! Listing 11.5 duck_zero.m 1 2 function res = duck_zero( r, rho ) res = fzero(@duck2,[0,r]); 3 function res = duck2( x ) res = 0.3*4*r^(3)-rho*(3*r*x^(2)-x^(3)); end 4 5 6 7 8 end >> r = 10; >> rho = 1; >> d = duck_zero(r, rho) d = 7.2651 This is in perfect agreement with our previous solution. We will look at an application to ode45, which has a restriction of two input variables, later in this chapter. Application to fsolve is also straightforward. t Remember, an anonymous function is restricted to a single executable statement. There is no such restriction in a nested function. 388 Chapter 11 Second-order systems 11.2 Newtonian motion Newton’s second law of motion is often written like this F = ma where F is the net force acting on a object, m is the mass of the object, and a is the resulting acceleration of the object. In a simple case where the object is moving along a straight line, F and a are scalars, but in general they are vectors. Even more generally, if F and a vary in time, then they can be thought of as functions that return vectors; that is, F is a function and the result of evaluating F (t ) is a vector that describes the net force at time t . So a more explicit way to write Newton’s law is ~ (t ) = m~ ∀t : F a (t ) The arrangement of this equation suggests that if you know m and a you can compute force, which is true, but in most physical simulations it is the other way around. Based on a physical model, you know F and m, and compute a. So if you know acceleration, a, as a function of time, how do you find the position of the object, z? Well, we know that acceleration is the second derivative of position, so we can write a differential equation z 00 = a Where a and z are functions of time that return vectors, and z 00 is the second time derivative of z. Because this equation includes a second derivative, it is a second-order ODE. ode45 can’t solve this equation in this form, but by introducing a new variable, v, for velocity, we can rewrite it as a system of first-order ODEs. z0 = v v0 = a t Screen casts are available for Professor Paluch working through the following three sections: Freefall, Air resistance, and Parachute. They should be viewed as a series of screen casts that build-off of each other. The first equation says that the first derivative of z is v; the second says that the derivative of v is a. Note that while in this chapter we will focus on examples from Newtonian mechanics, the same strategy can be used in general to solve higher order (second order and higher) ODEs; we can convert the ODE to a system of first order ODEs, which we learned to solve in the previous chapter. 11.3 Freefall Let’s start with a simple example, an object in freefall in a vacuum (where there’s no air resistance). Near the surface of the earth, the acceleration of gravity is g = −9.8 m/s2 , where here the minus sign indicates that gravity pulls down. 11.3 Freefall 389 If the object falls straight down (in the same direction as gravity), we can describe its position with a scalar value, altitude. So this will be a one-dimensional problem, at least for now. Here is a rate function we can use with ode45 to solve this problem: Listing 11.6 freefall.m 1 2 3 4 function res = freefall(t, X) % Un-pack z = X(1); % the first element is position v = X(2); % the second element is velocity 5 % Compute rates dzdt = v; dvdt = acceleration(t, z, v); 6 7 8 9 10 11 12 end % Pack the rates up as a column vector res = [dzdt; dvdt]; 13 14 15 16 17 function res = acceleration(t, z, v) g = -9.8; % acceleration of gravity in m/s^2 res = g; end The first function is the rate function. It gets t and X as input variables, where the elements of X are understood to be position and velocity. The return value from freefall is a (column) vector that contains the derivatives of position and velocity, which are velocity and acceleration, respectively. Computing z 0 is easy because we are given velocity as an element of X. The only thing we have to compute is acceleration, which is what the second function does. acceleration computes acceleration as a function of time, position and velocity. In this example, the net acceleration is a constant, so we don’t really have to include all this information yet, but we will soon. Here’s how to run ode45 with this rate function: >> ode45(@freefall, [0, 30], [4000, 0]) As always, the first argument is the function handle, the second is the time interval (30 seconds) and the third is the initial condition: in this case, the initial altitude is 4000 meters and the initial velocity is 0. So you can think of the “object” as a skydiver jumping out of an airplane at about 12,000 feet. Here’s what the result looks like: 390 Chapter 11 Second-order systems 4000 3500 3000 2500 2000 1500 1000 500 0 -500 0 5 10 15 20 25 30 Figure 11.1 ode45(@freefall,[0,30],[4000,0]) The bottom line shows velocity starting at zero and dropping linearly. The top line shows position starting at 4000 m and dropping parabolically (but remember that this parabola is a function of time, not a ballistic trajectory). While in some cases it may be nice to plot both position and velocity on the same graph using the same y-axis, this is not always the case. Let’s re-solve, storing to variables, and then plot each on its own graph. >> [T,M] = ode45(@freefall, [0, 30], [4000, 0]); >> Z = M(:,1); >> V = M(:,2); We can now readily plot position versus time and velocity versus time, each in their own graph. However, they do have a common x-axis, so it may be nice to “stack” the graphs. By this, I mean create two graphs, but have them lined up in a column. Since we have two graphs, we can think of this as a single column with two rows. Here is how we do it: >> >> >> >> >> >> >> >> figure subplot(2,1,1) % First plot in 2 by 1 matrix plot(T,Z,'-r') ylabel('position [m]') subplot(2,1,2) % Second plot in 2 by 1 matrix plot(T,V,'-b') ylabel('velocity [m/s]') xlabel('time [s]') 11.3 Freefall 391 Here’s what the result looks like: position [m] 4000 2000 0 -2000 0 5 10 0 5 10 15 20 25 30 15 20 25 30 velocity [m/s] 0 -100 -200 -300 time [s] Figure 11.2 Creating a stacked plot of our results. Notice that ode45 doesn’t know where the ground is, so the skydiver keeps going through zero into negative altitude. No worries, we will fix this. Let’s solve for the time it takes to hit the ground. Contact is made when z = 0. Remember when we use ode45, the only times we know exactly are the initial and final time. So let’s modify freefall by adding a new top function freefall_time, such that the user can pass the final time and the function returns z at the final time. We can then use fzero with this function to find the time at which contact is made. 392 Chapter 11 Second-order systems Listing 11.7 freefall_time.m 1 2 3 4 5 6 7 function res = freefall_time(t) % Solve over the range 0 to t [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end 8 9 10 11 12 function res = freefall(t, X) % Un-pack z = X(1); % the first element is position v = X(2); % the second element is velocity 13 % Compute rates dzdt = v; dvdt = acceleration(t, z, v); 14 15 16 17 18 19 20 end % Pack the rates into a column vector res = [dzdt; dvdt]; 21 22 23 24 25 function res = acceleration(t, z, v) g = -9.8; % acceleration of gravity in m/s^2 res = g; end >> t_contact = fzero(@freefall_time,[0.1,30]) t_contact = 28.5714 Great! Let’s keep going. Let’s update our function so that the fzero call is contained in the function file. To do this, let’s create a new top function freefall_contact. The function need not take any input variables, but it will return the time it takes to make contact. 11.3 Freefall 393 Listing 11.8 freefall_contact.m 1 2 3 4 function res = freefall_contact() t_contact = fzero(@freefall_time,[0.1,30]); res = t_contact; end 5 6 7 8 9 10 11 12 function res = freefall_time(t) % Solve over the range 0 to t [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end 13 14 15 16 17 function res = freefall(t, X) % Un-pack z = X(1); % the first element is position v = X(2); % the second element is velocity 18 % Compute rates dzdt = v; dvdt = acceleration(t, z, v); 19 20 21 22 23 24 25 end % Pack the rates into a column vector res = [dzdt; dvdt]; 26 27 28 29 30 function res = acceleration(t, z, v) g = -9.8; % acceleration of gravity in m/s^2 res = g; end >> t_contact = freefall_contact() t_contact = 28.5714 Nice! What’s that, keep going? Okay! Knowing the time at which we make contact is great, but it would be even cooler if we knew the velocity too. We can do this two ways, the second will be more efficient than the first. But first, since we now know the time of contact, we could use this to call ode45 again, use this value as the final time, and then return the contact time and velocity. Let’s do it! 394 Chapter 11 Second-order systems Listing 11.9 freefall_contact_b.m 1 2 3 4 5 6 function [t_contact, v_contact] = freefall_contact_b() t_contact = fzero(@freefall_time,[0.1,30]); [T,M] = ode45(@freefall, [0, t_contact], [4000, 0]); V = M(:,2); v_contact = V(end); end 7 8 9 10 11 12 13 function res = freefall_time(t) [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end 14 15 16 17 function res = freefall(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity 18 dzdt = v; dvdt = acceleration(t, z, v); 19 20 21 22 23 end res = [dzdt; dvdt]; % pack the results in a column vector 24 25 26 27 28 function res = acceleration(t, z, v) g = -9.8; % acceleration of gravity in m/s^2 res = g; end >> [sol_t, sol_v] = freefall_contact_b() sol_t = 28.5714 sol_v = -280 Note that 280 m/s is 626.342 miles per hour, ouch! The second, more efficient way to solve this problem is using a nested function. We know that the last time fzero will call freefall_time is when the solution is found. Therefore, the last row of M contains our desired solution. Remember that when we nest a function, the inner function can see the workspace of outer function, and the outer function can see the workspace of the inner function. Therefore, if we nest freefall_time in freefall_contact, freefall_contact will see the final value of M, and we therefore do not need to call ode45 an additional time. (This is what makes it more efficient.) 11.3 Freefall 395 Listing 11.10 freefall_contact_c.m 1 function [t_contact,v_contact] = freefall_contact_c() 2 t_contact = fzero(@freefall_time,[0.1,30]); V = M(:,2); v_contact = V(end); 3 4 5 6 function res = freefall_time(t) [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end 7 8 9 10 11 12 13 14 end 15 16 17 18 function res = freefall(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity 19 dzdt = v; dvdt = acceleration(t, z, v); 20 21 22 23 24 end res = [dzdt; dvdt]; % pack the results in a column vector 25 26 27 28 29 function res = acceleration(t, z, v) g = -9.8; % acceleration of gravity in m/s^2 res = g; end >> [sol_t, sol_v] = freefall_contact_c() sol_t = 28.5714 sol_v = -280 Perfect! And since freefall_contact will see the final value of T and M, we could plot the complete trajectory if we would like. Running freefall_contact_c MATLAB R2018a additionally prints the following warning message: 396 Chapter 11 Second-order systems Warning: File: freefall_contact_c.m Line: 7 Column: 12 Defining "M" in the nested function shares it with the parent function. In a future release, to share "M" between parent and nested functions, explicitly define it in the parent function. This is a pretty clear warning message, and only started to appear when I upgraded to MATLAB 2018a. Essentially, if we are using a nested function to share variables, all shared variables should be created in the outer (parent) function. And in future releases “should” will become a “must,” so we should get in the habit of doing so. Listing 11.11 freefall_contact_d.m 1 2 3 4 5 6 7 function [t_contact,v_contact] = freefall_contact_d() % We can use empty brackets to define a variable without assigning it a % value. This corresponds to an empty matrix. M = []; t_contact = fzero(@freefall_time,[0.1,30]); V = M(:,2); v_contact = V(end); 8 function res = freefall_time(t) [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end 9 10 11 12 13 14 15 16 end 17 18 19 20 function res = freefall(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity 21 dzdt = v; dvdt = acceleration(t, z, v); 22 23 24 25 26 end res = [dzdt; dvdt]; % pack the results in a column vector 27 28 29 30 31 function res = acceleration(t, z, v) g = -9.8; % acceleration of gravity in m/s^2 res = g; end 11.4 Air resistance 397 I created a variable without assigning a value using empty brackets; this is equivalent to creating an empty matrix. Note that if I had instead used M =1 instead, the result is unchanged. As a very final note before moving on, if you wished, you could nest acceleration within freefall. The advantage of doing so would be that you would no longer need to pass t, z, and v. t Screen casts are available 11.4 Air resistance To make this simulation more realistic, we can add air resistance (or drag). For large objects moving quickly through air, the force due to air resistance, called “drag,” is proportional to v 2 : for Professor Paluch working through the following three sections: Freefall, Air resistance, and Parachute. They should be viewed as a series of screen casts that build-off of each other. F drag = c v 2 Where c is a drag constant that depends on the density of air, the cross-sectional area of the object and the surface properties of the object. For the purpose of this problem, let’s say that c = 0.2. To convert from force to acceleration, we have to know mass, so let’s say that the skydiver (with equipment) weighs 75 kg. The acceleration can then be computed using Newton’s second law: a drag = F drag /m. Here’s a version of acceleration that takes air resistance into account (you don’t have to make any changes in freefall, I just renamed it freefall_2 so as not to be confused with the case of no drag). Recall that while gravity is pulling the skydiver down, drag acts in the opposite direction of motion; in this case where the skydiver is falling down, drag is pushing the skydiver up. Listing 11.12 freefall_2.m 1 2 3 function res = freefall_2(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity 4 5 6 dzdt = v; dvdt = acceleration(t, z, v); 7 8 9 10 11 12 13 14 15 16 17 res = [dzdt; dvdt]; % pack end function res = acceleration(t, z, a_grav = -9.8; % c = 0.2; % m = 75; % f_drag = c * v^2; % a_drag = f_drag / m; % res = a_grav + a_drag; % end the results in a column vector v) acceleration of gravity in m/s^2 drag constant mass in kg drag force in N drag acceleration in m/s^2 total acceleration t In your fluid mechanics class you likely saw the drag equation: F drag = 12 ρv 2C D A, where ρ is the density of the fluid (here air), C D is the drag coefficient which is a function of Reynold’s number, and A is the reference cross-sectional area (here the cross-sectional area of the skydiver). We lump these parameters into a single parameter (drag constant), c for convenience. 398 Chapter 11 Second-order systems The sign of the drag force (and acceleration) is positive as long as the object is falling, the direction of the drag force is up; the drag force always acts opposite to the direction of motion (or the velocity). The net acceleration is the sum of gravity and drag. Be careful when you are working with forces and accelerations; make sure you only add forces to forces or accelerations to accelerations. In my code, I use comments to remind myself what units the values are in. That helps me avoid nonsense like adding forces to accelerations. Here’s what the result looks like with air resistance: >> ode45(@freefall_2, [0, 30], [4000, 0]) 4000 3500 3000 2500 2000 1500 1000 500 0 -500 0 5 10 15 20 25 30 Figure 11.3 ode45(@freefall_2,[0,30],[4000,0]) Big difference! With air resistance, velocity increases until the drag acceleration equals g ; after that, velocity is a constant, known as “terminal velocity,” and position decreases linearly (and much more slowly than it would in a vacuum). To examine the results more closely, we can assign the position and velocity to variables >> [T, M] = ode45(@freefall_2, [0, 30], [4000, 0]); And let’s again create a stacked plot. >> >> >> >> Z = M(:,1); V = M(:,2); figure subplot(2,1,1) % First plot in 2 by 1 matrix 11.4 Air resistance >> >> >> >> >> >> 399 plot(T,Z,'-r') ylabel('position [m]') subplot(2,1,2) % Second plot in 2 by 1 matrix plot(T,V,'-b') ylabel('velocity [m/s]') xlabel('time [s]') Here’s what the result looks like: position [m] 4000 3500 3000 2500 2000 0 5 10 0 5 10 15 20 25 30 15 20 25 30 velocity [m/s] 0 -20 -40 -60 -80 time [s] Figure 11.4 Creating a stacked plot of our results with air resistance. Lastly, let’s update freefall_c to use our updated acceleration file to solve for the time to contact and the corresponding velocity. 400 Chapter 11 Second-order systems Listing 11.13 freefall_2_contact.m 1 2 3 4 5 function [t_contact,v_contact] = freefall_2_contact() M = []; % Creating variable without assigning a value. t_contact = fzero(@freefall_time,[0.1,90]); V = M(:,2); v_contact = V(end); 6 function res = freefall_time(t) [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end 7 8 9 10 11 12 13 14 end 15 16 17 18 function res = freefall(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity 19 20 21 dzdt = v; dvdt = acceleration(t, z, v); 22 23 24 25 26 27 28 29 30 31 32 res = [dzdt; dvdt]; % pack the results in a column vector end function res = acceleration(t, z, v) a_grav = -9.8; % acceleration of gravity in m/s^2 c = 0.2; % drag constant m = 75; % mass in kg f_drag = c * v^2; % drag force in N a_drag = f_drag / m; % drag acceleration in m/s^2 res = a_grav + a_drag; % total acceleration end >> [t_contact,v_contact] = freefall_2_contact() t_contact = 70.2693 v_contact = -60.6218 We find that it takes longer to contact the ground, and the velocity at impact is smaller. (Here it is 135.6 miles per hour.) The final velocity here corresponds to the terminal velocity. 11.4 Air resistance 401 Example 11.1 Let’s increase the mass of the skydiver and confirm that terminal velocity increases. This relationship is the source of the intuition that heavy objects fall faster; in air, they do! (In vacuum, they don’t! Have you ever seen the famous Apollo 15 hammer and feather experiment?) In your solution, you can just update the mass in acceleration. But could you also think of how to allow the user the specify the mass in the Command Window? Solution: Let’s double the mass from 75 kg to 150 kg. To solve, let’s update freefall_2_contact to use the larger mass, and to also create a stacked plot of the position and velocity versus time. Listing 11.14 freefall_3_contact.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function [t_contact,v_contact] = freefall_3_contact() M = []; T=[]; Z=[]; % Creating variables without assigning a value. t_contact = fzero(@freefall_time,[0.1,90]); V = M(:,2); v_contact = V(end); % Now create the stacked plot too. Here the final % time will be the contact time. figure subplot(2,1,1) % First plot in 2 by 1 matrix plot(T,Z,'-r') ylabel('position [m]') subplot(2,1,2) % Second plot in 2 by 1 matrix plot(T,V,'-b') ylabel('velocity [m/s]') xlabel('time [s]') print('-depsc','freefall_stack_3.eps') function res = freefall_time(t) [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end end function res = freefall(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity dzdt = v; dvdt = acceleration(t, z, v); end res = [dzdt; dvdt]; % pack the results in a column vector 402 Chapter 11 Second-order systems function res = acceleration(t, z, v) a_grav = -9.8; % acceleration of gravity in m/s^2 c = 0.2; % drag constant m = 150; % mass in kg f_drag = c * v^2; % drag force in N a_drag = f_drag / m; % drag acceleration in m/s^2 res = a_grav + a_drag; % total acceleration end Solving: >> [t_sol,v_sol] = freefall_3_contact() t_sol = 52.7186 v_sol = -85.7311 So the heavier person takes less time to contact the ground, and has a higher terminal velocity. And here is the resulting plot: 4000 position [m] 3000 2000 1000 0 0 10 20 0 10 20 30 40 50 60 30 40 50 60 0 -20 velocity [m/s] 37 38 39 40 41 42 43 44 -40 -60 -80 -100 time [s] Figure 11.5 Creating a stacked plot of our results with air resistance where we have doubled the mass of the object. We again find that after a short period of time the position decreases linearly. Now how could we allow the user to specify the mass in the Command Window? Well, the only function we can call from the Command Window is the top function, freefall_3_contact. The mass will therefore need to be passed to freefall_3_contact. And then to get the mass to acceleration, we can make sure it is nested in the top function. But we can not nest just acceleration. Since it is called by freefall, we will need to nest it too. So in the final version of my code, I will rename the top function freefall_3m_contact, and 11.4 Air resistance 403 freefall_time, freefall, and acceleration will all be parallel with each other (at the same level) but nested in freefall_3m_contact. Also, I add as inputs to freefall_3m_contact the mass, m, and a string that will contain the name of the resulting figure, fname. Also, within acceleration we delete the line where the mass was previously assigned. Listing 11.15 freefall_3m_contact.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 function [t_contact,v_contact] = freefall_3m_contact(m,fname) M = []; T=[]; Z=[]; % Creating variables without assigning a value. t_contact = fzero(@freefall_time,[0.1,90]); V = M(:,2); v_contact = V(end); % Now create the stacked plot too. Here the final % time will be the contact time. figure subplot(2,1,1) % First plot in 2 by 1 matrix plot(T,Z,'-r') ylabel('position [m]') subplot(2,1,2) % Second plot in 2 by 1 matrix plot(T,V,'-b') ylabel('velocity [m/s]') xlabel('time [s]') print('-depsc',fname) function res = freefall_time(t) [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end function res = freefall(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity dzdt = v; dvdt = acceleration(t, z, v); end res = [dzdt; dvdt]; % pack the results in a column vector function res = acceleration(t, z, v) a_grav = -9.8; % acceleration of gravity in m/s^2 c = 0.2; % drag constant f_drag = c * v^2; % drag force in N a_drag = f_drag / m; % drag acceleration in m/s^2 res = a_grav + a_drag; % total acceleration end 404 Chapter 11 Second-order systems end Solving: >> >> [t_sol,v_sol] = freefall_3m_contact(75,'freefall_3_75m.eps') t_sol = 70.2693 v_sol = -60.6218 4000 position [m] 3000 2000 1000 0 -1000 0 10 20 30 0 10 20 30 40 50 60 70 80 40 50 60 70 80 0 velocity [m/s] 44 -20 -40 -60 time [s] Figure 11.6 [t_sol,v_sol] = freefall_3m_contact(75,’freefall_3_75m.eps’) >> >> [t_sol2,v_sol2] = freefall_3m_contact(150,'freefall_3_150m.eps') t_sol2 = 52.7186 v_sol2 = -85.7311 11.5 Parachute! 405 4000 position [m] 3000 2000 1000 0 0 10 20 0 10 20 30 40 50 60 30 40 50 60 0 velocity [m/s] -20 -40 -60 -80 -100 time [s] Figure 11.7 [t_sol2,v_sol2] = freefall_3m_contact(150,’freefall_3_150m.eps’) Nice! 11.5 Parachute! In the previous section, we saw that the terminal velocity of a 75 kg skydiver is about 60 m/s, which is about 130 mph. If you hit the ground at that speed, you would almost certainly be killed. That’s where parachutes come in. Example 11.2 Modify acceleration so that after 30 seconds of free-fall the skydiver deploys a parachute, which (almost) instantly increases the drag constant to 2.7. What is the terminal velocity now? How long (after deployment) does it take to reach the ground? Solution: Let’s update the acceleration function in freefall_3_contact such that when t > 30 the drag constant is 2.7. We will also restore the mass back to 75 kg, and with the use of a parachute, we expect it to take longer to hit the ground. We will therefore increase the upper bracket initially used by fzero. We will name this new file parachute_contact. t Screen casts are available for Professor Paluch working through the following three sections: Freefall, Air resistance, and Parachute. They should be viewed as a series of screen casts that build-off of each other. 406 Chapter 11 Second-order systems Listing 11.16 parachute_contact.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 function [t_contact,v_contact] = parachute_contact() M = []; T=[]; Z=[]; % Creating variables without assigning a value. t_contact = fzero(@freefall_time,[0.1,300]); V = M(:,2); v_contact = V(end); % Now create the stacked plot too. Here the final % time will be the contact time. figure subplot(2,1,1) % First plot in 2 by 1 matrix plot(T,Z,'-r') ylabel('position [m]') subplot(2,1,2) % Second plot in 2 by 1 matrix plot(T,V,'-b') ylabel('velocity [m/s]') xlabel('time [s]') print('-depsc','parachute.eps') function res = freefall_time(t) [T,M] = ode45(@freefall, [0, t], [4000, 0]); % The first column of M is position, the second is velocity Z = M(:,1); res = Z(end); end end function res = freefall(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity dzdt = v; dvdt = acceleration(t, z, v); res = [dzdt; dvdt]; % pack the results in a column vector end function res = acceleration(t, z, v) a_grav = -9.8; % acceleration of gravity in m/s^2 c = 0.2; % drag constant % After 30 seconds the parachute is deployed if t > 30 c = 2.7; end m = 75; % mass in kg f_drag = c * v^2; % drag force in N a_drag = f_drag / m; % drag acceleration in m/s^2 res = a_grav + a_drag; % total acceleration end 11.5 Parachute! 407 Solving: >> [t_sol,v_sol] = parachute_contact() t_sol = 176.5233 v_sol = -16.5020 So with the parachute it takes much longer to hit the ground, and the terminal velocity is much smaller. The value of 16.5 m/s corresponds to approximately 37 miles per hour. And here is the resulting plot: 4000 position [m] 3000 2000 1000 0 -1000 0 20 40 60 80 100 120 140 160 180 0 20 40 60 80 100 120 140 160 180 velocity [m/s] 0 -20 -40 -60 time [s] Figure 11.8 Creating a stacked plot parachute example. The plots are very cool. After a short time, we reach our terminal velocity. The velocity is constant and the position decreases linear. Then at 30 seconds we deploy the parachute. We get a step change in the velocity, which quickly reaches a new terminal velocity. The position continues to decrease linearly, only the slope is smaller because the new terminal velocity is smaller in magnitude. This solution is good and correct, and updating our code was very straightforward. However, given that we have a step change in the drag constant at 30 s, we could alternatively use ode45 to solve from 0 to 30 s, then take the conditions at 30 s and use ode45 to solve from 30 s to the final time. When we solve from 0 to 30 s we will use a a drag constant of 0.2. Then from 30 s to the final time we will use a drag constant of 2.7. By breaking up the integration (ode45) into these two step, we can avoid having a step change in the drag constant within the integration range. To facilitate switching from a drag constant of 0.2 to 2.7, I will nest the functions freefall and acceleration within parachute_2 so that they can all see the flag variable defined in parachute_2 to indicate which drag constant should be used. Here I will only solve for the contact time using fzero. 408 Chapter 11 Second-order systems Listing 11.17 parachute_2.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function res = parachute_2(tfinal) t0 = 0; % initial time t1 = 30;% end of first integration range z0 = 4000; % initial altitude v0 = 0; % initial velocity flag = 0; % use c = 0.2; [T, M] = ode45(@freefall, [t0, t1], [z0, v0]); v1 = M(end,2); % velocity after 30 s z1 = M(end,1); % altitude after 30 s flag = 1; % use c = 2.7 [T, M] = ode45(@freefall, [t1, tfinal], [z1, v1]); res = M(end,1); % altitude at time t % Uncomment when you want to call parachute % and display the velocity. %M(end,2) function res = freefall(t, X) z = X(1); % the first element is position v = X(2); % the second element is velocity dzdt = v; dvdt = acceleration(t,z,v); end res = [dzdt; dvdt]; % pack the results in a column vector function res = acceleration(t,z,v) a_grav = -9.8; % acceleration of gravity in m/s^2 if flag == 0 % solving from 0 to 30 s c = 0.2; else c = 2.7; % solving from 30 s to tfinal end m = 75; % mass in kg f_drag = c * v^2; % drag force in N a_drag = f_drag / m; % drag acceleration in m/s^2 res = a_grav + a_drag; % total acceleration end end Solving in this manner we have: >> sol = fzero(@parachute_2,150) sol = 176.5302 In this case we have only a very small change of 0.0069 s. So we see that ode45 is 11.6 Two dimensions 409 robust and adaptive, and can perform well even when integrating step changes. So the moral of the story is, here I would stick with using parachute_contact. 11.6 Two dimensions So far we have used ode45 for a system of first-order equations and for a single second-order equation. The next logical step is a system of second-order equations, and the next logical example is a projectile. A “projectile” is an object propelled through space, usually toward, and often to the detriment of, a target. If a projectile stays in a plane, we can think of the system as two-dimensional, with x representing the horizontal distance traveled and y representing the height or altitude. So now instead of a skydiver, think of a circus performer being fired out of a cannon (a “human cannonball”). According to Wikipedia, the record distance for a human cannonball is 61.06 meters (just over 200 feet). 11.6.1 Flying in vacuum Here is a general framework for the rate function for computing the trajectory of a projectile in two dimensions using ode45. We will begin with a projectile in vacuum, and then add air resistance (drag) later. Listing 11.18 projectile.m 1 2 3 function res = projectile(t, W) P = W(1:2); V = W(3:4); 4 dPdt = V; dVdt = acceleration(t, P, V); 5 6 7 8 9 end res = [dPdt; dVdt]; 10 11 12 13 14 function res = acceleration(t, P, V) g = 9.8; % acceleration of gravity in m/s^2 res = [0; -g]; end The second argument of the rate function is a vector, W, with four elements. The first two are assigned to P, which represents position; the last two are assigned to V, which represents velocity. P and V are vectors with elements for the x and y components. I perform a “partial” un-packing of W because the governing ODEs in 410 Chapter 11 Second-order systems the x and y direction will be the same. The only difference will be the acceleration in each direction, but that will be handled in the separate acceleration function. The result from acceleration is also a vector; ignoring air resistance (for now), the acceleration in the x direction is 0; in the y direction it’s −g . Other than that, this code is similar to what we saw in Section 11.3. Note that if you wished to perform a “complete” un-packing of W you could, and here is what that might look like: Listing 11.19 projectile_a.m 1 2 3 4 5 6 7 function res = projectile_a(t, W) % Position, x then y px = W(1); py = W(2); % Velocity, x then y vx = W(3); vy = W(4); 8 % Computing rates dpxdt = vx; dpydt = vy; dvxdt = acceleration_x(t,px,vx); dvydt = acceleration_y(t,py,vy); 9 10 11 12 13 14 15 16 17 end % Packing up the rates res = [dpxdt; dpydt; dvxdt; dvydt]; 18 19 20 21 function res = acceleration_x(t, px, vx) res = 0; end 22 23 24 25 26 function res = acceleration_y(t, py, vy) g = 9.8; % acceleration of gravity in m/s^2 res = -g; end Use what makes most sense to you, but I will build off of projectile. If we launch the human cannonball from an initial height of 3 meters (x 0 = 0 m, y 0 = 3 m), with initial velocities of 40 m/s and 40 m/s in the x and y direction (v x,0 = v y,0 = 40 m/s), the ode45 call would look like this: >> ode45(@projectile, [0,10], [0, 3, 40, 40]); 11.6 Two dimensions 411 And the result looks like this: 400 350 300 250 200 150 100 50 0 -50 -100 0 2 4 6 8 10 Figure 11.9 ode45(@projectile,[0,10],[0,3,40,40]) You might have to think a little to figure out which line is which, but we will try to clarify that momentarily. But it looks like the flight time is about 6 seconds. Let’s make three improvements to our code. 1) First, it is unlikely that we are provided with values of the initial velocity in the x and y direction. It is more likely that we are provided with the initial velocity (v 0 , or speed) and the launch angle θ0 . We could use this to compute the x and y component as: v x,0 = v 0 cos (θ0 ) and v y,0 = v 0 sin (θ0 ). 2) Let’s add a top function projectile_model that for now takes no input, but solves the system of ODEs, generates plots, and returns the values of our functions back to the Command Window. 3) Plot the trajectory in the x y plain. Let’s update our code, and solve for the same conditions as before, but here let’s use an initial velocity of 60 m/s with a launch angle of 45 degrees. 412 Chapter 11 Second-order systems Listing 11.20 projectile_model.m 1 function [T,X,Y,VX,VY] = projectile_model() 2 % The initial position in m x0 = 0; y0 = 3; 3 4 5 6 % The initial velocity in m/s and launch angle in degrees v0 = 60; theta0 = 45; 7 8 9 10 % Computing the x and y component of the initial velocity vx0 = v0*cosd(theta0); vy0 = v0*sind(theta0); 11 12 13 14 % The time range to solve over in s t0 = 0; tfinal = 10; 15 16 17 18 % Solving our ODE [T,M] = ode45(@projectile,[t0,tfinal],[x0,y0,vx0,vy0]); 19 20 21 % Let's break M up. The columns will correspond to our functions in the % order x,y,vx,vy, the same order used by the initial conditions and % within the rate function. X = M(:,1); Y = M(:,2); VX = M(:,3); VY = M(:,4); 22 23 24 25 26 27 28 29 % Next, let's plot the trajectory in the xy plane figure(1) plot(X,Y,'-k') xlabel('x position [m]') ylabel('y position [m]') title('Trajectory in xy plan') print('-depsc','xy_trajectory.eps') 30 31 32 33 34 35 36 37 38 end 39 40 41 42 43 function res = projectile(t, W) P = W(1:2); V = W(3:4); 11.6 Two dimensions 413 dPdt = V; dVdt = acceleration(t, P, V); 44 45 46 47 48 end res = [dPdt; dVdt]; 49 51 52 53 function res = acceleration(t, P, V) g = 9.8; % acceleration of gravity in m/s^2 res = [0; -g]; end >> [T,X,Y,VX,VY] = projectile_model(); Trajectory in xy plan 100 80 60 40 y position [m] 50 20 0 -20 -40 -60 -80 0 50 100 150 200 250 300 350 400 450 x position [m] Figure 11.10 projectile_model() Cool! When we solved and stored the results to variables, T was a vector of times at which our functions were evaluated. M was again a matrix. Each column of M corresponds to a function, in the order that we are solving. Columns 1 to 4 are x, y, v x , and v y , respectively, where v x and v y correspond to the velocity in the x and y direction, respectively. The rows of M correspond to the rows (times) of T at which the functions were evaluated. The order of the columns agreed with the order of our functions when specifying our initial conditions, the order of un-packing the input vector to our rate function, and the order of the vector of computed rates in our rate function. We solved over the range of 0 to 10 s. When solving using ode45 we need to specify a time range. However, this is an example where we do not know 414 Chapter 11 Second-order systems the correct final time to use. We see that toward the end of the trajectory the y position is negative. This would correspond to the human cannonball tunneling through the Earth! Next, let’s update our code so that our trajectory ends when contact is made with Earth. This would correspond to when y = 0. To accomplish this, I re-save projectile_model as projectile_model_2, and update the name of the top function accordingly. I then nest within this top function an error function named projectile_solve. Within this error function I solve the system of ODEs over the range t 0 to t , and return the y position at t . This will allow me to add an fzero call in the top function to solve for t when y = 0. The last time fzero calls the error function with when we find the desired t . By nesting, we save a final call to ode45 and it facilitates passing parameters to the error function. Here is my updated code and solution. Listing 11.21 projectile_model_2.m 1 function [T,X,Y,VX,VY] = projectile_model_2() 2 3 4 5 % The initial position in m x0 = 0; y0 = 3; 6 7 8 9 % The initial velocity in m/s and launch angle in degrees v0 = 60; theta0 = 45; 10 11 12 13 % Computing the x and y component of the initial velocity vx0 = v0*cosd(theta0); vy0 = v0*sind(theta0); 14 15 16 17 % The initial time. We will solve for the final time as the time % required to make contact, y=0. (This criteria can also be updated.) t0 = 0; 18 19 20 21 22 23 24 25 26 % Solving for the time required to make contact using fzero. The error % function will be nested. The final time fzero calls the error % function will correspond to when t is when contact is made. By % nesting we will not need to resolve at this time. The error function % is just solving the ODEs over the rante t0 to t, and returning the y % position at t. M = []; % Creating variable M without assign a value. t_impact = fzero(@projectile_solve,[0.1,100]); 27 28 29 30 % Let's break M up. The columns will correspond to our functions in the % order x,y,vx,vy, the same order used by the initial conditions and % within the rate function. 11.6 Two dimensions X = M(:,1); Y = M(:,2); VX = M(:,3); VY = M(:,4); 31 32 33 34 35 % Next, let's plot the trajectory in the xy plane figure(1) plot(X,Y,'-k') xlabel('x position [m]') ylabel('y position [m]') title('Trajectory in xy plan') print('-depsc','xy_trajectory.eps') 36 37 38 39 40 41 42 43 function res = projectile_solve(t) % Solving our ODE over the range t0 to t [T,M] = ode45(@projectile,[t0,t],[x0,y0,vx0,vy0]); 44 45 46 47 48 49 end 50 % Returning the y position at t res = M(end,2); 51 52 end 53 54 55 56 function res = projectile(t, W) P = W(1:2); V = W(3:4); 57 dPdt = V; dVdt = acceleration(t, P, V); 58 59 60 61 62 end res = [dPdt; dVdt]; 63 64 65 66 67 function res = acceleration(t, P, V) g = 9.8; % acceleration of gravity in m/s^2 res = [0; -g]; end >> [T,X,Y,VX,VY] = projectile_model_2(); 415 416 Chapter 11 Second-order systems Trajectory in xy plan 100 90 80 y position [m] 70 60 50 40 30 20 10 0 0 50 100 150 200 250 300 350 400 x position [m] Figure 11.11 projectile_model_2() Excellent! Building off of this code, the number of questions we can ask is endless. One particularly fun question would be for an initial velocity v 0 , what is the optimal launch angle? From our general physics class we know this is 45 degrees. But can we demonstrate that here? I start by saving a copy of projectile_model_2 as projectile_model_angle, update the name of the top function accordingly, and update the function so that it only returns the x distance traveled and does not generate a plot. Great. So now we have a function that will return the distance traveled for a given initial launch angle. Next, we just build upon it to create a vector of initial angles which we will loop over and compute the distance traveled for each case. We will then plot the distance traveled versus launch angle, and return the appropriate vectors. Here it is: 11.6 Two dimensions Listing 11.22 projectile_model_angle.m 1 function [Theta0,X_impact] = projectile_model_angle() 2 3 4 5 % The initial position in m x0 = 0; y0 = 3; 6 7 8 % The initial velocity in m/s v0 = 60; 9 10 11 % Vector of initial launch angles in degrees Theta0 = 30:1:60; 12 13 14 15 % Computing the x and y component of the initial velocity Vx0 = v0*cosd(Theta0); Vy0 = v0*sind(Theta0); 16 17 18 19 % The initial time. We will solve for the final time as the time % required to make contact, y=0. (This criteria can also be updated.) t0 = 0; 20 21 22 % Creating a vector to store the distance travled to X_impact = zeros(1,length(Theta0)); 23 24 25 26 27 28 29 30 31 % Looping over initial launch angles and solving for the distance % traveled. This will be collected in a matrix. M = []; % Creating variables without assiging a value for i = 1:length(Theta0) t_impact = fzero(@projectile_solve,[0.1,100]); % Collect the x distance travled at impact X_impact(i) = M(end,1); end 32 33 34 35 36 37 % Generate a plot of distance travled vs launch angle plot(Theta0,X_impact,'-ok') xlabel('Launch angle [degrees]') ylabel('Distance traveled [m]') print('-depsc','optimal_launch_angle.eps') 38 39 40 41 function res = projectile_solve(t) % Solving our ODE over the range t0 to t [T,M] = ode45(@projectile,[t0,t],[x0,y0,Vx0(i),Vy0(i)]); 42 43 % Returning the y position at t 417 418 Chapter 11 Second-order systems 44 end 45 res = M(end,2); 46 47 end 48 49 50 51 function res = projectile(t, W) P = W(1:2); V = W(3:4); 52 dPdt = V; dVdt = acceleration(t, P, V); 53 54 55 56 57 end res = [dPdt; dVdt]; 58 60 61 62 function res = acceleration(t, P, V) g = 9.8; % acceleration of gravity in m/s^2 res = [0; -g]; end >> [Theta0,X_impact] = projectile_model_angel(); 380 370 360 Distance traveled [m] 59 350 340 330 320 310 30 35 40 45 50 55 60 Launch angle [degrees] Figure 11.12 projectile_model_angle() From the plot we find that the maximum distance traveled is with an initial launch angle of 45 degrees. If you wanted to identify the maximum in the resulting vec- 11.6 Two dimensions 419 tors (which could be included in your code), you can use the built-in function max, which returns the maximum element of a vector. >> max_x_dist = max(X_dist) max_x_dist = 370.3228 >> theta_opt = Theta0( X_impact==max_x_dist ) theta_opt = 45 11.6.2 Flying with air resistance Next, let’s add air resistance (drag) to this simulation. In the skydiver scenario, we estimated that the drag constant was 0.2, but that was based on the assumption that the skydiver is falling flat. A human cannonball, flying head-first, probably has a drag constant closer to 0.1. Let’s build-up our code to ultimately solve for the initial velocity required to achieve the record flight distance of 61.06 m. Before jumping in, I have admittedly been lax in my presentation of the drag equation. In the skydiver scenario, we had a one-dimensional trajectory where the y position was always decreasing with time. While gravity always acts in the negative y direction, drag acts opposite to motion. So in the skydiver scenario, drag always acted in the positive y direction. What about our projectile problem? If we consider our trajectory, for x, motion will always be in the positive x direction. However for y, motion will initially be in the positive y direction, then go through an extremum, and the be in the negative y direction. The more proper way to express the force due to drag is: F drag = −c v 2 v̂ where v̂ is a unit vector that gives the direction of motion, so that −v̂ has the effect of having F drag act in the opposite direction of the motion. Computing v̂ using MATLAB is straightforward using the built-in sign function. Let’s start by updating projectile_model_2 (Listing 11.21) to include drag. We will assume our human cannonball has the same mass as our skydiver, 75 kg. To include the effect of drag, we need just update the acceleration function. I will also update the code to return only the distance traveled. I will name this updated code projectile_resistance. 420 Chapter 11 Second-order systems Listing 11.23 projectile_resistance.m 1 function res = projectile_resistance() 2 3 4 5 % The initial position in m x0 = 0; y0 = 3; 6 7 8 9 % The initial velocity in m/s and launch angle in degrees v0 = 60; theta0 = 45; 10 11 12 13 % Computing the x and y component of the initial velocity vx0 = v0*cosd(theta0); vy0 = v0*sind(theta0); 14 15 16 17 % The initial time. We will solve for the final time as the time % required to make contact, y=0. (This criteria can also be updated.) t0 = 0; 18 19 20 21 22 23 24 25 26 % Solving for the time required to make contact using fzero. The error % function will be nested. The final time fzero calls the error % function will correspond to when t is when contact is made. By % nesting we will not need to resolve at this time. The error function % is just solving the ODEs over the rante t0 to t, and returning the y % position at t. M = []; % Creating variable M without assign a value. t_impact = fzero(@projectile_solve,[0.1,100]); 27 28 29 30 31 32 33 34 % Let's extract X and Y from M to facilitate plotting. We will also % return the distance traveled. X = M(:,1); Y = M(:,2); %VX = M(:,3); %VY = M(:,4); res = X(end); 35 36 37 38 39 40 41 42 43 % Next, let's plot the trajectory in the xy plane figure(1) plot(X,Y,'-k') xlabel('x position [m]') ylabel('y position [m]') title('Trajectory in xy plan') print('-depsc','xy_trajectory.eps') 11.6 Two dimensions function res = projectile_solve(t) % Solving our ODE over the range t0 to t [T,M] = ode45(@projectile,[t0,t],[x0,y0,vx0,vy0]); 44 45 46 47 48 49 end 50 % Returning the y position at t res = M(end,2); 51 52 end 53 54 55 56 function res = projectile(t, W) P = W(1:2); V = W(3:4); 57 dPdt = V; dVdt = acceleration(t, P, V); 58 59 60 61 62 end res = [dPdt; dVdt]; 63 64 65 66 67 68 69 70 71 72 function res = acceleration(t, P, V) g = 9.8; % acceleration of gravity in m/s^2 % c = 0.1; % drag constant m = 75; % mass in kg Vhat = sign(V); % unit vector giving direction of velocity % componetns f_drag = -c * V.^(2) .* Vhat; % drag force in N a_drag = f_drag ./ m; % drag acceleration in m/s^2 73 % Return as vector acceleration in x and y direction. % In the x direction is just drag. In the y direction it is drag - g. res = [a_drag(1); a_drag(2)-g]; 74 75 76 77 78 end >> x_dist = projectile_resistance() x_dist = 287.7751 421 422 Chapter 11 Second-order systems Trajectory in xy plan 90 80 70 y position [m] 60 50 40 30 20 10 0 -10 0 50 100 150 200 250 300 x position [m] Figure 11.13 projectile_resistance() We find that including air resistance, the distance traveled is shorter. The distance traveled is almost 288 m, which tells me the initially velocity is... very large considering the world record is about 61 m. Next, let’s solve for the initial velocity required to achieve the world record of 61.06 m traveled. To do this, I will start by keeping the initial launch angle fixed at 45 degrees and add the initial velocity as an input variable. This gives us a function where we pass the initial velocity and it returns the distance traveled. 11.6 Two dimensions Listing 11.24 projectile_resistance_2.m 1 function res = projectile_resistance_2(v0) 2 3 4 5 % The initial position in m x0 = 0; y0 = 3; 6 7 8 9 % The initial velocity in m/s and launch angle in degrees %v0 = 60; theta0 = 45; 10 11 12 13 % Computing the x and y component of the initial velocity vx0 = v0*cosd(theta0); vy0 = v0*sind(theta0); 14 15 16 17 % The initial time. We will solve for the final time as the time % required to make contact, y=0. (This criteria can also be updated.) t0 = 0; 18 19 20 21 22 23 24 25 26 % Solving for the time required to make contact using fzero. The error % function will be nested. The final time fzero calls the error % function will correspond to when t is when contact is made. By % nesting we will not need to resolve at this time. The error function % is just solving the ODEs over the rante t0 to t, and returning the y % position at t. M = []; % Creating variable M without assign a value. t_impact = fzero(@projectile_solve,[0.1,100]); 27 28 29 30 31 32 33 34 % Let's extract X and Y from M to facilitate plotting. We will also % return the distance traveled. X = M(:,1); Y = M(:,2); %VX = M(:,3); %VY = M(:,4); res = X(end); 35 36 37 38 39 40 41 42 43 % Next, let's plot the trajectory in the xy plane figure(1) plot(X,Y,'-k') xlabel('x position [m]') ylabel('y position [m]') title('Trajectory in xy plan') print('-depsc','xy_trajectory.eps') 423 424 Chapter 11 Second-order systems function res = projectile_solve(t) % Solving our ODE over the range t0 to t [T,M] = ode45(@projectile,[t0,t],[x0,y0,vx0,vy0]); 44 45 46 47 48 49 end 50 % Returning the y position at t res = M(end,2); 51 52 end 53 54 55 56 function res = projectile(t, W) P = W(1:2); V = W(3:4); 57 dPdt = V; dVdt = acceleration(t, P, V); 58 59 60 61 62 end res = [dPdt; dVdt]; 63 64 65 66 67 68 69 70 71 72 function res = acceleration(t, P, V) g = 9.8; % acceleration of gravity in m/s^2 % c = 0.1; % drag constant m = 75; % mass in kg Vhat = sign(V); % unit vector giving direction of velocity % componetns f_drag = -c * V.^(2) .* Vhat; % drag force in N a_drag = f_drag ./ m; % drag acceleration in m/s^2 73 % Return as vector acceleration in x and y direction. % In the x direction is just drag. In the y direction it is drag - g. res = [a_drag(1); a_drag(2)-g]; 74 75 76 77 78 end I could keep updating the code, but it is also easy enough to solve in the Command Window. >> projectile_error = @(v) projectile_model_resistance_2(v) - 61.06; >> vmin = fzero(projectile_error,[0.1,60]) vmin = 24.5105 The minimum velocity required is 24.5105 m/s; that’s about 54.8 mph. You will 11.6 Two dimensions notice that each time fzero calls projectile_model_resistance_2 it generates a plot. We can avoid this using the following updated code: Listing 11.25 projectile_resistance_3.m 1 function res = projectile_resistance_3() 2 3 4 5 % Creating variables without assiging an initial value X = []; Y = []; 6 7 res = fzero(@projectile_error,[0.1,60]); 8 9 10 11 12 13 14 15 % Next, let's plot the trajectory in the xy plane figure(1) plot(X,Y,'-k') xlabel('x position [m]') ylabel('y position [m]') title('Trajectory in xy plan') print('-depsc','xy_trajectory.eps') 16 17 function res = projectile_error(v0) 18 19 20 21 % The initial position in m x0 = 0; y0 = 3; 22 23 24 25 % The initial velocity in m/s and launch angle in degrees %v0 = 60; theta0 = 45; 26 27 28 29 % Computing the x and y component of the initial velocity vx0 = v0*cosd(theta0); vy0 = v0*sind(theta0); 30 31 32 33 % The initial time. We will solve for the final time as the time % required to make contact, y=0. (This criteria can also be updated.) t0 = 0; 34 35 36 37 38 39 40 % % % % % % Solving for the time required to make contact using fzero. The error function will be nested. The final time fzero calls the error function will correspond to when t is when contact is made. By nesting we will not need to resolve at this time. The error function is just solving the ODEs over the rante t0 to t, and returning the y position at t. 425 426 Chapter 11 Second-order systems M = []; % Creating variable M without assign a value. t_impact = fzero(@projectile_solve,[0.1,100]); 41 42 43 % Let's extract X and Y from M to facilitate plotting. X = M(:,1); Y = M(:,2); VX = M(:,3); VY = M(:,4); 44 45 46 47 48 49 % return the distance traveled - 61.06 m, the world record res = X(end) - 61.06; 50 51 52 function res = projectile_solve(t) % Solving our ODE over the range t0 to t [T,M] = ode45(@projectile,[t0,t],[x0,y0,vx0,vy0]); 53 54 55 56 57 58 end 59 % Returning the y position at t res = M(end,2); 60 end 61 62 63 end 64 65 66 67 function res = projectile(t, W) P = W(1:2); V = W(3:4); 68 dPdt = V; dVdt = acceleration(t, P, V); 69 70 71 72 73 end res = [dPdt; dVdt]; 74 75 76 77 78 79 80 81 82 83 84 function res = acceleration(t, P, V) g = 9.8; % acceleration of gravity in m/s^2 % c = 0.1; % drag constant m = 75; % mass in kg Vhat = sign(V); % unit vector giving direction of velocity % componetns f_drag = -c * V.^(2) .* Vhat; % drag force in N a_drag = f_drag ./ m; % drag acceleration in m/s^2 11.7 What could go wrong? 427 % Return as vector acceleration in x and y direction. % In the x direction is just drag. In the y direction it is drag - g. res = [a_drag(1); a_drag(2)-g]; 85 86 87 88 89 end >> vmin = projectile_resistance_3() vmin = 24.5105 Where here only a single plot is generated with our final answer. Cool! The minimum velocity of 54.8 mph is less than the velocity suggested by Wikipedia of over 70 mph (approximately 31.3 m/s). Likely my estimate of c was off. But that was a fun problem nonetheless. 11.7 What could go wrong? What could go wrong? Well, vertcat for one. To explain what that means, I’ll start with catenation, which is the operation of joining two matrices into a larger matrix. “Vertical catenation” joins the matrices by stacking them on top of each other; “horizontal catenation” lays them side by side. Here’s an example of horizontal catenation with row vectors: >> x = 1:3 x = 1 2 3 >> y = 4:5 y = 4 5 >> z = [x, y] z = 1 2 3 4 5 Inside brackets, the comma operator performs horizontal catenation. The vertical catenation operator is the semi-colon. Here is an example with matrices: >> X = zeros(2,3) X = 0 0 0 0 0 0 >> Y = ones(2,3) Y = 1 1 1 1 1 1 >> Z = [X; Y] 428 Chapter 11 Second-order systems Z = 0 0 1 1 0 0 1 1 0 0 1 1 These operations only work if the matrices are the same size along the dimension where they are glued together. If not, you get: >> a = 1:3 a = 1 2 3 >> b = a' b = 1 2 3 >> c = [a, b] Error using horzcat Dimensions of matrices being concatenated are not consistent. >> c = [a; b] Error using vertcat Dimensions of matrices being concatenated are not consistent. In this example, a is a row vector and b is a column vector, so they can’t be catenated in either direction. Reading the error messages, you probably guessed that horzcat is the function that performs horizontal catenation, and likewise with vertcat and vertical catenation. These operations are relevant to projectile because of the last line, which packs dPdt and dVdt into the output variable: 1 2 3 function res = projectile(t, W) P = W(1:2); V = W(3:4); 4 dPdt = V; dVdt = acceleration(t, P, V); 5 6 7 8 9 end res = [dPdt; dVdt]; As long as both dPdt and dVdt are column vectors, the semi-colon performs vertical catenation, and the result is a column vector with four elements. But 11.8 ODE Events if either of them is a row vector, that’s trouble. ode45 expects the result from projectile to be a column vector, so if you are working with ode45, it is probably a good idea to make everything a column vector. In general, if you run into problems with horzcat and vertcat, use size to display the dimensions of the operands, and make sure you are clear on which way your vectors go. 11.8 ODE Events Normally when you call ode45 you have to specify a start time and an end time. But in many cases, you don’t know ahead of time when the simulation should end. We have seen now how we can deal with this using fzero. Additionally, MATLAB provides a mechanism for dealing with this problem directly using ode45. The bad news is that it is a little awkward. Here’s how it works: 1. Before calling ode45 you use odeset to create an object called options that contains values that control how ode45 works: >> options = odeset('Events', @event_function); In this case, the name of the option is Events and the value is a function handle. When ode45 runs, it will invoke event_function after each timestep. You can call this function anything you want, but the name event_function helps eliminate confusion. If you wish to define multiple Events in a single code, then you might expand the name to prevent mixing them up (i.e., event_function_projectile). 2. The function you provide has to take the same input variables as your rate function. For example, here is an event function that would work with projectile (Listing 11.18) from Section 11.6 to find the time when the projectile hits the ground: 429 430 Chapter 11 Second-order systems Listing 11.26 event_function.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 t Remember, you do not need to unpack W at all. It is a habit I tend to do to eliminate any confusion. You could just as well use value = W(2). % When value is equal to zero, an event is triggered. % % Set isterminal to 1 to stop the solver at the first event, % or 0 to get all events. % % direction = 0 if all zeros are to be computed (the default), % +1 if only zeros where the event function is increasing, % and -1 if only zeros where the event function is decreasing. % function [value,isterminal,direction] = event_function(t,W) py = W(2); % Extract the current height. value = py; % When value = 0, an event is triggered isterminal = 1; % Stop the integration if height crosses zero. % (Terminate after first event.) direction = -1; % But only if the height is decreasing. end t is the time, and W is a vector of the value of our functions at t. For projectile, this is the position in the x and y direction, and the velocity in the x and y direction, respectively. Here I unpack just the height since we don’t use the others. events returns three output variables: a) value determines when an event occurs. In this case value gets the second element of W, which is understood to be the height of the projectile. An “event” is a point in time when this value passes through 0. b) direction determines whether an event occurs when value is increasing (direction=1), decreasing (direction=-1), or either (direction=0). c) isterminal determines what happens when an event occurs. If isterminal=1, the event is “terminal” and the simulation stops. If isterminal=0, the simulation continues, but ode45 does some additional work to make sure that the solution in the vicinity of the event is accurate, and that one of the estimated values in the result is at the time of the event. 3. When you call ode45, you pass options as a fourth argument: >> ode45(@projectile, [0,10], [0, 3, 40, 30], options); As before, calling ode45 in this manner results in a plot: 11.8 ODE Events 431 250 200 150 100 50 0 -50 0 1 2 3 4 5 6 7 Figure 11.14 ode45(@projectile,[0,10],[0,3,40,30],options) If you wished to save the result to variables, the command would be: >> [T,M,TE,ME] = ode45(@projectile, [0,10], [0, 3, 40, 30], options); Notice the two extra outputs. TE is the time where the event occurred, and is a scalar for this case; if more than one event occurs, then it will be a vector. ME is the value of the functions when the event occurred. Here it will be a row vector of length 4, where the first element is x, second element is y, third element if v x , and fourth element is v y : >> TE TE = 6.2209 >> ME ME = 248.8347 0 40.0000 -30.9645 This is in exact agreement with our previous result using fzero. If more than one event occurs, ME will be a matrix, where each row corresponds to an the event at the corresponding time in TE. (Think of the relation between T and M, only here we consider only times when events occur.) If the use of fzero and Events give the same result, why should I consider using Events? Remember, when using fzero we solved our ODEs for several values of t until our root was found. The use of Events is much less computationally demanding. While not noticeable here, it may make a 432 Chapter 11 Second-order systems noticeable difference if it will be used many times in a code. However, I do find sometimes that with the default settings, the numerical accuracy of Events is less than fzero. Typically this is not significant for our purposes. But you can use odeset to update the tolerance used by ode45 to overcome this. You might try something like: >> options = odeset('Events',@event_function,'RelTol',1e-8,'AbsTol', 1eFor additional information, the MATLAB documentation provides two help pages I would suggest you review: “ODE Event Location” and “Summary of ODE Options”. Example 11.3 How would you modify events to stop when the height of the projectile falls through 3 m? Solution: Similar to when using fzero, an event will be triggered when value is equal to zero. So to find when the height is equal to 3 m, we will subtract 3 from the current value of the height. Additionally, notice that the problem asks when the height falls through 3 m. I will take this to refer to when the projectile is descending. We will therefor use a direction of –1. Listing 11.27 event_function_y3.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 % When value is equal to zero, an event is triggered. % % Set isterminal to 1 to stop the solver at the first event, % or 0 to get all events. % % direction = 0 if all zeros are to be computed (the default), % +1 if only zeros where the event function is increasing, % and -1 if only zeros where the event function is decreasing. % function [value,isterminal,direction] = event_function_y3(t,W) py = W(2); % Exctract the current height. value = py-3; % Desire to know when height is 3 m. % When value = 0, an event is triggered isterminal = 1; % Stop the integration if height crosses zero. % (Terminate after first event.) direction = -1; % But only if the height is decreasing. end And now to execute and find then print the height and the time to reach a height of 3 m: 11.8 ODE Events 433 >> options = odeset('Events', @event_function_y3); >> [T,M,TE,ME] = ode45(@projectile, [0,10], [0, 3, 40, 30], options); >> ME(2) ans = 3.0000 >> TE ans = 6.1224 Example 11.4 Multiple Events. Find the time when the height of the projectile falls through 3 m and when it makes impact with the ground. (The integration should be stopped upon impact.) Plot the trajectory and label when the height falls through 3 m. Solution: Let’s start by updating our event_function_y3 from the last example. Since we have two events, our variables will all be row vectors of length 2. For isterminal, for the first event we will indicate to continue the integration, and then stop at the second event. Listing 11.28 event_function_y30.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 % When value is equal to zero, and event is triggered. % % Set isterminal to 1 to stop the solver at the first event, % or 0 to get all events. % % direction = 0 if all zeros are to be computed (the default), % +1 if only zeros where the event function is increasing, % and -1 if only zeros where the event function is decreasing. % function [value,isterminal,direction] = event_function_y30(t,W) py = W(2); % Extract the current height. value = [py-3,py]; % Event happens when the height is 3 m and 0 m. isterminal = [0,1]; % Continue after the first event, stop after the second. direction = [-1,-1]; % But only if the height is decreasing. end Now to run and obtain the desired information. Since we will have two events, TE and ME contain additional information. I will print the values and then and see if you can interpret the results: 434 Chapter 11 Second-order systems >> options = odeset('Events', @event_function_y30); >> [T,M,TE,ME] = ode45(@projectile, [0,10], [0, 3, 40, 30], options); >> TE TE = 6.1224 6.2209 >> ME ME = 244.8980 248.8347 3.0000 0 40.0000 40.0000 -30.0000 -30.9645 TE is now a column vector with two rows. The first entry is the time of the first event, the second entry is the time of the second event. For ME, we now have two rows. The first row corresponds to the value of the functions at the first event, and the second row corresponds to the value of the functions at the second event. Now let’s plot the trajectory and our first event: hold on plot(M(:,1),M(:,2),'-r',ME(1,1),ME(1,2),'bo') xlabel('x [m]') ylabel('y [m]') 50 45 40 35 30 y [m] >> >> >> >> 25 20 15 10 5 0 0 50 100 150 200 x [m] Figure 11.15 ode45(@projectile,[0,10],[0,3,40,30],options) 250 11.8 ODE Events 435 Example 11.5 Finding the maximum in the trajectory. Find the time when the height of the projectile is a maximum and when it makes impact with the ground. (The integration should be stopped upon impact.) Plot the trajectory and label the maximum. Solution: As compared to the last problem, here we are asked to find a maximum in the trajectory. This will correspond to the point where d y/d t = 0, and is then decreasing after the event. (To the right of the top of the hill we are going down. Note if we were looking for a minimum the opposite would be the case.) For our problem d y/d t = v y . Listing 11.29 event_function_max.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 % When value is equal to zero, and event is triggered. % % Set isterminal to 1 to stop the solver at the first event, % or 0 to get all events. % % direction = 0 if all zeros are to be computed (the default), % +1 if only zeros where the event function is increasing, % and -1 if only zeros where the event function is decreasing. % function [value,isterminal,direction] = event_function_max(t,W) py = W(2); % Extract the current height vy = W(4); % Extract the current y velocity value = [vy,py]; % Event happens when the y velocity and height are zero isterminal = [0,1]; % Continue after the first event, stop after the second. direction = [-1,-1]; % But only if the velocity and height are decreasing. end >> options = odeset('Events', @event_function_max); >> [T,M,TE,ME] = ode45(@projectile, [0,10], [0, 3, 40, 30], options); >> TE TE = 3.0612 6.2209 >> ME ME = 122.4490 248.8347 48.9184 0 40.0000 40.0000 -0.0000 -30.9645 So the maximum occurs at 3.0612 seconds, and the maximum height is 48.9184 m. Plotting: >> >> >> >> hold on plot(M(:,1),M(:,2),'-r',ME(1,1),ME(1,2),'bo') xlabel('x [m]') ylabel('y [m]') 436 Chapter 11 Second-order systems 50 45 40 35 y [m] 30 25 20 15 10 5 0 0 50 100 150 200 250 x [m] Figure 11.16 ode45(@projectile,[0,10],[0,3,40,30],options) Example 11.6 Minimum and Maximum. Let’s revisit the Lotka-Voltera population model of Section 10.1. We used this predator and pray model to model the population of rabbits and foxes. Solving or a period of 1 year (or 365 days), we found that the rabbit and fox population went through a series of minimums and maximums. Find the time when the population of rabbits and foxes experiences a minimum and maximum. Plot these points along with the population over the course of the year. Solution: Let’s start by writing a function for Events. Since we are looking for minimum and maximum, we will need to find where the first derivative is zero. How do we get the first derivative? Use our rate function! How do we identify maximum versus minimum? For a maximum, after the maximum the function is decreasing. For minimum, after the minimum the function is increasing. 11.8 ODE Events 437 Listing 11.30 event_function_lotka.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 % When value is equal to zero, and event is triggered. % % Set isterminal to 1 to stop the solver at the first event, % or 0 to get all events. % % direction = 0 if all zeros are to be computed (the default), % +1 if only zeros where the event function is increasing, % and -1 if only zeros where the event function is decreasing. % function [value,isterminal,direction] = event_function_lotka(t,X) % Find the rate of change of r and f from the rate function, % and unpack the result rate = lotka(t,X); drdt = rate(1); dfdt = rate(2); % rabbit max and min, followed by fox max then min % For all events we continue the integration. For this problem % we know the stopping time is 365 days. value = [drdt,drdt,dfdt,dfdt]; isterminal = [0,0,0,0]; direction = [-1,1,-1,1]; end And now solving. To distinguish our events, we now add an extra output variable IE. IE will be a column vector of the same length of T, which indicates which event has occurred (here 1, 2, 3, or 4) at each time. The order of the events agrees with value, isterminal, and direction. >> >> >> IE options = odeset('Events', @event_function_lotka); [T,M,TE,VE,IE] = ode45(@lotka, [0,365], [100,10], options); IE = 1 3 2 4 1 3 2 4 1 3 2 4 1 3 2 4 1 3 438 Chapter 11 Second-order systems 2 4 1 3 2 >> TE TE = 0.0000 11.4774 27.6459 49.9308 66.1076 77.5703 93.7310 116.0370 132.2143 143.6729 159.8410 182.1584 198.3378 209.7896 225.9572 248.2904 264.4706 275.9181 292.0897 314.4344 330.6162 342.0582 358.2304 Nice! It would be great if we could isolate occurrences for each event... We can do that too! Remember logical statements as applied to vectors. >> IE==1 ans = 1 0 0 0 1 0 0 0 1 0 0 0 1 11.8 ODE Events 0 0 0 1 0 0 0 1 0 0 The result is a vectors of 1’s and 0’s. 1 correspond to true, 0 false. This tells us which events correspond to event 1 and which do not. We can pass this information to our vector TE to find all times for which event 1 occurs: >> TE(IE==1) ans = 0.0000 66.1076 132.2143 198.3378 264.4706 330.6162 The same could be done for the other events, but we will not do so here in the interest of space. Now let us plot the results of the number of rabbits and foxes versus time, and label the maximums and minimums. I will use a different color symbol for the maximum and minimum of rabbits and foxes. I will start by separating M and VE into two separate vectors, one for the population of rabbits, the other for the population of foxes. >> >> >> >> >> >> RAB = M(:,1); FOX = M(:,2); RABE = VE(:,1); FOXE = VE(:,2); hold on plot(T,RAB,'-r',T,FOX,'-b',TE(IE==1),RABE(IE==1),'ro', TE(IE==2),RABE(IE==2),'rx',TE(IE==3),FOXE(IE==3),'bo', TE(IE==4),FOXE(IE==4),'bx') >> xlabel('time [days]') >> ylabel('population') 439 440 Chapter 11 Second-order systems 120 100 population 80 60 40 20 0 0 50 100 150 200 250 300 350 400 time [days] Figure 11.17 Max and mins in our predator and prey model. Note that in the previous two examples we knew what the order of the events would be. I.e., the projectile would go through a maximum before hitting the ground. But there too IE could be very useful in identifying events as part of a larger program, and it can help reduce mistakes. 11.9 What could go wrong? The MATLAB documentation lists three limitations of the use of ODE Events: 1. If a terminal event occurs during the first step of the integration, then the solver registers the event as nonterminal and continues integrating. 2. If more than one terminal event occurs during the first step, then only the first event registers and the solver continues integrating. 3. Zeros are determined by sign crossings between steps. Therefore, zeros of functions with an even number of crossings between steps can be missed. I do not expect this to cause any issues in this class, but I thought it good to mention them. If you encounter an issue, you can look at the documentation for additional options to increase the accuracy of the integration or decrease the maximum step size. 11.10 Glossary 11.10 Glossary parallel functions: Two or more functions defined side-by-side, so that one ends before the next begins. nested function: A function defined inside another function. outer function: A function that contains another function definition. inner function: A function defined inside another function definition. The inner function can access the variables of the outer function. catenation: The operation of joining two matrices end-to-end to form a new matrix. 11.11 Exercises Exercise 11.1 For our human cannonball problem, we found that the optimal launch angle in vacuum was 45 degrees (see Listing 11.22). Let’s build-up this code. In your solution, use fzero unless told otherwise. 1. Update your code to account for air resistance (drag). What is the optimal launch angle with air resistance? You should find that it does not change much. 2. The acceleration due to drag takes the form a drag = F drag /m. We therefore expect that as mass decreases, the effect of drag increases. Compute the optimal launch angle over the range 0.04593 kg (mass of a golf ball) to 75 kg (the mass of our human cannonball). Plot the optimal launch angle versus mass and comment on your findings. 3. Similar to the last exercise, F drag is proportional to the velocity squared. How does the initial velocity effect the optimal launch angle? Consider the case of a golf ball (0.04593 kg) and our human cannonball (75 kg). Compute the optimal launch angle over a range of velocities from 5 m/s (about 11 mph) to 50 m/s (about 112 mph). Plot the optimal launch angle versus initial velocity for each case and comment on your findings. 4. Last, repeat your calculations using using ODE Events in place of fzero. Comment on the improved efficiency (and maybe readability) of your code. If you want to compare the exact execution time of the calculation using fzero and ODE Events you can use the built-in MATLAB function timeit. Imagine my function is called test and takes no input variables. Then to run and time from the Command Window, I would call the function as timeit(test). It will return the execution time in seconds. Note that it performs the calculation several times to get an average execution time, so be patient if it is taking a little longer than expected. Due to the numerical precision of ODE Events, your results may differ from those obtained using fzero. They should be close, but not exactly the same. Comment on any differences observed. Know that it is possible to update the default settings and improve the accuracy of the ODE Events calculations. An example would be: 441 442 Chapter 11 Second-order systems >> options = odeset('Events',@event_function,'RelTol',1e-8,'AbsTol', 1e-8)}; 12 Chapter Interpolation In this chapter we will learn about interpolating with MATLAB. By the end of this chapter you will be able to: • Demonstrate use of interp1 to interpolate with respect to a single independent variable • Apply interp1 to created an automated steam table If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. 12.1 Discrete and continuous maps When you solve an ODE analytically, the result is a function, which you can think of as a continuous map. When you use an ODE solver, you get two vectors (or a vector and a matrix), which you can think of as a discrete map. For example, in Section 9.4, we used the following rate function to estimate the population of rats as a function of time, Listing 9.1: 1 2 3 4 5 function res = rats(t, y) a = 0.01; omega = 2 * pi / 365; res = a * y * (1 + sin(omega * t)); end The result from ode45 is two vectors: >> [T, Y] = ode45(@rats, [0, 365], 2); T contains the time values where ode45 estimated the population; Y contains the population estimates. 443 444 Chapter 12 Interpolation Now suppose we would like to know the population on the 180th day of the year. We could search T for the value 180: >> find(T==180) ans = Empty matrix: 0-by-1 But there is no guarantee that any particular value appears in T. We can find the index where T crosses 180: >> I = find(T>180); I(1) ans = 23 I gets the indices of all elements of T greater than 180, so I(1) is the index of the first one. Then we find the corresponding value from Y: >> [T(23), Y(23)] ans = 184.3451 40.3742 That gives us a coarse estimate of the population on Day 180. If we wanted to do a little better, we could also find the last value before Day 180: >> [T(22), Y(22)] ans = 175.2201 36.6973 So the population on Day 180 was between 36.6973 and 40.3742. But where in this range is the best estimate? A simple option is to choose whichever time value is closer to 180 and use the corresponding population estimate. In the example, that’s not a great choice because the time value we want is right in the middle. So far we have seen two techniques to overcome this limitation. 1. We can create a function to solve the differential equation over the range 0 to t , and return the value at t . For this problem we would pass t = 180 and the function would return the exact population at 180 days. 2. We can use ODE Events. Here we would create an event for t = 180 (or value = t-180). In this chapter we will learn yet another technique, interpolation. Interpolating is a broadly applicable technique, not just limited to differential equations. 12.2 Interpolation t Here we will use interp1 for interpolating in 1-D. interp2, interp3, and interpn also exist to interpolate in 2-D, 3-D, and N-D, respectfully. A better option is to draw a straight line between the two points that bracket Day 180 and use the line to estimate the value in between. This process is called linear 12.2 Interpolation 445 interpolation, and MATLAB provides a function named interp1 that does it: >> pop = interp1(T, Y, 180) pop = 38.6233 The first two arguments specify a discrete map from the values in T to the values in Y. The third argument is the time value where we want to interpolate. The result is what we expected, about halfway between the values that bracket it. interp1 can also take a fourth argument that specifies what kind of interpolation you want. The default is ’linear’, which does linear interpolation. Other choices include ’spline’ which uses a spline curve to fit two points on either side, and ’pchip’, which uses piecewise cubic Hermite interpolation. (Note that ’pchip’ was formerly ’cubic’). In addition to the MATLAB documentation, there is an interesting MATLAB blog page titled: “Splines and Pchips” that you may find interesting. >> pop = interp1(T, Y, 180, 'spline') pop = 38.6486 >> pop = interp1(T, Y, 180, 'pchip') pop = 38.6491 In this case we expect the spline and cubic interpolations to be better than linear, because they use more of the data, and we know the function isn’t linear. But we have no reason to expect the spline to be more accurate than the cubic, or the other way around. Fortunately, they are not very different. We can also use interp1 to project the rat population out beyond the values in T: >> [T(end), Y(end)] ans = 365.0000 76.9530 >> pop = interp1(T, Y, 370, 'pchip') pop = 80.9971 This process is called extrapolation. For time values near 365, extrapolation may be reasonable, but as we go farther into the “future,” we expect them to be less accurate. For example, here is the estimate we get by extrapolating for a whole year: >> pop = interp1(T, Y, 365*2, 'pchip') pop = -4.8879e+03 And that’s wrong. So very wrong. t Note that if you use ’cubic’ you will get a warning that the name will be discontinued in future releases of MATLAB. MATLAB instead wants you to use the keyword ’pchip’, which they say does the same thing. t Note that MATLAB has built-in functions spline and pchip for interpolating with these specific methods. I will exclusively use interp1 as it is more general an gives us greater flexibility. 446 Chapter 12 Interpolation Here we have passed the optional argument pchip for our interpolation method, which we in turn used to extrapolate. What if we didn’t list a method so as to use the default, and then try to extrapolate? Let’s give it a try: >> pop = interp1(T, Y, 365*2) pop = NaN >> pop = interp1(T, Y, 400) pop = NaN What gives? If four input parameters are provided, as we did by specifying the method, and the method is either pchip (or cubic) or spline, MATLAB will allow you to perform extrapolation. For all other cases (i.e., linear), you will need to pass an additional optional argument, ’extrap’, to perform an extrapolation. In your input list, ’extrap’ would follow the method used. If you wish to extrapolate using linear interpolation, you will need to specify the method as ’linear’ followed by ’extrap’. If the method is pchip (or cubic) or spline, then you do not need to list it if you do not want to, it is the default setting. Let’s try again to extrapolate with linear interpolation: >> pop = interp1(T, Y, 365*2,'linear','extrap') pop = 342.6316 >> pop = interp1(T, Y, 400,'linear','extrap') pop = 102.4291 Example 12.1 The Dortmund Data Bank (DDBST) has made the following set of isothermal vapor/liquid equilibrium data freely availabe for the binary system acetone(1)/benzene(2) at 318.15 K. 1 (a) Using the provided data, estimate the liquid (x 1 ) and vapor (y 1 ) mole fracs at 55 kPa using interp1. (b) Use interp1 to estimate values of the pressure (p) and y 1 over the range 0 ≤ x 1 ≤ 1, and plot the results. 1 http://www.ddbst.com/en/EED/VLE/VLE%20Acetone%3BBenzene.php 12.2 Interpolation 447 p (kPa) 33.428 36.666 43.230 46.450 50.647 53.293 57.722 60.527 63.380 66.037 67.189 x1 0.04700 0.09630 0.22070 0.29360 0.40110 0.47590 0.61250 0.70450 0.80810 0.90840 0.95290 y1 0.14440 0.25740 0.44170 0.52040 0.61390 0.66970 0.76140 0.82010 0.88050 0.94180 0.96990 Solution: (Link to screen cast and accompanying M-file.) Let’s do it! I will start by created vectors for the reference data. >> Pref = [33.428,36.666,43.230,46.450,50.647,53.293,57.722,60.527,... 63.380,66.037,67.189]; >> Xref = [0.04700,0.09630,0.22070,0.29360,0.40110,0.47590,0.61250,... 0.70450,0.80810,0.90840,0.95290]; >> Yref = [0.14440,0.25740,0.44170,0.52040,0.61390,0.66970,0.76140,... 0.82010,0.88050,0.94180,0.96990]; Note that while here I enter the data and perform all of the calculations in the Command Window for illustrative purposes, a script would likely be a better idea. Next, let’s estimate x 1 and y 1 at p = 55 kPa. >> x_linear = interp1(Pref,Xref,55) x_linear = 0.5285 >> y_linear = interp1(Pref,Yref,55) y_linear = 0.7050 >> x_pchip = interp1(Pref,Xref,55,'pchip') x_pchip = 0.5272 >> y_pchip = interp1(Pref,Yref,55,'pchip') y_pchip = 0.7051 We find the predictions using linear interpolation and pchip are in excellent agreement. Next, let’s make a plot. >> >> >> >> >> Xeval = linspace(0,1); Ylinear = interp1(Xref,Yref,Xeval,'linear','extrap'); Plinear = interp1(Xref,Pref,Xeval,'linear','extrap'); Ypchip = interp1(Xref,Yref,Xeval,'pchip'); Ppchip = interp1(Xref,Pref,Xeval,'pchip'); 448 Chapter 12 Interpolation >> >> >> >> >> >> >> >> >> >> hold on plot(Pref,Xref,'ok') plot(Pref,Yref,'ok') plot(Plinear,Xeval,'-r') plot(Plinear,Ylinear,'-r') plot(Ppchip,Xeval,'-g') plot(Ppchip,Ypchip,'-g') xlabel('x_1 or y_1') ylabel('p [kPa]') title('acetone(1)/benzene at 318.15 K') acetone(1)/benzene at 318.15 K 1 0.9 0.8 0.7 p [kPa] 0.6 0.5 0.4 0.3 0.2 0.1 0 30 35 40 45 50 x 1 or y 55 60 65 70 1 Figure 12.1 pxy phase diagram for acetone(1)/benzene(2) at 318.15 K. We find that both sets of predictions are again in excellent agreement, and appear to well represent the data. Note that the fit is not perfect as indicated by the fact that the lines (bubble and dew) do not meet up in the limit that x 1 → 0. 12.3 Interpolating the inverse function t Another alternative to using fzero, although you do need to be careful about the accuracy... if it’s important. And there are issues with multi-valued functions, as we will see later. We could alternatively use interp1 to create a function that will allow us to efficiently use fzero. We have used interp1 to find population as a function of time; by reversing the roles of T and Y, we can also interpolate time as a function of population. For example, we might want to know how long it takes the population to reach 20 rats. >> interp1(Y, T, 20) ans = 133.4128 12.3 Interpolating the inverse function 449 This use of interp1 might be confusing if you think of the arguments as x and y. You might find it helpful to think of them as the range and domain of a map (where the third argument is an element of the range). The following plot shows f (Y plotted as a function of T) and the inverse of f (T plotted as a function of Y). >> >> >> >> >> >> >> >> >> figure subplot(2,1,1) % First plot in 2 by 1 matrix plot(T, Y,'-r') ylabel('Y') xlabel('T') subplot(2,1,2) % Second plot in 2 by 1 matrix plot(Y, T, '-b') ylabel('T') xlabel('Y') 80 Y 60 40 20 0 0 50 100 150 200 250 300 350 400 50 60 70 80 T 400 T 300 200 100 0 0 10 20 30 40 Y Figure 12.2 Y versus T and T versus Y. In this case we can use interp1 either way because f is a single-valued mapping, which means that for each value in the domain, there is only one value in the range that maps to it. If we reduce the food supply so that the rat population decreases during the winter, our rate function might look something like this: 450 Chapter 12 Interpolation Listing 12.1 rats_winter.m 2 3 4 5 function res = rats_winter(t, y) a = 0.01; omega = 2 * pi / 365; res = a * y * (0.5 + sin(omega * t)); end Solving and plotting: >> >> >> >> [T, Y] = ode45(@rats_winter, [0, 365], 2); plot(T,Y) xlabel('T') ylabel('Y') 18 16 14 12 Y 1 10 8 6 4 2 0 50 100 150 200 250 300 350 400 T Figure 12.3 ode45(@rats_winter, [0, 365], 2) We can still use interp1 to map from T to Y: >> interp1(T, Y, 260) ans = 15.0309 So on Day 260, the population is about 15, but if we ask on what day the population was 15, there are two possible answers, 172.44 and 260.44. If we try to use interp1, we get the wrong answer: >> interp1(Y, T, 15) ans = 196.3833 12.4 Creating a function 451 On Day 196, the population is actually 16.8, so interp1 isn’t even close! The problem is that T as a function of Y is a multivalued mapping; for some values in the range there are more than one values in the domain. This causes interp1 to fail. The issue is interp1 expects a single valued functional relationship, and unfortunately this is not clearly communicated in the documentation. However, if our function is multivalued but we have an idea of where the solution occurs, we can specify bounds (or a range) for the search. For example: >> Trange1 = T<225; >> Trange2 = T>225; >> interp1(Y(Trange1), T(Trange1), 15) ans = 172.4407 >> interp1(Y(Trange2), T(Trange2), 15) ans = 260.4365 Nice! We will see in the next section how you can use fzero to obtain the same result. 12.4 Creating a function In your code you may find it useful or desirable to have a function that interpolates within T and Y or Y and T. This could be desirable for a number of reasons, such as: you will be interpolating often, wish to use T and Y again, will re-solve the ODE with different conditions within your code, wish to use other built-in functions, etc. We can do this quickly using an anonymous functions. Continuing on from the previous section: >> yt = @(t) interp1(T,Y,t); >> yt(260) ans = 15.0309 If we have a multivalued function, we can use fzero to overcome the limitations of interp1. Let’s revisit the case of finding when Y is equal to 15. For this case interp1 failed when using the entire data set. I will start by modifying our function so that it is 0 when Y is equal to 15, and then I will use fplot to get an estimate of the time. >> yt = @(t) interp1(T,Y,t)-15; >> fplot(yt,[0,365]) >> fzero(yt,170) ans = 172.4407 t From the documentation for interp1, linear requires atleast 2 points, and pchip and spline require alteast 4 points. 452 Chapter 12 Interpolation >> fzero(yt,260) ans = 260.4365 2 0 -2 -4 -6 -8 -10 -12 0 50 100 150 200 250 300 350 Figure 12.4 fplot(yt,[0,365]) Nice! We obtain the same value as when we set bounds in our call to interp1. Both work, which do you find easiest to use? We will see another use of fzero in the next section. One issue with creating a function in your code is over time you might forget the range covered or another user of your program might not know the range. If you are using the method ’pchip’ or ’spline’, then MATLAB will by default allow you to extrapolate. For this case you might want to add a check that if the user is trying to extrapolate, return a default value. A common choice is 0 and NaN (not a number), which we found is what MATLAB uses when we tried to extrapolate without specifying a method (i.e, using the default). Following the specification of the method used, list the value you wish to return if extrapolating. Let’s take a look at an example. First we will create a function yt and use it to extrapolate using ’pchip’. Then we will update the function to return a default value if we extrapolate, and try again. >> yt = @(t) interp1(T,Y,t,'pchip'); >> yt(260) ans = 15.0339 >> yt(400) 12.5 Field mice 453 ans = 14.3354 >> yt = @(t) interp1(T,Y,t,'pchip',NaN); >> yt(260) ans = 15.0339 >> yt(400) ans = NaN 12.5 Field mice As we’ve seen, one use of interpolation is to interpret the results of a numerical computation; another is to fill in the gaps between discrete measurements. For example2 , suppose that the population of field mice is governed by this rate equation: g (t , y) = a y − b(t )y 1.7 where t is time in months, y is population, a is a parameter that characterizes population growth in the absence of limitations, and b is a function of time that characterizes the effect of the food supply on the death rate. Although b appears in the equation as a continuous function, we might not know b(t ) for all t . Instead, we might only have discrete measurements: t 0 1 2 3 4 5 6 7 8 b (t ) 0.0070 0.0036 0.0011 0.0001 0.0004 0.0013 0.0028 0.0043 0.0056 If we use ode45 to solve the differential equation, then we don’t get to choose the values of t where the rate function (and therefore b) gets evaluated. We need to provide a function that can evaluate b everywhere: 2 This example is adapted from Gerald and Wheatley, Applied Numerical Analysis, Fourth Edition, Addison-Wesley, 1989. 454 Chapter 12 Interpolation Listing 12.2 interpolate_b.m 1 2 3 4 5 6 7 8 9 function res = interpolate_b(t) T = 0:8; B = [70 36 11 1 4 13 28 43 56] * 1e-4; % While linear is default, and by default MATLAB will not % extrapolate with linear interpolation, I explicitly specify it % here. This way if I change interp1 to use spline or cubic % interpolation, I need just update the method. res = interp1(T, B, t,'linear',NaN); end Abstractly, this function uses a discrete map to implement a continuous map. Example 12.2 Write a rate function that uses interpolate_b to evaluate g and then use ode45 to compute the population of field mice from t = 0 to t = 8 with an initial population of 100 and a = 0.9. Then modify interpolate_b to use spline interpolation and run ode45 again to see how much effect the interpolation has on the results. Solution: (Link to screen cast and accompanying M-file.) Let’s do it! I solved both problems simultaneously. I first created another function file to perform spline interpolation of b: Listing 12.3 interpolate_b_spline.m 1 2 3 4 5 6 7 8 9 function res = interpolate_b_spline(t) T = 0:8; B = [70 36 11 1 4 13 28 43 56] * 1e-4; % While linear is default, and by default MATLAB will not % extrapolate with linear interpolation, I explicitly specify it % here. This way if I change interp1 to use spline or cubic % interpolation, I need just update the method. res = interp1(T, B, t,'spline',NaN); end Our rate function will contain the population growth parameter a. While we could explicitly define it in the file, it would be nice if I could pass it as a parameter. Passing it is not possible since ode45 will expect just two input parameters. I will therefore write a top function that I can pass a and solve the ODE, then I will nest the rate function within so that it can see a. Likewise, I will pass to the top function a flag variable. When flag == 1 we will use linear interpolation for b, otherwise we will use spline interpolation. By nesting the rate function, it can see the value of the flag variable. It will then pass this to a modified version of interpolation_b 12.5 Field mice 455 which will perform linear or spline interpolation depending on the value of flag passed to it. In addition to solving and returning the result to the Command Window , I will also plot the results within the function to help visualize whether using spline interpolation makes a difference. Below are my functions followed with their use from the Command Window and the results. Listing 12.4 field_mice.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 % function res = field_mice(TSPAN,y0,a,flag) % % TSPAN is a two element row vector with elements t0 and tfinal % y0 is the initial population % a is the population parameter % flag will indicate to use linear or spline interpolation % if flag == 1, linear % else, spline % % The function will return a matrix where column 1 is % time and column 2 is the population. % % I will also generate a function of y vs t within the function. % If we are using linear interpolation, the line will be red, % and will be blue for spline. % function res = field_mice(TSPAN,y0,a,flag) [T,Y] = ode45(@field_mice_rate,TSPAN,y0); % Pack T and Y into a matrix and return as result res = [T,Y]; % Plotting if flag == 1 plot(T,Y,'-r') else plot(T,Y,'-b') end xlabel('time [months]') ylabel('field mice population') function res = field_mice_rate(t,y) % Interpolate to find b if flag == 1 b = interpolate_b(t); else b = interpolate_b_spline(t); end end end res = a*y-b*y^(1.4); 456 Chapter 12 Interpolation Listing 12.5 interpolate_b_field_mice.m function res = interpolate_b_field_mice(t,flag) T = 0:8; B = [70 36 11 1 4 13 28 43 56] * 1e-4; % If flag == 1, use linear interpolation, else use spline if flag == 1 res = interp1(T, B, t,'linear',NaN); else res = interp1(T, B, t,'spline',NaN); end end >> >> >> >> >> >> >> TSPAN = [0,8]; y0 = 100; a = 0.9; linear = field_mice(TSPAN,y0,a,1); hold on spline = field_mice(TSPAN,y0,a,2); legend('linear interpolation','spline interpolation') 7 ×10 4 linear interpolation spline interpolation 6 5 field mice population 1 2 3 4 5 6 7 8 9 10 4 3 2 1 0 0 1 2 3 4 5 6 7 8 time [months] Figure 12.5 Comparison of linear and spline interpolation of b. Looking at the plot, there does not seem to be much difference on the result when using linear and spline interpolation of b. From the figures, the growth appears to be exponential, so let us plot the log of the population versus time. This should linearize the functions. We will then see if they still appear to be the same. 12.6 Good results come from good users >> >> >> >> >> >> 457 plot(linear(:,1),log(linear(:,2)),'-r') hold on plot(spline(:,1),log(spline(:,2)),'-b') xlabel('time [months]') ylabel('ln(field mice population)') legend('linear interpolation','spline interpolation') 12 linear interpolation spline interpolation 11 ln(field mice population) 10 9 8 7 6 5 4 0 1 2 3 4 5 6 7 8 time [months] Figure 12.6 Comparison of linear and spline interpolation of b, where here we plot the log of the population. The functions still appear to be the same, so the use of spline made little difference as compared to using linear interpolation. 12.6 Good results come from good users Computers are great! They do exactly what they are told. But that is also the biggest source of problems. If there is a typo in your code, MATLAB does not notice this, it just keeps going. Finding such errors was part of our motivation for incremental development. On the same note, when interpolating, MATLAB does exactly what it is told. It does not know if linear, pchip, or spline is the most appropriate for a model. It just uses what you tell it to. You are irreplaceable. While in many cases it does not make much of a difference whether you use linear or spline, sometimes the effect on the accuracy can be very noticeable and very important. Let’s consider two examples. First, let’s revisit the interpolation of b. We found that the difference between linear and spline interpolation on the result of the 458 Chapter 12 Interpolation solution to our ODE was small and not noticeable. Let’s take a closer look. Let’s plot the reference data. Then, for a time range of 0 to 8 months in increments of 0.1 months, let’s perform linear interpolation and spline interpolation. Then plot the results and compare to our reference data. >> >> >> >> >> >> >> >> >> >> >> >> T = 0:8; B = [70 36 11 1 4 13 28 43 56] * 1e-4; plot(T,B,'ro') hold on Teval = 0:0.1:8; linear = interp1(T, B, Teval,'linear',NaN); spline = interp1(T, B, Teval, 'spline',NaN); plot(Teval,linear,'-b') plot(Teval,spline,'-g') xlabel('time [months]') ylabel('b') legend('ref data','linear','spline') 7 ×10 -3 ref data linear spline 6 5 b 4 3 2 1 0 0 1 2 3 4 5 6 7 8 time [months] Figure 12.7 Comparison of linear and spline interpolation of b. Red circles are reference data, blue line is linear interpolation, and green line is spline interpolation. We see that for the most part the results of linear and spline interpolation are in good agreement with each other. The only noticeable difference is around the minimum. Here the value of b is smallest and close to 0, and is why we probably did not notice much of a difference. Let’s consider another example for which we know the true, analytic answer, which will allow us to directly compare the methods. Imagine you collected the 12.6 Good results come from good users following data: t 0 2 4 6 c (t ) 1.0000 7.3891 54.5982 403.4288 This actually corresponds to c = exp(t ), which we will use to compare our results. Let’s start by constructing a plot just as we did for b, where here we will also plot the c = exp(t ) data as a reference. >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> >> T = 0:2:6; C = [1.0000, 7.3891, 54.5982, 403.4288]; hold on plot(T,C,'ro') Teval = 0:0.1:6; Cref = exp(Teval); Linear = interp1(T, C, Teval,'linear',NaN); Spline = interp1(T, C, Teval, 'spline',NaN); Pchip = interp1(T, C, Teval,'pchip',NaN); plot(Teval,Cref,'--r') plot(Teval,Linear,'-b') plot(Teval,Spline,'-g') plot(Teval,Pchip,'-k') xlabel('t') ylabel('c') legend('ref data','exp(t)','linear','spline','pchip') 459 460 Chapter 12 Interpolation 450 ref data exp(t) linear spline pchip 400 350 300 c 250 200 150 100 50 0 0 1 2 3 4 5 6 t Figure 12.8 Comparison of linear, spline and pchip interpolation of c, which is actually c = exp(t ). Red circles are reference data, red dashed line is exp(t ) drawn for reference, blue line is linear interpolation, green line is spline interpolation, and the black line is pchip interpolation. We find that as time increases, the deviation between linear interpolation and the reference curve increases. The agreement between the reference curve and spline is better for t > 2. However, for t ≤ 2 , spline actually predicts a function that is concave down. Here pchip does better, but like spline, it over predicts over the range 4 > t > 6. We can compare numerical values at t = 5: >> exp(5) ans = 148.4132 >> interp1(T,C,5,'linear') ans = 229.0135 >> interp1(T,C,5,'spline') ans = 175.0107 >> interp1(T,C,5,'pchip') ans = 176.9537 Even with spline this deviation corresponds to approximately 18%. This might be okay for some applications, but not for others. What are we to do? Have you ever heard of logarithmic graph paper before? Natural log (ln) is log with base e. It received its name because ln and e appear in many expressions when modeling physical systems. (Key point, they appear 12.6 Good results come from good users 461 a lot.) In the pre-MATLAB days there was, and still is today, a desire to plot data such that it appears to be linear. Taking the log of an exponential function, or a variable raised to a power, the result is a linear function. This is often called linearization. Let us take the log of c, plot the result, and then see the result of performing linear and spline interpolation on this new data set. I will then un-linearize the interpolation results and plot them against the original reference data. LOGC = log(C); plot(T,LOGC,'ro') hold on RefLOG = Teval; LinearLOG = interp1(T, LOGC, Teval,'linear',NaN); SplineLOG = interp1(T, LOGC, Teval, 'spline',NaN); PchipLOG = interp1(T, LOGC, Teval, 'pchip',NaN); plot(Teval,RefLOG,'--r') plot(Teval,LinearLOG,'-b') plot(Teval,SplineLOG,'-g') plot(Teval,PchipLOG,'-k') xlabel('t') ylabel('ln c') legend('ref data','x','linear','spline','pchip') 7 ref data x linear spline pchip 6 5 4 ln c >> >> >> >> >> >> >> >> >> >> >> >> >> >> 3 2 1 0 0 1 2 3 4 5 6 t Figure 12.9 Comparison of linear, spline and pchip interpolation of ln(c), where c is actually exp(t ), so ln(c) = t . Red circles are reference data, red dashed line is t drawn for reference, blue line is linear interpolation, green line is spline interpolation, and black line is pchip interpolation. t Remember in MATLAB log is ln, and log10 is log. 462 Chapter 12 Interpolation Beautiful! All three methods result in a perfect match. Let’s write an anonymous function to perform linear interpolation of ln c versus t , and then to return the value of c, which is what we actually want. We can then calculate the value at t = 5 to compare to our previous result. In writing the function, I will assume we just have our original data tabulated as T and C. >> ct = @(t) exp( interp1(T,log(C),t,'linear',NaN) ); >> ct(5) ans = 148.4132 In perfect agreement with the reference curve. This is a huge improvement over performing interpolation of c versus t , and it required minimal extra work. And that is what I mean by “good results come from good users.” Example 12.3 Interpolating vapor pressure data. Below I have tabulated vapor pressure data for benzene which was taken from the NIST Chemistry WebBook, although you can pretend you collected it in the laboratory. T [K] 300 320 340 360 380 400 420 440 460 480 500 P sat [bar] 0.1381 0.32029 0.66128 1.2425 2.1614 3.5284 5.4649 8.1007 11.575 16.037 21.651 where the temperature is in K and the pressure is in bar. Plot the data, and compare the use of linear and spline interpolation. (Follow the work in Section 12.6.) How do the two methods perform? Use them both to estimate the vapor pressure at 390 K. From your thermodynamics class, you know that the Clausius-Clapeyron equation suggests that ln p sat scales linearly with T −1 . Transform the data as suggested by the Clausius-Clapeyron equation and re-plot it, and again compare the use of linear and spline interpolation. Use them both to estimate the vapor pressure at 390 K. How do the results compare? 12.6 Good results come from good users 463 Solution: Let’s begin by entering the data in to two vectors, T_K and Psat_bar. The temperature data is evenly spaced, so I will use the extended colon operator. t Psat_bar is long, so I will >> T_K = 300:20:500; >> Psat_bar = [0.1381,0.32029,0.66128,1.2425,2.1614,3.5284,5.4649,... 8.1007,11.575,16.037,21.651]; Next, to graphically compare the use of linear and spline interpolation, let’s use linear and spline interpolation to estimate the vapor pressure over the range 300 to 500 K in increments of 1 K. To do this, we will create a new vector T_eval which contains the temperatures we wish to estimate the vapor pressure at. We can then use interp1 with either linear or spline interpolation, and store the results to two new vectors. We will then plot the original data and our interpolation results, and compare the two. Remember, with linear and spline interpolation, we are only estimating the data in between points. >> >> >> >> >> >> >> >> >> >> T_eval = 300:1:500; Psat_linear = interp1(T_K,Psat_bar,T_eval,'linear',NaN); Psat_spline = interp1(T_K,Psat_bar,T_eval,'spline',NaN); plot(T_K,Psat_bar,'kx') hold on plot(T_eval,Psat_linear,'-r') plot(T_eval,Psat_spline,'--b') xlabel('T [K]') ylabel('Psat [bar]') legend('reference','linear','spline') break it up onto two lines. To continue onto another line, we need to add ... to the end of the line. Then to get to the next line in the Command Window, use Shift + Enter. Know that if I were to omit the ..., MATLAB would interpret the second line as the second column in matrix. 464 Chapter 12 Interpolation 25 reference linear spline Psat [bar] 20 15 10 5 0 300 350 400 450 500 T [K] Figure 12.10 Comparing our original reference data to the result using linear and spline interpolation. As far as we can tell from the figure, linear and spline interpolation appear to be in close agreement, and appear to represent our data well. Next, let’s use both methods to estimate the vapor pressure at 390 K. >> Psat_390_linear = interp1(T_K,Psat_bar,390,'linear',NaN) Psat_390_linear = 2.8449 >> Psat_390_spline = interp1(T_K,Psat_bar,390,'spline',NaN) Psat_390_spline = 2.7815 Quantitatively we do find a small difference in the two methods. Next we are told to repeat, but plot our data as a Clapeyron plot and to interpolate in this fashion. Then we will again make predictions at 390 K. For this case we will interpolate using our linearized data, ln p sat versus 1/T , and then un-linearize to get p sat . >> >> >> >> >> >> close % Close our open figure and start fresh Tinv = 1./T_K; Ln_Psat = log(Psat_bar); Teval_inv = 1./T_eval; Ln_Psat_linear = interp1(Tinv,Ln_Psat,Teval_inv,'linear',NaN); Ln_Psat_spline = interp1(Tinv,Ln_Psat,Teval_inv,'spline',NaN); 12.6 Good results come from good users >> >> >> >> >> >> >> 465 plot(Tinv,Ln_Psat,'kx') hold on plot(Teval_inv,Ln_Psat_linear,'-r') plot(Teval_inv,Ln_Psat_spline,'--b') xlabel('1/T [1/K]') ylabel('ln Psat/bar') legend('reference','linear','spline') 4 reference linear spline 3 ln Psat/bar 2 1 0 -1 -2 2 2.2 2.4 2.6 2.8 3 3.2 1/T [1/K] 3.4 ×10 -3 Figure 12.11 Comparing our original reference data to the result using linear and spline interpolation. Here we use ln p sat versus 1/T to linearize our data. Again, as far as we can tell from the figure, linear and spline interpolation appear to be in close agreement, and appear to represent our data well. As compared to before, our transformed data now appears to be linear. While linear and spline were in close agreement before, I suspect it is just because of the spacing used; over the small range between data points, linear interpolation was a fair approximation. Here, we expect linear to work well because the data appears to be linear. Let’s see by using both linear and spline interpolation to estimate the vapor pressure at 390 K. We will interpolate using our linearized data, then un-linearize to get the desired result. >> ln_Psat_390_linear = interp1(Tinv,Ln_Psat,1/390,'linear',NaN) ln_Psat_390_linear = 1.0221 >> Psat_390_linear_clap = exp(ln_Psat_390_linear) 466 Chapter 12 Interpolation Psat_390_linear_clap = 2.7790 >> ln_Psat_390_spline = interp1(Tinv,Ln_Psat,1/390,'spline',NaN) ln_Psat_390_spline = 1.0230 >> Psat_390_spline_clap = exp(ln_Psat_390_spline) Psat_390_spline_clap = 2.7815 Using spline interpolation, we find that we obtain the same result whether we interpolate using the original data or the linearized data. Using linear interpolation, using our linearized date the result in now in closer agreement with the result using spline interpolation. 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) Most thermodynamics 1 courses rely heavily on reading tabulated data of pure fluids. Most typically for Water (the steam tables) and Refrigerant 134a. However, the exact conditions you often need are in between points, requiring interpolation. What might be better is the use of an analytic equation of state, possibly fit to the tabulated data, obviating the need for interpolation. But this is not without challenge, especially for two-phase equilibria and non-pV T (pressure, volume, temperature) data. In the remainder of the chapter we will demonstrate the use of MATLAB as the ultimate equation of state. We will read in the saturated steam tables and use interp1 to effortlessly interpolate and extrapolate. Essentially, MATLAB will provide us with an analytic form of the steam tables. 12.7.1 Downloading the saturated steam tables t If you are having difficulties downloading a data set from NIST, a copy of my file for water can be downloaded here. We will download the saturated steam tables from the NIST WebBook Thermophysical Properties of Fluid Systems.3 While here we will look at water, other fluids are available, and the database keeps growing. For the species, from the drop-down menu select “Water,” and then choose your preferred units. Comparing to typically steam tables in the appendix of a thermodynamics text for temperature, pressure, density, and energy, I will choose K, MPa, g/mL and kJ/kg respectively. And since we will look at the saturated steam tables, I choose “Saturation properties – temperature increments.” You could 3 http://webbook.nist.gov/chemistry/fluid/ 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) 467 alternatively look at “pressure increments,” but temperature increments seem more common. The other options would be used for supercritical, superheated vapor, and compressed liquid phases. Figure 12.12 NIST WebBook Thermophysical Properties of Fluid Systems units settings On the next window we select the temperature range and the temperature increments. The minimum value listed corresponds to the triple point, and the maximum value corresponds to the critical point. The two-phase vapor-liquid region is between these two points. I will choose 280 to 640 K in increments of 10 K. Figure 12.13 NIST WebBook Thermophysical Properties of Fluid Systems temperature settings While lots of information is available on the next page, we would like to download the tabulated data. This can be accomplished by right-clicking on “Download data as tab-deliminated text file.” On my computer the default name comes up as “fluid.cgi.” I will save to my current working directory as “water_sat_nist.dat.” The other useful piece of information to note before closing our web browser is the “Auxiliary data.” Of particular note is the “Reference States.” Remember, there is no such thing as absolute energy. We only know energy within a constant. And t I mention again that if you are having difficulties downloading a data set from NIST, a copy of my file for water can be downloaded here. 468 Chapter 12 Interpolation that is okay. In thermodynamics, we are only interested in differences. We use reference states to simplify data tabulation. Figure 12.14 The reference states used by the NIST WebBook Thermophysical Properties of Fluid for water, along with the critical temperature (Tc ), critical pressure (p c ), and critical density (ρ c ), accentric factor (ω), normal boiling point (Tb ), and the dipole moment. 12.7.2 dlmread t As a starting point to learn more about importing data into MATLAB, have a look at the documentation page “Ways to Import Text Files”. Now that we have tabulated data, we need to read it into MATLAB. There are several ways to read-in data in MATLAB. Here we will use dlmread which is well suited for this case. The “dlm” stands for a “deliminated” text file. A deliminated text file is a file that contains columns of data separated by a fixed character (typically a comma) or space (or tab). We will first go over the syntax of dlmread, one required argument and three optional arguments, and then we will look at the specific example of the saturated steam tables. If you would like more, have a look at the MATLAB documentation. Know that a limitation of dlmread is that it can only read in numeric data. There are ways to read mixed data types, but they are more involved and will be reserved for later. The base use would be M = dlmread(filename). filename is the name of the file you wish to read-in and is a string. For our case this will be ‘water_sat_nist.dat’. Note that the quotes are important to indicate that it is a string. dlmread reads in the contents of the file and stores it in matrix M. Remember a deliminated text file is a file that contains columns of data. Each column of the text file will be stored to a column in M, in the order that they appear. The first optional argument is delimiter, resulting in M = dlmread(filename,delimiter). delimiter allows you to specify what is separating the columns in the text file. In the previous case, when we did not specify delimiter, MATLAB detects the 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) 469 delimiter from the file and treats repeated white spaces as a single delimiter. delimiter is also a string. To specify the delimiter as a space, comma, and tab, use ’ ’, ’,’, and ’\t’, respectively. Again, the quotes are important to indicate that it is a string. When we downloaded the saturated steam tables, the website indicated that it was a tab-deliminated text file, so we will use ’\t’. Note that if you would like to stick with the default of MATLAB determining the delimiter, you can use quotes with no space in between, “; this is also referred to as an empty character. The second optional argument is the the row, R1, and column, C1, offset, resulting in M = dlmread(filename,delimiter,R1,C1). If you would like to read-in all of the data, then R1=0 and C1=0. Recall that dlmread can only be used to read in numeric data. In our case, the first row will be a string indicating the property in each column. We will therefore need to skip (not read-in) the first row. This is accomplished with R1=1 and C1=0. This means use a row offset of 1, i.e. skip the first row, but don’t skip any columns. The fourth optional argument is to specify the row and column range as [R1 C1 R2 C2], resulting in M = dlmread(filename,delimiter,[R1 C1 R2 C2]). Here R1, R2, C1, and C2 are still row and column offsets. So if we wanted to read in rows 1 to 6 of columns 1 and 2, we would have R1=0, R2=5, and C1=0, C2=1. So if you would like to think in terms of row and column numbers rather than row and column offsets, you need just recognize they differ by 1. So finally, let’s read in the steam tables: t If your text file was in a >> M=dlmread('water_sat_nist.dat','\t',1,0); I suppressed the output because the saturated steam tables contain a large amount of data. If we would like to check the size of M, we can: >> whos M Name M Size 37x25 Bytes Class 7400 double Attributes So M is a matrix with 37 rows and 25 columns. What does each row correspond to? You could find out by viewing “water_sat_nist.dat” in your favorite text editor. To view the file within the MATLAB Command Window, you can use the command type ’water_sat_nist.dat’. I do not do so here in the text because the file is very large. You might find that the columns do not appear to be exactly lined up. That is okay. In each row the data are separated by a tab, indicating the next column. Where the columns do not appear to line up is due to differences in the number of digits in a number or the use of scientific notation. You will find that the columns correspond to the following: different directory, you could still load it, you would just need to augment its name to provide its location. If the file were in my home directory, located at “/home/paluchas”, we could replace the file name with “/home/paluchas/water_sat_nist.dat”. You can always use global commands like this. Local commands are also possible if you know where the file is in relation to the current directory. 470 Chapter 12 Interpolation 1. Temperature (K) 2. Pressure (bar) 3. Density (l, mol/l). Here the first “l” is used to indicate the phase as liquid. The second “l” corresponds to liters, which should actually be capitalized, “L”. 4. Volume (l, l/mol) 5. Internal Energy (l, kJ/mol) 6. Enthalpy (l, kJ/mol) 7. Entropy (l, J/mol*K) 8. Cv (l, J/mol*K) 9. Cp (l, J/mol*K) 10. Sound Spd. (l, m/s) 11. Joule-Thomson (l, K/bar) 12. Viscosity (l, uPa*s) 13. Therm. Cond. (l, W/m*K) 14. Surf. Tension (l, N/m) 15. Density (v, mol/l). Now here “v” stands for vapor phase. 16. Volume (v, l/mol) 17. Internal Energy (v, kJ/kg) 18. Enthalpy (v, kJ/kg) 19. Entropy (v, J/g*K) 20. Cv (v, J/g*K) 21. Cp (v, J/g*K) 22. Sound Spd. (v, m/s) 23. Joule-Thomson (v, K/MPa) 24. Viscosity (v, Pa*s) 25. Therm. Cond. (v, W/m*K) 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) That is a lot of data! If I look at typical saturated steam tables in the appendix of a thermodynamics text, they typically contain just temperature, pressure, density, internal energy, enthalpy, and entropy. Let us therefore create a new matrix, water_sat that contains just this data as columns: 1. Temperature (K) 2. Pressure (bar) 3. Density (l, mol/l) 4. Internal Energy (l, kJ/mol) 5. Enthalpy (l, kJ/mol) 6. Entropy (l, J/mol*K) 7. Density (v, mol/l) 8. Internal Energy (v, kJ/kg) 9. Enthalpy (v, kJ/kg) 10. Entropy (v, J/g*K) To accomplish this, we see we will need all rows and columns 1–3, 5–7, 15, and 17–19. This is accomplished as: >> water_sat = [M(:,1:3),M(:,5:7),M(:,15),M(:,17:19)]; You may find that you would like to look at many of the systems provided in the NIST WebBook Thermophysical Properties of Fluid Systems database, and this might seem like a lot to do each time (and to remember!). We can therefore create a function to generalize and automate the process: 471 472 Chapter 12 Interpolation Listing 12.6 import_sat_vle.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 % function res = import_sat_vle(sat_vle_nist) % % Imporating tabulated saturated VLE data from NIST % % res will be the matrix of desired properties (type numeric). % By default this will by T, P, rho L, U L, H L, S L, % rho V, U V, H V, S V % but you can easily modify this. % % sat_vle_nist will be a string corresponding to the file name. % be sure to include the path if it is not in the current % directory. % function res = import_sat_vle(sat_vle_nist) % Read in our saturated VLE data from NIST, skipping % the first row which corresponds to the column labels M=dlmread(sat_vle_nist,'\t',1,0); % Columns are labeled as: % 1) Temperature (K) % 2) Pressure (MPa) % 3) Density (l, mol/l) % 4) Volume (l, l/mol) % 5) Internal Energy (l, kJ/mol) % 6) Enthalpy (l, kJ/mol) % 7) Entropy (l, J/mol*K) % 8) Cv (l, J/mol*K) % 9) Cp (l, J/mol*K) %10) Sound Spd. (l, m/s) %11) Joule-Thomson (l, K/bar) %12) Viscosity (l, uPa*s) %13) Therm. Cond. (l, W/m*K) %14) Surf. Tension (l, N/m) %15) Density (v, mol/l) %16) Volume (v, l/mol) %17) Internal Energy (v, kJ/kg) %18) Enthalpy (v, kJ/kg) %19) Entropy (v, J/g*K) %20) Cv (v, J/g*K) %21) Cp (v, J/g*K) %22) Sound Spd. (v, m/s) %23) Joule-Thomson (v, K/MPa) %24) Viscosity (v, Pa*s) %25) Therm. Cond. (v, W/m*K) end % Creating a matrix with specified columns. % By default this will by T, P, rho L, U L, H L, S L, % rho V, U V, H V, S V % but you can easily modify this. This will be returned. res = [M(:,1:3),M(:,5:7),M(:,15),M(:,17:19)]; %res = [M(:,1),M(:,14)]; 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) Let’s give it a try: >> water_sat = import_sat_vle('water_sat_nist.dat'); Nice! 12.7.3 interp1 Now let’s interpolate! Here we are dealing with a pure component system (N = 1) at vapor-liquid equilibrium (π = 2). According to Gibbs phase rule, we have F = N −π+2 = 1−2+2 = 1, a single degree of freedom. That is, we need only specify a single, intensive, thermodynamics function to pin down the thermodynamic state of our system. What does that mean? You can think of it as every thermodynamic property is a function of only a single variable. This will correspond to interpolating in 1-D using interp1. Remember, our data is tabulated from 280 to 640 K in increments of 10 K. What if we needed the properties of saturated water at 298.15 K? Are we out of luck? No, we can use interp1. Remember temperature is column 1 of water_sat. Using spline interpolation: >> sol = interp1(water_sat(:,1),water_sat,298.15,'spline') sol = 1.0e+03 * Columns 1 through 6 0.2981 0.0000 0.0010 0.1048 2.5465 0.0086 0.1048 0.0004 Columns 7 through 10 0.0000 2.4091 We see that we are able to simultaneously interpolate on every column of water_sat. The result, sol, is a row vector with each column corresponding to the function in the same column of water_sat. The one issue you will notice is the number of decimal places used when displaying the result. (MATLAB actually uses more decimal places in its calculations, it just displays fewer.) Right now we have a pressure of zero and density of the vapor of zero, which certainly can not be the case. The issue is MATLAB is multiplying everything by a factor of 1,000; it is trying to use the best overall scale, which might not be the best for each individual case. There are number of workarounds. The easiest is just to display the specific elements in question. For the case of pressure, the second element of sol: 473 474 Chapter 12 Interpolation >> p_MPa = sol(2) p_MPa = 0.0032 We could go further and use different units if MPa is not appropriate at this temperature. Instead of MPa, we could use kPa by multiplying by a factor of 1,000: >> p_kPa = sol(2)*1000 p_kPa = 3.1695 Or you could just interpolate in pressure if that is what you are interested in. What is important to remember is that while MATLAB may display zero, the pressure and vapor density are not actually zero. It is just a limit of the number of decimal places displayed. Or you could just tell MATLAB to display more decimal places: >> format long >> sol = interp1(water_sat(:,1),water_sat,298.15,'spline') sol = 1.0e+03 * Columns 1 through 3 0.298150000000000 0.000003169460821 0.000997001514381 0.104824285101334 0.000367225663839 2.409079489426976 2.546546392886929 Columns 4 through 6 0.104824990686921 Columns 7 through 9 0.000000023072679 Column 10 0.008556666952043 Hmmmm, scientific notation might be better: >> format short e >> sol = interp1(water_sat(:,1),water_sat,298.15,'spline') 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) sol = Columns 1 through 4 2.9815e+02 3.1695e-03 9.9700e-01 1.0482e+02 2.3073e-05 2.4091e+03 Columns 5 through 8 1.0482e+02 3.6723e-01 Columns 9 through 10 2.5465e+03 8.5567e+00 Besides temperature, you can interpolate in any variable you wish. For example, what are the properties of our two phase system at a pressure of 0.3 MPa? Pressure is the second column of water_sat: >> sol = interp1(water_sat(:,2),water_sat,0.3,'spline') sol = Columns 1 through 4 4.0668e+02 3.0000e-01 9.3182e-01 5.6112e+02 1.6509e-03 2.5431e+03 Columns 5 through 8 5.6143e+02 1.6718e+00 Columns 9 through 10 2.7249e+03 6.9915e+00 Or how about when the liquid density is 0.8 g/mL? The liquid density is the third column of water_sat: 475 476 Chapter 12 Interpolation >> sol = interp1(water_sat(:,3),water_sat,0.8,'spline') sol = Columns 1 through 4 5.2240e+02 3.9262e+00 8.0000e-01 1.0772e+03 1.9709e-02 2.6020e+03 Columns 5 through 8 1.0821e+03 2.7867e+00 Columns 9 through 10 2.8011e+03 6.0773e+00 And the list goes on and on. 12.7.4 Plotting phase diagrams It is great that MATLAB can interpolate the steam tables for us. But it can do more. A benefit of using MATLAB over conventional steam tables is that we can readily generate phase diagrams, which in turn can help us better understand our system and phase-equilibria thermodynamics in general. Many great contributions in the field have come from understanding and generalizing trends. Let’s start with the T ρ plane. That is, let’s plot T versus ρ L and ρV , the density of the liquid and vapor phase. This is a fun case because the density of the two phases are different, resulting in a “phase envelope”. If you are at a point under the dome, your system will split along the isotherm to form a vapor and liquid in coexistence with each other. >> >> >> >> >> hold on plot(water_sat(:,3),water_sat(:,1),'-ro') plot(water_sat(:,7),water_sat(:,1),'-bo') xlabel('Density [g/mL]') ylabel('Temperature [K]') 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) 650 600 Temperature [K] 550 500 450 400 350 300 250 0 0.2 0.4 0.6 0.8 1 Density [g/mL] Figure 12.15 A plot of the T ρ plane of water at vapor-liquid equilibrium. We know that the liquid density is larger than the vapor density, so is given by the red line to the right of the diagram. As temperature increases, the liquid density decreases while the vapor density increases. At the critical point, the two phases become one, so the densities approach the same value, the critical density, at the critical point. To the left of the dome is the vapor region, to the right is the compressed liquid region, and above would be the supercritical region. Our dome does not close up on top because our maximum temperature is 640 K while the critical temperature is 647.096 K. We can extrapolate to find this value, but it is a difficult task; as you might be able to tell from the diagram, the phase envelope is flattening out near the critical point. However, we just said that the density of the two phases should be equal at the critical point, or that the difference in the density of the two phases should go to 0. Let’s create a new vector which is the difference in the two densities and add it to our plot. I will subtract the vapor density from the liquid density so that the result is a positive number. Then I will extrapolate to find the temperature where this function is equal to zero, which should correspond to the critical point. I will then add the critical point to the plot. 477 478 Chapter 12 Interpolation >> rho_diff = water_sat(:,3) - water_sat(:,7); >> plot(rho_diff,water_sat(:,1),'-k') >> sol = interp1(rho_diff,water_sat,0,'spline','extrap') sol = Columns 1 through 4 6.5309e+02 2.0483e+01 3.1966e-01 2.0275e+03 3.1966e-01 2.0185e+03 Columns 5 through 8 2.0953e+03 4.4283e+00 Columns 9 through 10 2.0949e+03 4.3552e+00 >> plot(sol(3),sol(1),'kx') 700 650 Temperature [K] 600 550 500 450 400 350 300 250 0 0.2 0.4 0.6 0.8 1 Density [g/mL] Figure 12.16 A plot of the T ρ plane of water at vapor-liquid equilibrium with an estimate of the critical point. We estimate a critical temperature of 653.09 K, a critical pressure of 20.483 MPa, and a critical density of 0.31966 g/mL. As compared to the reference NIST data, we overestimate the temperature by about 6 K, underestimate the pressure by about 2 MPa, and underestimate the critical density by approximately 0.002 g/mL. That’s not too bad for a prediction. Let’s next plot the hT plane, where h is the molar enthalpy. That is, let’s plot 12.7 Fun with tabulated pure component VLE data (i.e., the saturated steam tables) h L versus T and hV versus T . We get a phase envelope very similar to the ρT case. However, here on the right we have vapor and on the left we have liquid. (The internal energy of a vapor will be greater than a liquid.) >> >> >> >> >> hold on plot(water_sat(:,5),water_sat(:,1),'-ro') plot(water_sat(:,9),water_sat(:,1),'-bo') xlabel('Enthalpy [kJ/kg]') ylabel('Temperature [K]') 650 600 Temperature [K] 550 500 450 400 350 300 250 0 500 1000 1500 2000 2500 3000 Enthalpy [kJ/kg] Figure 12.17 A plot of the T h plane of water at vapor-liquid equilibrium. At the critical point, where the two phases go to one, the enthalpies should approach the same value. Let us calculate the enthalpy of vaporization, ∆h vap = hV − h L , and plot it on our graph. >> h_vap = water_sat(:,9) - water_sat(:,5); >> plot(h_vap,water_sat(:,1),'-k') 479 480 Chapter 12 Interpolation 650 600 Temperature [K] 550 500 450 400 350 300 250 0 500 1000 1500 2000 2500 3000 Enthalpy [kJ/kg] Figure 12.18 A plot of the T h plane of water at vapor-liquid equilibrium. The black curve corresponds to the enthalpy of vaporization. We find that as the temperature increases, the enthalpy of vaporization decreases, until it ultimately goes to zero at the critical point. The other point to note is that the enthalpy of vaporization is not constant, which is assumed when using the Clausius-Clapeyron equation to extrapolate vapor pressure data... so only use the Clausius-Clapeyron equation over narrow temperature ranges. We can go on and on, but I think you get the picture. 12.8 Glossary interpolation: Estimating the value of a function using known values on either side. extrapolation: Estimating the value of a function using known values that don’t bracket the desired value. single-valued mapping: A mapping where each value in the range maps to a single value in the domain. multivalued mapping: A mapping where at least one value in the range maps to more than one value in the domain. 12.9 Exercises 481 12.9 Exercises Exercise 12.1 Thermistors are used to measure the temperature of bodies. Thermistors are based on materials’ change in resistance with temperature. To measure temperature, manufacturers provide you with a temperature versus resistance calibration curve. If you measure resistance, you can find the temperature. A manufacturer of thermistors makes several observations with a thermistor, which are given at the end of the problem. Determine the temperature corresponding to 754.8 ohms. R (ohm) 1101.0 911.3 636.0 451.1 T (◦ C) 25.113 30.131 40.120 50.128 Exercise 12.2 To try and estimate the critical point of water, we extrapolated to estimate where the difference between the liquid and vapor density were zero. We could have done the same thing with the enthalpy of vaporization, but given the shape of the phaseenvelope, we would expect similar results. Another approach pioneered by Guggenheim (a really, really smart thermodynamicist) was to plot the surface tension versus the temperature. The surface tension is the interfacial (vapor/liquid) free energy per unit area. At the critical point the free energy barrier between the phases goes to zero, so the surface tension goes to zero. Try Guggenheim’s method. Plot surface tension versus temperature, then estimate the critical point by extrapolating to where the surface tension is zero. Based on the plot, do you expect the result to be more promising? To help you import the necessary data, you can use the updated function import_sat_vle_gug. As an aside, Guggenheim’s method is really a very powerful tool, that is often unappreciated. It’s greatest utility is estimating the critical point for non-volatile fluids; I have seen the critical point of NaCl estimated in this way. Now why would someone want an estimate of the critical point of NaCl? So that we can model its phase behavior using an equation of state and to use or develop corresponding states theories. 13 Chapter Numerical Integration In Chapter 13 we learn how to numerically compute the definite integral of a function with a single variable. By the end of this chapter you will be able to: • Provide a basic explanation of how to numerically compute the definite integral of a function with a single variable • Exhibit ability to construct a function for the expression to be integrated • Apply integral to compute definite integrals • Use integral to compute definite integrals with tabulated data. If you work through the chapter and believe these goals are not met, please re-review the material and reach out for help. Some of the material on numerical integration is adopted from Rachel Duke and Spencer Sabatino from this course during Spring 2020. 13.1 Integration Integration is a fundamental operation of Calculus and is important for the solution of a wide range of engineering problems. Considering a function with a single variable, the definite integral of the function corresponds to the signed area of the function in the plane between two points (i.e., the “area under the curve”). For some function, the definite integral may readily be computed analytically. For example: · ¸¯ Z 2 1 3 ¯¯2 1 3 1 3 2 x dx = x ¯ = 2 − 0 = 8/3 = 2.6667 3 3 3 0 0 The “Power Rule” is one I remember. Within this expression, recall x 3 /3 is the antiderivative of x 2 . Others functions may be more complicated. However, many 483 484 Chapter 13 Numerical Integration antiderivates may be found in a list of integrals. But some may elude us. Consider the following: Z 2 2 e −x d x 0 which can not be evaluated analytically nor expressed in elementary functions. This function can, however, be evaluated numerically. Numerical integration is the practice of applying various algorithms and approximations to solve definite integrals. We often desire to employ numerical techniques for integration for a few reasons. First, as shown in expression section 13.1, our integral expression might be impossible to evaluate analytically, or at least without elementary functions, preventing us from solving by hand. Another big reason is simply because it would be quicker for us to evaluate integrals indirectly using these methods without a need to consult a list of integrals. This is similar to the motivation for solving ordinary differential equations numerically with ode45. For example, while section 13.1 could readily be solved analytically, to give you a preview of what’s to come, the numerical solution with MATLAB is as easy as: >> fune = @(X) X.^(2); >> sol = integral(fune,0,2) sol = 2.6667 And even the impossible becomes possible! >> fune = @(X) exp(-X.^(2)); >> sol = integral(fune,0,2) sol = 0.8821 Essentially, all that you need is a function for your function of interest. Recall the formal definition of an integral: Z b a f (x) = lim n→∞ n X ¡ ¢ f x i∗ ∆x i =1 While this definition may look involved, all it ultimately is saying is that a definite integral is an infinite summation of the area of infinitesimally small rectangles of width ∆x and height f (x ∗ ). This definition ultimately forms the basis of several common numerical integration techniques as we’ll discuss next. 13.2 Numerical Integration As one would anticipate, there are numerous ways to numerically evaluate integrals. Our first idea originates with the definition from section 13.1. Rather than use an infinite amount of infinitely small rectangles, we can elect to approximate the area under the curve with a finite amount of uniform rectangles. This technique is commonly introduced in your Calculus 1 courses as Riemann Sums, and 13.2 Numerical Integration 485 these approximations often work well. The orientation of the rectangle is often important too, as you could orient the rectangles so that the left side, right side, or midpoint of the rectangle is on the curve, often denoted as left hand, right hand, or midpoint sums. 8 7 6 5 4 3 2 1 0 0 0.5 1 1.5 Figure 13.1 The left-hand sum of x 3 over the range 0 to 2. Image from Wikipedia. 2 486 Chapter 13 Numerical Integration 8 7 6 5 4 3 2 1 0 0 0.5 1 1.5 2 Figure 13.2 The right-hand sum of x 3 over the range 0 to 2. Image from Wikipedia. 8 7 6 5 4 3 2 1 0 0 0.5 1 1.5 Figure 13.3 The midpoint sum of x 3 over the range 0 to 2. Image from Wikipedia. 2 13.2 Numerical Integration 487 We see that the computed integral wil be sensitive to how the rectangles are constructures. Additionally, as ∆x decrases we know that the accuracy will increase. That’s not the end of it though; why should we be limited to rectangles? Why not try more curved and unique shapes to better match the curve we’re working with? That’s where we run into the trapezoidal method, which relies upon summing the area of finitely many trapezoids instead of rectangles. Similar to our discussion of Euler’s method, let us assume that the function is linear over the range ∆x. The integral over this range is equivalent to the area of a trapezoid, 1 2 b (h 1 + h 2 ). Here, b = x 0 + ∆x − x 0 = ∆x, h 1 = f (x 0 ) and h 2 = f (x 0 + ∆x). The smaller the value of ∆x, the better the linear approximation, and in the limit that ∆x → 0 we expect the relation to be exact. 8 7 6 5 4 3 2 1 0 0 0.5 1 1.5 2 Figure 13.4 Application of the trapezoid method for x 3 over the range 0 to 2. Image from Wikipedia. Let’s us view the trapezoid method from a different angle. Namely, earlier I mentioned that power rule was one that I actually remembered. That is, I could readily compute the integral of a polynomical analytically. The trapezoid method assumes that the funciton is linear over the range x 0 to x 0 + ∆x. We can readily determine the equation of the line over this range as g (x) = mx + b where: m= f (x 0 + ∆x) − f (x 0 ) ∆x and b = f (x 0 ) − x 0 f (x 0 + ∆x) − f (x 0 ) ∆x 488 Chapter 13 Numerical Integration where here g (x) corresponds to the linear equation used to approximate the function f (x). Now integrating over the range x 0 to x 0 + ∆x: Z x0 +∆x hm i¯ ¡ ¢ 1 ¯ x 2 + bx ¯ 0x0 +∆x = ∆x f (x 0 + ∆x) − f (x 0 ) (mx + b) d x = 2 2 x x0 Why limit ourselves to assuming the function is linear? We could readily fit any order polynomial which could be integrated analytically. We would expect that higher order polynomials could better represent the function of interest over a range ∆x, allowing for a more accurate estimate of the integral. Additionally, I point out that ∆x need not be constant when numerically integrating. There may be some regions where smaller or larger values of ∆x are needed to achieve a given level of accuracy. A similar observation was made with Euler’s method. Fortunately for us, MATLAB provides an optimized function to perform numerical integration, integral which can determine the most appropriate method and optimal ∆x. 13.2.1 integral Evaluating definite integrals in MATLAB is rather straightforward. And if you have made it this far in the course, I hope you find it easy, especially compared to your Calculus course. Essentially, all that is needed is a function for the function of interest (with a single independent variable) that you wish to integrate. Just as with fzero, fsolve, and ode45, the inputs to this function are limited. Namely here the function can only have a single input, the independent variable. Parameters will need to be passed via the use of anonymous functions or nested functions. We then only need to specify the range of interest. Let’s give it a try and evaluate the integral of a function whose antiderivative cannot be expressed in closed form: Z 10 p −x xe d x 0 >> fun = @(X) sqrt(X).*exp(-X); >> sol = integral(fun,0,10) sol = 0.8861 We see our first step was to create a function. This could be in a separate function file, or here I use an anonymous function. When using integral, your function needs to be vectorized. Next, we use integral. In its basic form, the first variable is the function handle for the function to integrate, the second variable is the lower-limit, and the third argument is the upper-limit. Remember, an anonymous function is alread of type function handle, so @ is not needed. If you have used a separate function file, then we would need @. That’s it! The integral function is fairly robust, and can even handle infinite limits. Consider: Z ∞ p −x 1p xe d x = π 2 0 13.2 Numerical Integration 489 >> sol = integral(fun,0,inf) sol = 0.8862 >> sol_check = 0.5*sqrt(pi) sol_check = 0.8862 Nice! Next, let’s have some fun! Let’s create an anonymous function to evaluate the integral for an arbitrary upper-limit. >> int_fun = @(xlim) integral(fun,0,xlim); >> sol = int_fun(10) sol = 0.8861 This is a function with a single input and single output. We can use fplot to plot the intgral as a function of the upper-limit. Note that here we can use a lower-limit of 0 since the integral from 0 to 0 is just... 0. This will not result in an error as we had with ode45. >> fplot(int_fun,[0,10]) 0.8 0.7 0.6 0.5 0.4 0.3 0.2 0.1 0 0 2 4 6 8 10 Figure 13.5 fplot(int_fun,[1e-6,10]) Nice! Wait, we can do more. What is you wanted to know the value of the upper-limit for which the integral was equal to 0.5? Well, again, we have a function with a single input and a single output. That sounds like something I can use with our old friend fzero. We can set-up our error function in a number of ways. Here I will use what I suspect is the most clear. 490 Chapter 13 Numerical Integration >> int_fun_error = @(xlim) integral(fun,0,xlim) - 0.5; >> sol = fzero(int_fun_error,[0,10]) sol = 1.3630 Awesome! 13.2.2 Working with Tabulated Data We have seen that the first variable required to use integral is of type function handle. Therefore, integral can not directly be used to integrate tabulated data. However, it is possible if we combine with our friend interp1. Let’s work through and example to see this in action. First, let’s start with data provided below: >> X = [0, 1, 2, 3, 4, 5, 6, 7]; >> Y = [5, 6, 6.5, 7, 8, 8.3, 9, 11]; Here y = f (x) and we would like to evaluate the integral: 7 Z f (x) d x 0 In order to use integral, we need a function where for a specified value of x it returns the corresponding value of y. One way for us to acheive this is using interp1. Let’s consider the case of linear and cubic interpolation. >> fun_linear = @(x) interp1(X,Y,x,'linear',NaN); >> fun_cubic = @(x) interp1(X,Y,x,'pchip',NaN); >> sol_linear = integral(fun_linear,0,7) sol_linear = 52.8000 >> sol_cubic = integral(fun_cubic,0,7) sol_cubic = 52.6833 Nice! Just to get an idea of how linear interpolation and cubic interpolation are able to model the tabulated data, let’s make a plot: >> >> >> >> hold on plot(X,Y,'ko') fplot(fun_linear,[0,7],'-k') fplot(fun_cubic,[0,7],'-r') 13.2 Numerical Integration 491 11 10 9 8 7 6 5 0 1 2 3 4 5 6 7 Figure 13.6 The black circles are the tabulated data, the black line is the use of linear interpolation, and the red line is the use of cubic interpolation. Very cool. And just think, you could even combine this with fzero! While this is relatively straightforward, know too that MATLAB has a built-in function trapz which can be used to directly integrate tabulated data. And as you might guess from the name, it use trapezoid method. >> sol = trapz(X,Y) sol = 52.8000 And as we would expect, this answer is exactly the same as the use of linear interpolation. Does that make sense to you? And that’s it. I trust that you are now prepared to take on any integrals in the future, no matter how complicated. If you ever encounter multi-dimensional integrals, no worries. MATLAB still has you covered. Have a look at integral2 and hrefhttps://www.mathworks.com/help/matlab/ref/integral3.htmlintegral3. 492 Chapter 13 Numerical Integration 13.3 Exercises Exercise 13.1 The change in molar entropy of an ideal gas is given by the following equation: Z T2 ig CP P2 ig ig ∆S ig = S 2 (T2 , P 2 ) − S 1 (T1 , P 1 ) = d T − R ln P1 T1 T where T1 and T2 are the initial and final temperature, respectively, P 1 and P 2 are the initial and final pressure, respectively, R is the molar gas constant, 8.314 J/(mol K), and ig C P is the constant pressure heat capacity which is given by the following equation: ig CP R = a0 + a1 T + a2 T 2 + a3 T 3 + a4 T 4 ig Where T is in units of K. Know that C P /R is dimensionless; multiply by R in your favorite ig units to get C P . For methane, we have a 0 = 4.568, a 1 = −8.975 × 10−3 , a 2 = 3.631 × 10−5 , a 3 = −3.407 × 10−8 and a 4 = 1.091 × 10−11 . 1. Methane is available at T1 = 300 K and P 1 = 1 bar, and is to be compressed to T2 = 500 K and P 2 = 100 bar. Compute ∆S ig . 2. Methane is available at T1 = 300 K and P 1 = 1 bar, and is isentropically compressed (∆S ig = 0) to P 2 = 100 bar. Solve for T2 . 14 Chapter Monte Carlo In this chapter we will briefly discuss the topic of Monte Carlo sampling (or integration). Monte Carlo integration is a powerful tool to calculate integrals of complicated, multidimensional functions. What do I mean by complicated and multidimensional? My PhD research was in the area of statistical thermodynamics, and a major component of my PhD dissertation was developing strategies to compute classical configurational integrals of fluid-phase systems. Imagine we have a fluid-phase system consisting N Argon atoms: Z Z N = e −βU (~r ) d~ rN V where Z N is the classical configurational integral, U is the interaction energy of the system which is a function of the location of every atom in the system, β−1 = k B T , and the integral is over the coordinates of all of the atoms in the system. What does this mean? The location of an atom, regardless of your coordinate system, requires three coordinates, say x, y, and z. So if we have N atoms, this corresponds to 3N independent variables. If we have just N = 10 atoms, this corresponds to a function with 59,049 independent variables. These are the types of problems that Monte Carlo integration is perfectly suited for. If you have a simple one-dimensional (1-D) problem, stick to Simpson’s rule and the other techniques you learned in you Calculus class. (If Simpson’s rule isn’t familiar, what about the trapezoid method?) So how does it work? Monte Carlo integration get’s it name from the gambling city in Monaco, and is based on the concept of random sampling. It was developed shortly after the time of World War II. At that time, the first supercomputers were developed to help the war effort, and after the war, scientists were left with some cool toys to experiment with. A group of scientists at Los Alamos National Laboratory developed Monte Carlo integration as a tool to compute the classical configurational integral of a hard sphere fluid. (Although the same method was developed shortly before this time at Berkeley National Lab/UC Berkeley, but the findings were never published.) The basic idea, as presented to me, was imagine I 493 494 Chapter 14 Monte Carlo would like to compute the area of a pond. I have no way of measuring the area of the pond directly. However, I can layout a rectangular bounding box around the pond of known dimensions. I like rectangles because I can easily compute it’s area. So this could be taken as first guess of the area of the pond, which is smaller. The experiment then proceeds that I walk around the bounding box and randomly throw rocks. If I hear a splash, the rock landed in the pond, it was a hit. If I don’t hear a splash, then I hit land. I can then determine the fraction of the rocks I randomly threw that hit the pond relative to the total number. Assuming I randomly threw the rocks (and I threw an infinite number), this ratio should be equal to the ratio of the area of the pond to the bounding box (the total area). So if I multiply this number by the area of the bounding box, which is known, I get an estimate of the area of the pond. Cool! So let’s see it in practice and use it to numerically estimate a value of π. We wish to compute the area of a circle with radius r = 1. The area of this circle is A circle = πr 2 = π. However, π is unknown and is the quantity we wish to compute. We will do so using Monte Carlo integration. To accomplish this, I center my circle at the origin (0, 0), and then construct a bounding box with an edge length of 2r . The area of this bounding box will be A = (2r )2 = 4. Let’s graph the circle and bounding box: >> >> >> >> >> >> >> >> hold on Xs = [-1,1,1,-1,-1]; Ys = [1,1,-1,-1,1]; plot(Xs,Ys,'-k') % Plotting the bounding box theta = 0:pi/100:2*pi; Xc = cos(theta); Yc = sin(theta); plot(Xc,Yc,'-r') % Plotting the circle using 100 points 14.1 Random number generator basics 495 1 0.8 0.6 0.4 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 -1 -0.5 0 0.5 1 Figure 14.1 Plot of circle with bounding box. ¡ ¢ Now for the recipe. We will randomly sample x, y coordinates over the range ¡ ¢ −1 ≤ x ≤ 1 and −1 ≤ y ≤ 1. Then for each x, y coordinate, we need to determine if we are in or out of the circle. The equation of a circle of radius r = 1 and centered at the origin is: x2 + y 2 = 1 So, if x 2 + y 2 ≤ 1, we are inside the circle. We then determine the fractions (or density, ρ) of hits relative to the total number of trials: ρ= Nhits Ntrials Then A circle = ρ A = 4ρ = π Great! Now let’s do it. Before doing so, let’s pause for a moment and briefly discuss the generation of random numbers, which is the heart of any Monte Carlo program. 14.1 Random number generator basics Using the command >> rand(1,n) 496 Chapter 14 Monte Carlo we can generate a vector of length n containing numbers randomly distributed between 0 and 1. Therefore >> 100*rand(1,n) will be a vector of numbers randomly distributed between 0 and 100, and >> round(100*rand(1,n)) will be a vector of integers randomly distributed between 0 and 100. Lastly, >> round(100*rand(1,n)-50) will give a vector of integers randomly distributed between –50 and +50. (Note that both the 100 and –50 could be included inside or outside the round function with the same result.) So we can readily scale and shift our range. For our problem of calculating π, we need random numbers between –1 and 1. This is therefore accomplished as: >> 2*rand(1,n)-1 When we use rand(1,n) to generate a vector of length n containing numbers randomly distributed between 0 and 1, the numbers are uniformly distributed. That is, all of the numbers between 0 and 1 may occur with equal probability. There is an entire field dedicated to developing and testing random number generators. One key characteristic is the period. That is, how many unique numbers the random number generator may generate before the string of numbers repeats. If you are interested in these things, you should go speak with my friend Alan Ferrenberg. One of his many claims to fame is pointing out issues related to the period of common random number generators, which was of great interest to state lotteries across the USA. Fortunately for us, MATLAB’s default random number generator is the Mersenne Twister. The Mersenne Twister has a period of 219937 − 1; that’s a lot of numbers! When MATLAB generates a sequence of random numbers, it is dependent on the current value of the “seed”. Every time your restart MATLAB it begins with a seed value of 0. So if each time you start MATLAB you were to execute the command rand(1,10), you would obtain identical results. Rather than restarting MATLAB, you can restore a seed value of 0 using either rng(0) or rng(’default’). Note that each time you generate a new random number, the seed value changes. Knowing this is beneficial because it allows you to reproduce your results and can be helpful for debugging. But if you want truly random results, try setting your seed when you begin a new MATLAB session with rng(’shuffle’). This will use a seed based on the current time, which is always unique. 14.2 Back to π 497 Now let’s return to our regularly scheduled programming... 14.2 Back to π Now that we know how to generate random numbers, let’s write a function to compute π. You will notice that I do not write a for loop to loop over all of the random samples. When the number of samples becomes large this becomes very slow. I instead take advantage of MATLAB’s vector operations. Listing 14.1 circle_area.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 % % % % Function to perform Monte Carlo simulation to estimate the value of pi. This is accomplished by computing the area of a circle of radius 1, for which the area is equal to pi. The bounding square will be of area (2*r)^(2) = 4. function res = circle_area(n) % Generate a vector of random numbers of length n. % We will have one vector corresponding to the x value and another % corresponding to the y value. % % The numbers will be uniformly distributed between 0 and 1. % We want numbers between -1 and 1. So first multiply by 2, so the % numbers are between 0 and 2, then shift by -1 so they are between % -1 and 1. X = 2*rand(1,n)-1; Y = 2*rand(1,n)-1; end % The equation of our circle is x^2 + y^2 = 1. Check to see if % our x,y coordinates fall within the circle. (I.e., <= 1) Circle = X.^(2) + Y.^(2); % Performing a logical vector comparison. Circle_hits = Circle <= 1; % This is logical vector, so 1 for true and 0 for false. So if % we sum the vector, that will give the number of hits. If we divide % by the length of the vector (or n), that gives us the fraction that % were hits. Since the area of the bounding square is (2*r)^(2)=4, % multiply the fraction of hits by the area of the square to get % the area of the cirle. Since r=1, the area of the circle % is pi. frac_hits = sum(Circle_hits)/n; res = frac_hits*4; Now let’s give it a try using 10, 1,000, 10,000, and 100,000 samples. >> circle_area(10) ans = 2 >> circle_area(1000) 498 Chapter 14 Monte Carlo ans = 3.2160 >> circle_area(10000) ans = 3.1332 >> circle_area(1000000) ans = 3.1427 Cool! Not too bad. Remember, >> pi ans = 3.1416 As the number of trials increases, we appear to be getting closer and closer to the correct result. Let’s write a script that loops over a range of trials and plots the results. And as a reference, I will plot the value of π. Listing 14.2 pi_convergence.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 % This script is a simple for loop to estimate values of pi using % the function circle_area for a range of number of Monte Carlo trials. % Number of trials N = 10:10:1e5; % Initialize our vector of estimates of pi Sol = ones(1,length(N)); for i=1:length(N) Sol(i) = circle_area(N(i)); end hold on plot(N,Sol,'-ko') ylabel('estimate of pi') xlabel('number of trials') % Now plot as a reference y=pi Ref = pi * ones(1,length(N)); plot(N,Ref,'-r') 14.2 Back to π 499 3.5 3.4 3.3 estimate of pi 3.2 3.1 3 2.9 2.8 2.7 2.6 0 2 4 6 number of trials 8 10 10 4 Figure 14.2 The value of π estimated using Monte Carlo integration versus the number of samples. We see that we converge rather quickly, but fluctuations about the mean persist. Last, before moving on, I will update the function circle_area to plot the trials (or sample points). The updated function is below, followed by plots generated by executing the function for the case of 10, 1,000, and 10,000 trials. 500 Chapter 14 Monte Carlo Listing 14.3 circle_area_plot_trials.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 % % % % % % % Function to perform Monte Carlo simulation to estimate the value of pi. This is accomplished by computing the area of a circle of radius 1, for which the area is equal to pi. The bounding square will be of area (2*r)^(2) = 4. In this version of the code I will the square, circle, and trial points. function res = circle_area_plot_trials(n) % Generate a vector of random numbers of length n. % We will have one vector corresponding to the x value and another % corresponding to the y value. % % The numbers will be uniformly distributed between 0 and 1. % We want numbers between -1 and 1. So first multiply by 2, so the % numbers are between 0 and 2, then shift by -1 so they are between % -1 and 1. X = 2*rand(1,n)-1; Y = 2*rand(1,n)-1; % The equation of our circle is x^2 + y^2 = 1. Check to see if % our x,y coordinates fall within the circle. (I.e., <= 1) Circle = X.^(2) + Y.^(2); % Performing a logical vector comparison. Circle_hits = Circle <= 1; % This is logical vector, so 1 for true and 0 for false. So if % we sum the vector, that will give the number of hits. If we divide % by the length of the vector (or n), that gives us the fraction that % were hits. Since the area of the bounding square is (2*r)^(2)=4, % multiply the fraction of hits by the area of the square to get % the area of the cirle. Since r=1, the area of the circle % is pi. frac_hits = sum(Circle_hits)/n; res = frac_hits*4; hold on % Plot the trials. We plot them first so that the circle will % be printed on top. plot(X,Y,'ko') % Let's start by plotting the square Xs = [-1,1,1,-1,-1]; Ys = [1,1,-1,-1,1]; plot(Xs,Ys,'-k') % Next, we plot the circle % Creating a vector of angles. Let's plot 100 points theta = 0:pi/100:2*pi; Xc = cos(theta); Yc = sin(theta); plot(Xc,Yc,'-r') end 14.2 Back to π 501 1 0.8 0.6 0.4 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 -1 -0.5 0 0.5 1 0.5 1 Figure 14.3 10 trials. 1 0.8 0.6 0.4 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 -1 -0.5 0 Figure 14.4 1,000 trials. 502 Chapter 14 Monte Carlo 1 0.8 0.6 0.4 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 -1 -0.5 0 0.5 1 Figure 14.5 10,000 trials. 14.3 Evaluating Integrals in General The Monte Carlo method can be used to evaluate integrals in general. Our only restrictions will be that: 1. the function is always positive 2. integral of the function is finite Let’s consider the example of evaluating the integral of the function x 2 using Monte Carlo integration over an arbitrary range, say a to b: Z I= b x 2d x a We need to begin by defining a bounding box of known area. For the case of x 2 , I know that the function will be a maximum at either of the end points of the range of x, a or b. So we begin by evaluating the function at the end points, finding the maximum (y max ), and then define a bounding box of width b − a and height y max . The area of the bounding box is A = y max (b − a). ¡ ¢ Next, we will generate our random trials of x, y coordinates such that they fall inside our bounding box. For this case, we will generate values of x over the range a ≤ x ≤ b and values of y over the range 0 ≤ y ≤ y max . To determine if we have a hit, we evaluate our function at each of our trial values of x, here x 2 , and check to see if this value is less than or equal to our corresponding trial value of y 14.3 Evaluating Integrals in General hits (≤ y). If it is, it is a hit. As before, we compute the frequency of hits, ρ = NNtrials . The value of the integral (or area under the curve) is then found as I ≈ ρ A. Nice! Below is a function I wrote to evaluate the integral of x 2 over a specified range as just described. We will then evaluate its performance by comparing to ¢ Rb ¡ the analytic solution a = b 3 − a 3 /3. Listing 14.4 xsq_area.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 % function [res,ref] = xsq_area(n,X_range) % % Function to compute the integral of x^2 using Monte Carlo integration % Inputs: n, the number of trials % X_range, a vector of length 2 containing the range of % integration. The first element should be the lower % bound and the second value should be the upper bound. % Outpusts: res, the integral as computed using Monte Carlo integration % ref, the analytic solutions % function [res,ref] = xsq_area(n,X_range) % We need to size our bounding box. We know the smallest y value will be % 0, and here we find the largest y value. Y_ends = X_range.^(2); Y_range = [0,max(Y_ends)]; % Since we know that ymax will occur at the upper bound in our x range, % we could just as well use Y_ends(end) or Y_ends(2). % Generate the x coordinate of our trials. X spans the range of % X_range. Remember rand generates numbers uniformly over the range 0 % to 1. So first we multiple by the range of x values, and then shift % by the lower bound. X_trial = (X_range(2)-X_range(1))*rand(1,n)+X_range(1); % Generate the y coordinate of our trials. We will generate our y % coorindates in the same way. While we could simplify this for this % problem, we will write the expression in general. Y_trial = (Y_range(2)-Y_range(1))*rand(1,n)+Y_range(1); % Determine number of hits. This will be when the coordinates of the % trial are less than f(x). In general we could create a function file % for f(x), but here x^2 is easy enough. N_hits = Y_trial <= X_trial.^(2); % The fraction of trials that are hits frac_hits = sum(N_hits)/n; % Multiplying the fraction of hits by the area of the bounding box to % get the integral (or area) of x^2. res = frac_hits*(Y_range(2)-Y_range(1))*(X_range(2)-X_range(1)); end % The analaytic solution for comparison ref = (1/3)*(X_range(2)^(3)-X_range(1)^(3)); 503 504 Chapter 14 Monte Carlo >> [mc_int, ref_inf] = xsq_area(1e6,[-2,6]) mc_int = 74.7360 ref_inf = 74.6667 One million samples takes very little time to execute, and the Monte Carlo estimate differs from the reference value by just 0.07 or 0.09%. 14.4 Simplifying and Generalizing the Algorithm The algorithm of the previous section is robust and will work in general when we have a funciton that is positive. However, the need to set-up a bounding box makes it challenging to generalize and automate the calculation for an arbitrary function. And what about functions that are both positive and negative? Here we will overcome this limitation. Imagine we wish to evaluate the integral of the function f (x) over the range a ≤ x ≤ b: b Z I= f (x) d x a Our random number generator samples from a uniform distribution. We saw rand(1,n) generated a vector of length n of numbers uniformly distributed between 0 and 1. This means each number will be observed with equal probability. We could use our random number generator to sample values of x over the range a ≤ x ≤ b as (b-a)*rand(1,n)+a. Since each number will be observed with equal probability, we can write: p (x) = C where p (x) is the probability of observing a value of x, and C is a constant. If the probability distribution is normalized, then: Z b a Z p (x) d x = b a C d x = C (b − a) = 1 For this to be true, it must be that: p (x) = 1 b−a Okay, so how does this help us out. The key is that b − a is a constant. So we can re-write our original integral as: 14.4 Simplifying and Generalizing the Algorithm Z I= b f (x) d x a Z b (b − a) dx − a) (b a Z b 1 = (b − a) f (x) dx (b − a) a Z b f (x) p (x) d x = (b − a) = f (x) a The integral in the last line is our definition of the expectation value of f (x). Assuming a large number of trials (or samples, n), the expectation value may be estimated as the arithmetic average of function: Z I= b a n ­ ® (b − a) X f (x i ) f (x) d x = (b − a) f (x) ≈ n i =1 The larger the number of trials, n, the better the approximation. This results in a much simpler algorithm: 1. Generate a vector of uniformly distributed random numbers over the range of interest (a ≤ x ≤ b). This corresponds to the trials or random samples of x. 2. Evaluate the function f (x) for each value of x from the first step. 3. Compute the arithmetic average of the function f (x) evaluated at each trial of x. This results in a much simpler function that may readily be generalized. Let’s apply this new algorithm to again integrate x 2 . 505 506 Chapter 14 Monte Carlo Listing 14.5 xsq_area_simple.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 % function [res,ref] = xsq_area_simple(n,X_range) % % Function to compute the integral of x^2 using Monte Carlo integration % Inputs: n, the number of trials % X_range, a vector of length 2 containing the range of % integration. The first element should be the lower % bound and the second value should be the upper bound. % Outpusts: res, the integral as computed using Monte Carlo integration % ref, the analytic solutions % function [res,ref] = xsq_area_simple(n,X_range) % Generate the x coordinate of our trials. X spans the range of % X_range. Remember rand generates numbers uniformly over the range 0 % to 1. So first we multiple by the range of x values, and then shift % by the lower bound. X_trial = (X_range(2)-X_range(1))*rand(1,n)+X_range(1); % Evaluate our function at the x trial values Y = X_trial.^(2); % Estimate the integral using MC res = sum(Y)./n*(X_range(2)-X_range(1)); end % The analaytic solution for comparison ref = (1/3)*(X_range(2)^(3)-X_range(1)^(3)) >> [mc_int, ref_inf] = xsq_area(1e6,[-2,6]) mc_int = 74.5899 ref_inf = 74.6667 Nice! What is beautiful as compared to the previous version of my code is I no longer need to worry about the bounding box and trying to determine the maximum y value. This readily lends itself to being generalized and eliminating the restriction that out function be positive. Rather than writing a specific code for a particular function, let’s generalize the code by allowing the user to pass a function handle for whatever function they would like integrated. Before doing so, let me again remind you of our one restrictions: 1. the integral of the function is finite 14.4 Simplifying and Generalizing the Algorithm Listing 14.6 monte_carlo_int.m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 % function res = monte_carlo_int(fcn_handle,n,X_range) % % Function to compute the integral of a function using Monte Carlo % integration. The method is restricted to functions that are positive, % and the integral must be finite. % Inputs: fcn_handle, function handle for the function you wish to % integrate. Note that for this code to work, the % function must be vectorized. % n, the number of trials % X_range, a vector of length 2 containing the range of % integration. The first element should be the lower % bound and the second value should be the upper bound. % Outpusts: res, the integral as computed using Monte Carlo integration. % function res = monte_carlo_int(fcn_handle,n,X_range) % Generate the x coordinate of our trials. X spans the range of % X_range. Remember rand generates numbers uniformly over the range 0 % to 1. So first we multiple by the range of x values, and then shift % by the lower bound. X_trial = (X_range(2)-X_range(1))*rand(1,n)+X_range(1); % Evaluate our function at the x trial values Y = fcn_handle(X_trial); % Estimate the integral using MC res = sum(Y)./n*(X_range(2)-X_range(1)); end >> xsq = @(X) X.^(2); >> mc_int = monte_carlo_int(xsq,1e6,[-2,6]) mc_int = 74.7077 >> mc_int = monte_carlo_int(xsq,1e8,[-2,6]) mc_int = 74.6560 This agrees with our previous result. You must appreciate the beauty of this. With just three lines of code, we can estimate the integral of any function (with a single independent variable), no matter how complicated. While the advantage of using Monte Carlo integration over conventional methods may not be clear for functions with a single variable, the method really shines for multi-dimensional integrals. Before wrapping-up, let’s us consider a well known function that is known to be both positive and negative. Let’s consider: 2π Z 0 sin (x) d x = 0 >> fun = @(X) sin(X); >> sol = monte_carlo_int(fun,1e6,[0,2*pi]) sol = -0.0018 507 508 Chapter 14 Monte Carlo >> sol1 = monte_carlo_int(fun,1e6,[0,pi]) sol1 = 1.9989 >> sol2 = monte_carlo_int(fun,1e6,[pi,2*pi]) sol2 = -2.0001 where I have also split the integral to show the ability to evaluate the integral of both positive and negative functions. 14.5 Exercises Exercise 14.1 Write a script or function to demonstrate the use of monte_carlo_int.m for a function of your choice, and compare to built-in function integral. Chapter 15 An Introduction to Symbolic Calculations 15.1 Some Symbolic Variable Basics Before we can get started, we first need to learn how to create a symbolic variable. This is relatively straightforward. We just need the keyword syms followed by the name of the variable (or variables) that you wish to be symbolic. For example: >> syms x With our symbolic variable, we can do things like create functions. Consider the case of: f (x) = 4x 4 − 2x + 3 >> f = 4*x^(4)-2*x+3 f = 4*x^4 - 2*x + 3 Here we have assigned the expression on the right hand side to the variable f. And since x is a symbolic variable, f will be symbolic too. If you would like to evaluate your function for a specific value of x, this can be accomplishd using the subs function. The basic call to subs used here will use as inputs: (1) the expression, (2) the symbolic variable that you would like to substitute a numerical value for, and (3) the numerical value you would like to substitute. For example: f (x = 3) = 4 · 34 − 2 · 3 + 3 = 321 >> f_3 = subs(f,x,3) f_3 = 321 Nice! And not to be outdone, we can plot our function as we did before using fplot. >> fplot(f,[-1,1],'-k') 509 510 Chapter 15 An Introduction to Symbolic Calculations 9 8 7 6 5 4 3 -1 -0.5 0 0.5 1 Figure 15.1 fplot(f,[-1,1],’-k’) Let’s consider also the case of: g (x) = ex x >> g = exp(x)/x g = exp(x)/x Next, let’s see what happens if we try to evaluate our function again at x = 3. >> g_3 = subs(g,3) g_3 = exp(3)/3 Here we obtain the exact answer expressed symbolically. But what if we wanted a numerical value? This is readily accomplished with the function double. The function double converts a symbolic variable to a (double precision) numerical value. Notice in the workspace window that g_3 and f_3 are of type symbolic. We can get a numerical value as: >> g_3_numeric = double(g_3) g_3_numeric = 6.6952 15.1 Some Symbolic Variable Basics Figure 15.2 Snapshot of Workspace window. By default Matlab will assume that your symbolic variable (or variables) can take on both real and imaginary components. If you would like to restrict yourself to reals only, for example, then you would add the following after creating your symbolic variable: >> assume(x, 'real') Please consult the documentation page for assume. This can be very useful if you are only interested in a certain range of values. While so far we have created symbolic variables and assigned expressions to symbolic variables, it is also possible to create symbolic functions. Symbolic functions look and act just like a function you might see in your mathematics course. >> f(x) = 4*x^(4)-2*x+3 f(x) = 4*x^4 - 2*x + 3 >> f_3 = f(3) f_3 = 321 >> g(x) = exp(x)/x g(x) = exp(x)/x >> g_3 = g(3) g_3 = exp(3)/3 >> g_3_numeric = double(g_3) g_3_numeric = 6.6952 Here f and g are symbolic functions as compared to symbolic variables. 511 512 Chapter 15 An Introduction to Symbolic Calculations Figure 15.3 Snapshot of Workspace window. Is it better to store the expression to a symbolic variable or to create a symbolic variable? Honestly, at present the answer is unclear to me. 15.2 Limits Matlab provides the function limit to calculate the limits of functions. In its basic form limit takes three arguments: (1) the function, (2) the symbolic variable that you would like to look at a limiting value of, and (3) the value of the limiting value, which may include inf for ∞. Using standard notation, limit(f,x,inf) would correspond to: lim f (x) x→∞ At times it may additionally be necessary to evaluate a directional limit. No worries, Matlab can handle this case with an optional fourth argument ’right’ or ’left’. Your Calculus textbook likely has an entire chapter devoted to computing limits, and should be cosulted for further technical details. Here we will only be concerned with the use of Matlab to evaluate limits, and will consider a series of examples ranging from basic to more challenging to demonstrate the power of Matlab and its simplicity of use. 15.2.1 Basic Limits Let’s get warmed up by evaluating some basic limits. I will refer to a basic limit as that of a well-behaved function that is continuous at the point c where the limit is desired. The limit may therefore be found using direct substitution. Below, we will take a look at some basic examples. For each example, I will display the 15.2 Limits 513 desired problem. This will be followed by actual Matlab input and output used. I will start off by clearing variables and creating a symbolic variable x. lim x x→−4 >> >> >> l1 syms x assume(x,'real') l1 = limit(x,x,4) = 4 lim x 2 x→2 >> l2 = limit(x^2,x,2) l2 = 4 x2 + x + 2 x→1 x +1 lim >> l3 = limit((x^2+x+2)/(x+1),x,1) l3 = 2 lim x→2 p x >> l4 = limit(sqrt(x),x,2) l4 = 2^(1/2) >> l4_numeric = double(l4) l4_numeric = 1.4142 where double was used to give a numerical result. lim x · cos (x) x→π >> l5 = limit(x*cos(x),x,pi) l5 = -pi >> l5_numeric = double(l5) l5_numeric = -3.1416 514 Chapter 15 An Introduction to Symbolic Calculations 15.2.2 Intermediate Forms and L’Hôpital’s Rule Recall from your Calculus class that direct substitution does not work all of the time. A common scenario occurs when direct substitution leads to an indeterminate of the form 0/0 or ∞/∞. These cases are called indeterminate because they do not guarantee that a limit exists, nor do they indicate what the limit is, if one does exist. In your Calculus class, you should have learned to solve such problems using L’Hôpital’s rule. Such problems are solved effortlessly using Matlab, with no need for user input beyond that used for basic limits. Let’s look at a few examples. e 2x − 1 x→0 x lim >> l6 = limit((exp(2*x)-1)/x,x,0) l6 = 2 lim x→∞ ln x x >> l7 = limit( log(x)/x,x,inf) l7 = 0 15.2.3 Directional Limits Lastly, we will consider the case of directional limits. Directional limits may be useful to determine if a function is continuous at a point c. If a function is continuous at point c, the limit from the left and right should be equivalent. Let’s consider a simple case. We would like to know if the function f (x) = |x| /x is continuous at x = 0. To text, we will compute the limit from the right (positivedirection) and from the left (negative-direction). >> l8_pos = limit(abs(x)/x,x,0,'right') l8_pos = 1 >> l8_neg = limit(abs(x)/x,x,0,'left') l8_neg = -1 We find that the function is not continuous at x = 0. We can generate a plot to confirm this is the case. >> fplot(abs(x)/x,[-1,1],'-k') 15.2 Limits 515 1 0.8 0.6 0.4 0.2 0 -0.2 -0.4 -0.6 -0.8 -1 -1 -0.5 0 0.5 1 Figure 15.4 fplot(abs(x)/x,[-1,1],’-k’) Nice! And what would happen if a direction was not specified? >> l8 = limit(abs(x)/x,x,0) l8 = NaN The limit does not exist! 15.2.4 Limits and Derivatives The definition of the derivative of a function is in terms of limits. Namely, the derivative of a function f at x is given by: df f (x + ∆x) − f (x) = f 0 (x) = lim ∆x→0 dx ∆x Or equivalently we will use the following notation where ∆x = h: df f (x + h) − f (x) = f 0 (x) = lim h→0 dx h Using Matlab, we can calculate the differential of a function using this definition and limit. Here I will start by clearing variables since we will need an additional symbolic variable h. We will consider cases for which I can find the differential in the front cover of my Calculus textbook. d x e = ex dx 516 Chapter 15 An Introduction to Symbolic Calculations >> syms x h >> d1 = limit((exp(x+h)-exp(x))/h,h,0) d1 = exp(x) And what if you wanted to evaluate the derivative at a specific value? >> d1_3 = subs(d1,x,3) d1_3 = exp(3) >> d1_3_numeric = double(d1_3) d1_3_numeric = 20.0855 where here we considered the case of x = 3. We can also work out derivatives in general. d x b = b x ln (b) dx >> syms b >> d2 = limit( (b^(x+h)-b^(x))/h,h,0) d2 = b^x*log(b) 15.3 Derivatives While we have just shown that we can use limit to evaluate derivatives, this is more easily accomplished using the function diff. In its basic form, diff takes two input arguments: (1) the function and (2) the (symbolic) variable to differentiate with respect to. We can also pass an optional third parameter, a scalar to indicate the order of differentiation. Let’s demonstrate using a few examples. I will again begin by clearing all variables. d x e = ex dx >> syms x >> d1 = diff(exp(x),x) d1 = exp(x) d x b = b x ln (b) dx >> syms b >> d2 = diff(b^x,x) d2 = b^x*log(b) 15.4 Indefinite and Definite Integrals For an example of higher-order differentiation, I will consider an example for which I can work out the solution analytically. f (x) = x 3 d 3 x = 3x 2 dx d2 3 d 2 x =3 x = 6x 2 dx dx d3 3 d x =6 x =6 3 dx dx >> d2_1 = diff(x^3,x,1) d2_1 = 3*x^2 >> d2_2 = diff(x^3,x,2) d2_2 = 6*x >> d2_3 = diff(x^3,x,3) d2_3 = 6 Nice! Know that Matlab also works very nicely with symbolic functions, which eliminates the need for subs. >> f(x) = exp(x); >> f(2) ans = exp(2) >> Df = diff(f,x) Df(x) = exp(x) >> Df(2) ans = exp(2) where note that x has already been defined as a symbolic variable. Instead of just assigning exp(x) to a symbolic variable f as we did at the start of this chapter, now we have a symbolic function with an explicit input of x. 15.4 Indefinite and Definite Integrals We can compute both the indefinite and definite integral of a symbolic function using the function int. To compute an indefinite integral int requires two inputs: (1) the function and (2) the variable you are integrating with respect to. To compute a definite integral, two additional input variables are required, namely the limits of integration. Z 1 xd x = x 2 + c 2 517 518 Chapter 15 An Introduction to Symbolic Calculations where c is our constant of integration. 2 Z 0 xd x = ¤ 1 £ 2 ¤¯¯2 1 £ 2 x 0 = 2 − 02 = 2 2 2 >> i1 = int(x,x) i1 = x^2/2 >> i1_d = int(x,x,0,2) i1_d = 2 Of course too we could compute the definite integral by taking the difference in the antiderivative evaluated at the two limits. >> i2 = int(x,x) i2 = x^2/2 >> i2_d = subs(i2,x,2) - subs(i2,x,0) i2_d = 2 >> i3(x) = int(x,x) i3(x) = x^2/2 >> i3_d = i3(2) - i3(0) i3_d = 2 Cool! What if you knew the lower limit was 0, but wanted to consider various values of the upper limit? >> syms b >> i4(b) = int(x,x,0,b) i4(b) = b^2/2 >> i4(2) ans = 2 Building off of this, what if you wished to solve for the value of b such that b Z 0 xd x = 11 For that, we can readily use Matlab’s symbolic solve function. The function solve requires just two inputs: (1) the equation of interest, and (2) the symbolic variable you wish to solve for. Before we see this in action, I would like to remind you that in Matlab = corresponds to a variable assignment, while == corresponds to equivalence. >> bsol = solve(i4 == 11,b) bsol = 15.5 More fun with solve 519 22^(1/2) -22^(1/2) >> bsol_numeric = double(bsol) bsol_numeric = 4.6904 -4.6904 Excellent! Notice that with solve, I do not need to set-up the function so that it is equal to 0 as we did with fzero or fsolve. Now what if we were told that b was greater than 0? Well, we can use assume to help. >> assume(b>0) >> bsol = solve(i4 == 11,b) bsol = 22^(1/2) Know that I like the notation assume(b>0) as it can be used in general. However, Matlab also has a keyword for this case: assume(b, ’positive’). 15.5 More fun with solve Do you remember the quadratic formula? If the answer is “no” or you are unsure, no worries, Matlab remembers. I will first clear my variables to start fresh, and then give it a try. And for those of us that have forgotten: ax 2 + bx + c = 0 p −b ± b 2 − 4ac x= 2a >> clear variables >> syms a b c x >> f = a*x^2 + b*x + c == 0 f = a*x^2 + b*x + c == 0 >> qf = solve(f,x) qf = -(b + (b^2 - 4*a*c)^(1/2))/(2*a) -(b - (b^2 - 4*a*c)^(1/2))/(2*a) Nice! What if we wanted to evaluate the case of a = 4, b = 6, and c = 2? >> qf_eval = subs(qf, [a,b,c], [4,6,2]) qf_eval = -1 -1/2 520 Chapter 15 An Introduction to Symbolic Calculations Here we were substituting multiple variables at once, so we list them all out as a vector. Nice! We could have instead used a symbolic function. Try this: >> qf(a,b,c) = solve(f,x) qf(a, b, c) = -(b + (b^2 - 4*a*c)^(1/2))/(2*a) -(b - (b^2 - 4*a*c)^(1/2))/(2*a) >> qf_eval = qf(4,6,2) qf_eval = -1 -1/2 Fantastic! So far we have made solve look like an all start. It is important to show some limitations. Consider the following equation from Exam 2: cos (x) cosh (x) = 1 We were asked to solve for the first five positive roots. To limit our range of search, we could plot and see over what range the first five roots occur over. We could then use assume to set the range of x values. Here I will just arbitarily set the range, and then could always update the upper limit (that is, the upper assume) to change the range. I will start by clearing my variables in case I had made any assumptions previously. >> syms x >> assume(x>0) >> assume(x<20) >> eq = cos(x)*cosh(x) == 1; >> sol = solve(eq,x) Warning: Unable to solve symbolically. Returning a numeric solution using vpasolve. > In solve (line 304) sol = 0 So there you have it. No analytic solution exists, so it is solving numerically. And when it solves numerically, just like before we get just one root at a time. To get a different value, my search range needs to change. For example >> assume(x>1) >> sol = solve(eq,x) Warning: Unable to solve symbolically. Returning a numeric solution using vpasolve. > In solve (line 304) sol = 20.420352245626061090936411189313 15.6 Factor my polynomial please 15.6 Factor my polynomial please Related to solve, Matlab can also be used to factor a polynomial. This can be useful to both simplify your expression and to solve for the roots. The Matlab function that can do this is appropriately names... factor. In its basic form used here, factor will take two arguments: (1) the expression of interest, and (2) the symbolic variable you would like to factor with respect to. Let’s consider the following simple example: x 2 − 2x − 8 = (x + 2) (x − 4) >> f = x^2-2*x-8; >> fs = factor(f,x) fs = [ x + 2, x - 4] Notice that our factors are provided as a vector. Similar to factor, Matlab can also simplify your expression by collecting coefficients of powers of a variable of interest using the collect function. Here’s a simple example provided by Matlab: >> syms x y >> coeffs_x = collect(x^2*y + y*x - x^2 - 2*x,x) coeffs_x = (y - 1)*x^2 + (y - 2)*x >> coeffs_y = collect(x^2*y + y*x - x^2 - 2*x,y) coeffs_y = (x^2 + x)*y - x^2 - 2*x The last “trick” I will mention is that Matlab can intelligently simplify an expression of interest. Please have a look at the Matlab documentation for simplify. It includes some interesting examples, including the ability to work with units. Yes, units! Here are a couple of examples to give you an idea: >> m = (x^2 + 5*x + 6)/(x + 2); >> ms = simplify(m) ms = x + 3 >> n = cos(x)^(2) + sin(x)^(2); >> ns = simplify(n) ns = 1 15.7 Solving systems of equations We have seen how we can use solve to solve a single equation for a single unknown. This would be the same application as we we had for fzero and roots when solving numerically. It should come as no surprise that solve can also handle systems of linear and non-linear equations too, where we had used fsolve 521 522 Chapter 15 An Introduction to Symbolic Calculations and rref previously. To see this in practice, let’s revisit our first example from Chapter 8: y = −x − 3 x 2 + y 2 = 17 >> syms x y >> eq1 = y == -x-3; >> eq2 = x^2 + y^2 == 17; >> [solx,soly] = solve([eq1,eq2],[x,y]) solx = 1 -4 soly = -4 1 And just like that we get both solutions in a single call, no initial guess necessary. Also, notice that we did not even need to re-arrange our equations into the form of an error function. Nice! Again, so far we have looked at rather “simple” problems using our symbolic solver. Try to solve the following system of equations which you encountered on Exam 2: ¡ ¢ ¡ ¢ sin x + y − cos y = 0.17 ¡ ¢ cos x − y + sin (x) = 1.8 15.8 Initial Value Ordinary Differential Equations Solving initial value ordinary differential equations (ODEs) can be tough. If I have a simple first order ODE such as: df =af dt No problem, the ODE is separable and I can readily separate and integrate. Beyond the separable case, if you are lucky, you can recast the ODE into its general form, and then look-up the general solution. Fortunately for us, MATLAB can symbolically solve ODEs too! To demonstrate, I will resolve some of our previous examples we solved numericall. Let’s consider first the example just mentioned. I’ll start by clearing variables and then finding the general solution: >> clear variables >> syms f(t) a >> ode = diff(f,t) == a*f ode(t) = diff(f(t), t) == a*f(t) 15.8 Initial Value Ordinary Differential Equations >> fsol(t) = dsolve(ode) fsol(t) = C3*exp(a*t) First, note that with the syms command I have f(t). This creates the symbolic function f, and additionally the symbolic variabl t since since f is a function of t. Here I also have a, which is an arbitary parameter. The differential equation is solved with the function dsolve. If a was known, you could readily using subs to provide its value. To solve for the constant of integration, here C3, you could use solve followed by subs. Interestingly, for our first order ODE we need just know the value of our function at some t , not necessarily t = 0. But what if for example we were told: f (t = 0) = 100 >> syms C3 >> Ci = solve(fsol(0) == 100, C3) Ci = 100 >> fsol_final(t) = subs(fsol,C3,Ci) fsol_final(t) = 100*exp(a*t) Notice that I needed to first needed to indicate that the constant of integration, C3, was symbolic. We could just as well do all of this with a single dsolve call: >> clear variables >> syms f(t) a >> ode = diff(f,t) == a*f ode(t) = diff(f(t), t) == a*f(t) >> ic = f(0) == 100 ic = f(0) == 100 >> fsol(t) = dsolve(ode,ic) fsol(t) = 100*exp(a*t) Could we solve our rat problem? df ((t ) = a f (t ) [1 + sin (ωt )] dt Where a = 0.01, ω = 2π/365, and f (t = 0) = 2 >> >> >> >> >> clear variables syms f(t) a = 0.01; omega = 2*pi/365; ode = diff(f,t) == a*f*(1+sin(omega*t)) 523 524 Chapter 15 An Introduction to Symbolic Calculations ode(t) = diff(f(t), t) == (f(t)*(sin((2*pi*t)/365) + 1))/100 >> ic = f(0) == 2; >> sol(t) = dsolve(ode,ic) sol(t) = 2*exp(t/100 - (73*cos((2*pi*t)/365))/(40*pi))*exp(73/(40*pi)) Incredible! When we solved numerically, we found that at 365 days we had 76.9530 rats. Let’s see if we get the same value. >> sol365 = sol(365) sol365 = 2*exp(73/(40*pi))*exp(73/20 - 73/(40*pi)) >> sol365_numeric = double(sol(365)) sol365_numeric = 76.9493 Amazing. We can also use dsolve to solve higher-order ODEs. Remember when we solved higher-order ODEs numerically, we first thad to re-write the equation as a series of first order ODEs. That is not necessary here. Let’s consider our free fall example. d 2z (t ) = z 00 (t ) = −g d x2 where z (t = 0) = z 0 and z 0 (t = 0) = v 0 . >> clear variables >> syms z(t) g z0 v0 >> ode = diff(z,t,2) == -g ode(t) = diff(z(t), t, t) == -g >> ic1 = z(0) == z0; >> Dz = diff(z,t); >> ic2 = Dz(0) == v0; >> zsol(t) = dsolve(ode,[ic1,ic2]) zsol(t) = z0 + t*v0 - (g*t^2)/2 Fantastic! Note that for the second initial condition, z 0 (t = 0) = v 0 , we first needed to create a symbolic function for the differential of z with respect to t , so then we could evaluate it at t = 0. If you also wanted velocity, we could get it two ways. First, we could just differentiate our solution for z (t ). >> vsol = diff(zsol,t) vsol(t) = v0 - g*t The alternative is we could re-write our second-order ODE as a system of firstorder ODEs as we did when we solved numerically, and then solve. z 0 (t ) = v (t ) v 0 (t ) = −g 15.9 Boundary Value Problems 525 where z (t = 0) = z 0 and v (t = 0) = v 0 . >> clear variables >> syms z(t) v(t) z0 v0 g >> ode1 = diff(z,t) == v ode1(t) = diff(z(t), t) == v(t) >> ode2 = diff(v,t) == -g ode2(t) = diff(v(t), t) == -g >> ic1 = z(0) == z0; >> ic2 = v(0) == v0; >> [solv(t),solz(t)] = dsolve([ode1,ode2],[ic1,ic2]) solv(t) = v0 - g*t solz(t) = z0 + (g*t^2)/2 + t*(v0 - g*t) A perfect match! 15.9 Boundary Value Problems When I taught mass transfer, boundary value problems were very common. Unfortunately, they are not always easy to solve. When we solved ODEs numerically, we only considered initial value ODEs. This is partially because solving boundary value problems numerically is challenging. Let’s consider the very simple example for the case of the diffusion of dilute A through a thin film of B of thickness l , where the concentration of A is known at x = 0 and x = l . d 2C A =0 d x2 C A (x = 0) = C A,0 C A (x = l ) = C A,l How would you go about solving this numerically? Think in terms of Eulers method. We would first need to introduce an additional variable so that we could re-write our ODE as a system of two first-order ODEs. dC A dN =N =0 dx dx Thinking in terms of Eulers method, to solve we would need to know the value of both C A and N at x = 0. We know the value of C A at x = 0, but not N . To solve, we can apply what’s called the shooting method. Essentially, we solve our system of ODEs for various values of N (x = 0) until we find the value such that we satisfy the requirement C A (x = l ) = C A,l . Fancier methods exist, and MATLAB has the functions bvp4c and bvp5c that you can use. However, these are iterative schemes, so success may be dependent on the quality of your initial guess of the solution. 526 Chapter 15 An Introduction to Symbolic Calculations Here we will see how we can readily solve boundary value problems symbolic, exactly as we solved initial value ODEs. For our diffusion example: >> clear variables >> syms cA(x) l cA0 cAl >> ode = diff(cA,x,2) == 0; >> b0 = cA(0) == cA0; >> bl = cA(l) == cAl; >> solC(x) = dsolve(ode,[b0,bl]) solC(x) = cA0 - (x*(cA0 - cAl))/l This is the analytic solution we would have obtained if we have solved by hand. We can solve harder cases too. Let’s again consider the case of the diffusion of dilute A through a thin film of B , but now let’s assume that A undergoes a homogeneous (bulk volumetric) reaction, where we will assume the reaction is first order in A, r A = −kC A . This leads to the following at steady state: k d 2C A − ◦ CA = 0 d x2 D AB C A (x = 0) = C A,0 C A (x = l ) = C A,l where D ◦AB is the diffusion coefficient of dilute A in B . >> >> >> >> >> >> clear variables syms cA(x) l cA0 cAl k DAB ode = diff(cA,x,2) - (k/DAB)*cA == 0; b0 = cA(0) == cA0; bl = cA(l) == cAl; solC(x) = dsolve(ode,[b0,bl]) solC(x) = (exp((x*(DAB*k)^(1/2))/DAB)*(cAl - cA0*exp(-(l*(DAB*k)^(1/2))/DAB)) (exp((l*(DAB*k)^(1/2))/DAB) - exp(-(l*(DAB*k)^(1/2))/DAB)) (exp(-(x*(DAB*k)^(1/2))/DAB)*(cAl - cA0*exp((l*(DAB*k)^(1/2))/DAB)))/ (exp((l*(DAB*k)^(1/2))/DAB) - exp(-(l*(DAB*k)^(1/2))/DAB)) This is great and correct, but it would really be nice if we could simplify this expression at all. If we apply simplify here, the solution does not change. The issue is we have terms of (DAB*k)ˆ (1/2). Remember that the square root of a negative number is imaginary. To make sure we are keeping it real, we can use assume where we know that both quantities are positive. >> assume(k>0) >> assume(DAB>0) >> solC(x) = simplify(solC) solC(x) = -(cA0*exp((2*k^(1/2)*l)/DAB^(1/2)) - cAl*exp((k^(1/2)*l)/DAB^(1/2) cAl*exp((k^(1/2)*(l + 2*x))/DAB^(1/2)))/(exp((k^(1/2)*x)/DAB^(1/2)) - exp((k^ 15.9 Boundary Value Problems That’s a big improvement! To shorten it up, let’s introduce α = k/D ◦AB . Note to get it to work here, I need to use α = k 1/2 /D 1/2 . AB >> solC = subs(solC, k^(1/2)/DAB^(1/2), alpha) solC(x) = -(cA0*exp(2*alpha*l) - cAl*exp(alpha*l) - cA0*exp(2*alpha*x) + cAl*exp(alpha*(l + 2*x)))/(exp(alpha*x) - exp(alpha*(2*l + x))) When solving by hand I could further simplify the equation and take advantage of the relation that ¢ 1¡ sinh (z) = e z − e −z 2 But try as I might with Matlab, I could not get it to simplify further. I was finally able to get it by telling Matlab that x and l are both greater than 0, and also using an additional agrument with simplify to increase the number of simplification steps used. >> assume(l>0) >> assume(x>0) >> solC(x) = simplify(solC,'Steps',100) solC(x) = (cAl*sinh(alpha*x) + cA0*sinh(alpha*l - alpha*x))/sinh(alpha*l) Beautiful! 527 Appendix A while loops A.1 Loops: what we’ve done so far Before discussing while loops, let us first review what we have done so far with for loops. The general form of a for loop is: for i=n0:n statement end We enter the loop with i=n0, perform the statement, and then check to see if i==n. If yes, we are done, exit the loop. If no, increment i by 1 and repeat. If we wish to increment by a value other than 1, we can using the extended colon operator: for i=n0:dn:n statement end where dn corresponds to the desired increment value. We have also seen that we can exit a loop before the specified number of iterations using the break statement. In Section 5.12 we were introduced to the continue statement. When the continue statement is encountered, we jump immediately to the next loop iteration. A.2 while loops Everything that we can do with a for loop we can also do with a while loop, and with both we can use the break and continue statements. It is for this reason that the while loop was not introduced sooner. First I wanted you to get comfortable 529 Chapter A while loops 530 with the for loop. Once you have mastered the for, there in principle is no need for the while. Choosing one over the other is a matter of programming style. When I was an undergraduate student my research advisor told me never to use for loops, that while loops were much better. As a graduate student, the philosophy of my group was the opposite, for loops were preferred over while loops. So in addition to your own preferred programming style, knowing both is good because a future collaborator may have a different style than you. After all that talk, let’s look at the structure of a while loop and a few examples we have already encountered. The basic structure of a while loop is: while <conditional statement is true> statement end This is best seen with an example. Let’s start by writing a function with a for loop to sum the elements of a vector. Listing A.1 sum_for.m 1 2 3 4 5 6 7 function res = sum_for(X) c = 0; for i=1:length(X) c = c + X(i); end res = c; end We can write an equivalent function using a while statement as: A.2 while loops 531 Listing A.2 sum_while.m 1 2 3 4 5 6 7 8 9 function res = sum_while(X) c = 0; i = 0; while i < length(X) i=i+1; c=c+X(i); end res = c; end As compared to using a for loop, here we update our own index variable each iteration. Notice that my conditional statement is i < length(X). You would think it should be i <= length(X). But that would result in an error. MATLAB will only check the conditional statement at the start of a new iteration, before we update our index variable. How my undergraduate advisor used to use while loops which did provide some additional flexibility was to use a “flag” variable. Before the loop, the flag variable is initialized with some value, say zero. Then when you want to stop/exit the loop, you just change the value of flag to a different value. This gives flexibility when there are multiple different reasons to exit a loop without needing to remember the break command. Here is how we could use a flag variable in our summation example: Listing A.3 sum_flag.m 1 2 3 4 5 6 7 8 9 10 11 12 13 function res = sum_flag(X) c=0; i=0; flag = 0; while flag == 0 i=i+1; c=c+X(i); if i == length(X) flag = 1; end end res = c; end All three functions yield identical results: t Remember, the while statement executes while the conditional statement is true. You could therefore just assign flag a value of one initially, flag = 1. Since to MATLAB a value of 1 means true, your conditional statement could be just flag. Then when you want to exit, assign to flag a value of zero, false. To me, explicitly having a logical statement of flag == 1, while using more characters, makes my code easier to understand. And if it is easier to understand, I am less likely to make a mistake. Chapter A while loops 532 >> Y = 1:6; >> sum_for(Y) ans = 21 >> sum_while(Y) ans = 21 >> sum_flag(Y) ans = 21 All three loops and methods are identical. I encourage you to try them all and decide which you prefer. Develop your own style. spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp spsp