The Mad Engineer Blog
Elyn Saks is a hero of mine. Ever since I watched her TED talk, I had to know more about her. She is an esteemed professor of law at University of Southern California. She holds an impressive slate of degrees from Vanderbilt, Oxford, and Yale. She also happens to have schizophrenia.
In her memoir, The Center Cannot Hold (2007), Elyn recounts her journey with mental illness (and its treatment) while staying on a tenuous path toward academic success. She shares deeply personal experiences including her symptoms of psychosis and the details of her numerous psychiatric hospitalizations, only to arrive at a regimen that works for her in the end. The book gives insight into an illness that confounds doctors, while giving hope to others who may experience symptoms of serious mental illness.
What I Loved about the Book
Overall, I thought this was an excellent book on all accounts. It was entertaining, heartbreaking, thought provoking, and it cast mental illness in a positive yet realistic light. This is a must-read for anyone in the mental health profession as well as anyone who is passionate about mental health.
The single most important thing to starting a research project is knowing what has already been done by others. Yet students tend to shy away from doing a literature review. Granted, reviewing the literature can be a daunting task. There are hundreds if not thousands of papers written on a topic. Where do you begin?
In this post, I will show how to use databases to search for references, organize references in a reference manager like EndNote, sort through titles and abstracts to find relevant papers, and search papers for additional references to add to your personal library. Keep in mind that a literature review is an ongoing task. You should always be on the lookout for new papers that aren't already in your library.
If we know the names of journals that publish papers on the topic we are researching, we can search those journals directly. But we may not know which journals are most relevant if we are working in a new field. In addition, if we only search specific journals, we might overlook important papers that were published elsewhere.
So we use databases to search large numbers of journals. Students like to default to Google Scholar because it is familiar, but there are much better databases for academic research and to access them you need to go through your library.
For engineering, I like ProQuest because it covers a lot of territory, including the Elsevier and Springer journals. Depending on what I'm researching, I may also search the ASCE database. This is because ASCE has its own library and won't turn up in other databases. To figure out which databases cover a field, you can ask your librarian or search the library's website.
To access the ProQuest database, I go to lib.umich.edu and search "proquest." ProQuest shows up under databases. I click the link to access ProQuest. I am prompted to enter my university credentials.
Once you have settled on a database (or databases), you need to figure out your keywords. The broader your search terms, the more results will be returned in your search. If your search is too narrow, you will miss a lot of important and relevant literature. I find choosing keywords to be an iterative process.
The other thing you should think about is where those keywords appear. In most cases, you can limit your search to keywords appearing in the title and abstract, for example. If you search anywhere for the keywords, you will get papers that just have a keyword appear once in the whole article.
Let's search for papers related to wildfire policy that were published in the last ten years. If I enter "wildfire AND policy" in the database for a period covering the last ten years (i.e., "PD(2011-2022)"), I get more than 100,000 results. The search is too broad and probably returns a lot of irrelevant results.
We can limit the results to peer-reviewed articles, which brings the number down to 11,000. Peer-reviewed means that the papers were rigorously evaluated by the authors' peers in the publishing process. Peer-reviewed papers carry a lot more weight than those not subject to peer review because the findings were presumably scrutinized very closely prior to publication.
Note that these results are searching anywhere in the document for the keywords. We can narrow our search even further by requiring the keywords to appear in the title or abstract by searching the following: "AB,TI(wildfire AND policy)". This returns a little more than a thousand papers, which is a lot more manageable.
Note that our search isn't necessarily done. We may need to consider alternative keywords. For our search on wildfire policy, we may need to also search for "wildland fire," “forest fire,” “brush fire,” and "bushfire" to accurately capture the whole field. In addition, we can't forget that there are other databases we may want to consider. In this case, we might want to consider a database like JSTOR that searches social science journals in addition to ProQuest.
Exporting Search Results
You can usually save your results in ProQuest or in other databases, but I prefer to have everything in one location. This is where having a reference management system comes into play. I use EndNote, but others may prefer Mendeley or Zotero due to cost savings and other features. Regardless of which software you use, you can usually export the results from a database search into your reference manager.
To export from ProQuest, you check the box next to the papers you want to export. Note that ProQuest will only allow you to export 500 references at a time. Once you've selected the papers, you click the button with the ellipse (three dots) for All Save Options. A window will appear that will allow you to save the results as an RIS file (i.e., an EndNote compatible file). You can then import the file into EndNote by double-clicking it. After importing, you have the bibliographic information, abstract, and hyperlink to each paper you have downloaded.
I like to place references in groups to keep them organized. For now, I place all the references I’ve downloaded into the Policy group. As I read them, I will create groups with more focused topics.
I also recommend going to Library → Find Duplicates to remove duplicate records. After deleting duplicate records in this case, the number of unique references drops to seven hundred.
Sorting through the Results
The only way to know what the papers are about is to read. Generally, I do a quick scan of titles and abstracts to see if the papers are relevant and to put them into groups with more focused topics. Papers that are highly interesting will be flagged to read in depth, whereas papers that are unrelated will be flagged as “do not read." An example of an irrelevant paper is shown below. From the title and abstract, you can clearly see that the paper has nothing to do with wildfires even though the words wildfire and policy appear in the abstract. I do not delete the record because it is possible to come up in another search later, and I don’t want to triage it again. Instead, I keep it in the "do not read" pile and delete any duplicates in the future.
Now it's time to sort through the seven hundred papers I've found. I'll share with you a tip that I use to organize the literature graphically, which is something I found necessary when I did the literature review for my Ph.D. dissertation. When organizing the literature, I create a concept map of the main topics. The concept map is dynamic until I've finished reading, meaning that I am constantly adding and rearranging topics until they make sense to me. A concept map allows me to visually organize the information so I can see the main topics and their relation to one another. Below is a concept map I created based on review papers I have recently read related to wildfires. I used a free web tool called Bubbl.us to create this one. It helps me to visualize all the information I take in during a literature review of a new topic and to identify gaps in the literature.
Reading with Openness
Your initial search will cover a lot of the literature, but you should always be on the lookout for papers to add to your library. Once you begin reading the journal papers, you should always scan the reference list for relevant papers. If a relevant paper is not already in your library, add it and flag it to be read. I call this reading with "openness" because the approach is inspired by qualitative methods of data analysis. This is a practice that should follow you your entire academic career.
A literature review is hard work. It is easy to get overwhelmed, but if you follow a systematic approach and keep everything organized in a reference manager, it is a lot more manageable. Keep in mind that your search should be broad but within reason. You may need to choose more than one database, and you may need to search multiple combinations of keywords. Downloading the references to your library is only half the battle. You will eventually need to read through the papers to get a true understanding of what the papers are about. In this article, we skimmed titles and abstracts, but the next step is to read the papers “in full.” I put “in full” in quotes because a lot of information can be gleaned from a paper by skimming. At the end of the day, your research will be better because it will be based on the current state of the art.
In my last blog entry, I showed how we can use object-oriented programming (OOP) to set up an analysis of a truss. My journey into OOP all started a few weeks ago when my eleven-year-old had to do a project for school and wanted to learn how to code video games. Simple enough, I thought.
A Google search led me to an app called Codea that allows you to code simple 2D video games on an iPad for a mere $15. It comes with some sample games already coded for you. This seemed like a good route for us to take because it was simple and low cost. Codea is in the language of Lua. There isn't much support out there, but I was able to find some useful tutorials on the web. I decided start simple by creating a jazzed up version of 1-player pong.
The idea of Color Pong is that the ball bounces off the left, top, and right sides of the screen, and the player controls a paddle at the bottom. The objective is to keep the ball from crossing the bottom edge of the screen (like the original Pong game). To add some flair to the game, I wanted the ball to change color everytime it hits a surface. I also threw in a sprite of a cat that my five year old helped draw.
From a programming standpoint, the game has two objects: the ball and the paddle, which would give me some experience with OOP. The screen is divided into pixels, and the origin is located at the bottom-left corner of the screen. Global variables WIDTH and HEIGHT give the dimensions of the screen, independent of the resolution on the device. We'll see how to use these in a bit.
The ball is a circle that is characterized by its diameter (or size), its x and y coordinates, and its velocity, which has x and y components. The units on these variables are in pixels. We also attach the score to the ball so that points are added when the ball hits the paddle. We create a class and initialize instances using the following code.
There are several functions related to the ball that we want to define in the class. The first defines the random color assigned to the ball when it hits a surface:
Next we define three functions that describe what to do when the ball hits the left (or west), top (or north), and right (or east) walls. I call these bounceW, bounceN, and bounceE, respectively. For bounceW, for example, we say that when the x-coordinate of the ball minus its radius is less than or equal to zero, the ball bounces.
Note that I am using "if (self.x - self.size/2) <= 0" instead of "if (self.x - self.size) == 0" because the ball travels in frames and the speed can be an odd number that does not neatly divide into WIDTH. I use this approach throughout the code.
Bouncing on the left wall means that the y-component of velocity stays the same but the x-component shifts directions. Thus, self.xSpeed = -self.xSpeed. Upon bouncing, the color changes (i.e., we call on self:color()).
We do something similar for the top and right walls. The code is:
Similarly, we define a function bounceS that takes the position and dimensions of the paddle and determines when ball comes in contact with the paddle. When it does, it changes direction, it changes color, and scores points. The code is as follows:
Lastly, the draw function draws an ellipse with diameter equal to self.size at coordinates self.x and self.y. The ellipse is filled with the random RGB values determined above.
The paddle is defined by its x- and y-coordinates, its width and its thickness. The y-coordinate and the width and thickness of the paddle are fixed. The x-coordinate of the paddle follows the touch measured by the iPad screen. I will explain this below. To create the class and initialize the instance, we use the following:
The only function is the draw function, which draws a rectangle at the position and with the dimensions of the paddle. The paddle is shaded yellow.
The Main Program
In the main program we need to define two functions: setup and draw. The setup function is where we define the initial inputs to the code. In our case, we will initialize the ball and paddle instances by specifying their dimensions, initial coordinates, etc. In the following, we set ball.size = 100, ball.x and ball.y so the ball is at the top-center of the screen, ball.xSpeed and ball.ySpeed to be random numbers between 3 and 8, RGB each to 256 so the ball is white, and ball.score = 0. For the paddle, we position it at the bottom-center, and we give it paddle.width = 250 and paddle.thickness = 20. We also initialize a variable called taps.
The draw function refreshes every frame. Objects are drawn in the order they appear in the draw function. Therefore, we draw the background first; then we place the ball and paddle on the screen; then we draw the sprite on the ball. Doing it in this order means the sprite will be in front of the ball, and the ball and paddle will be in front of the background.
The ball is drawn such that it doesn’t move until the screen is tapped. We do this using the variable CurrentTouch.tapCount, which measures every time the screen is touch. Once touched, then we update the x and y coordinates of the ball based on the assigned speed. To start the ball on touch, we use the following:
We also use the touch screen to control the paddle. We say that the x coordinate of the paddle matches the x coordinate of the touch while the touch is moving on the screen. In code, we have:
Then, we call on the bounce functions for the ball instance and we draw the ball and paddle. For the finishing touch, we draw the cat sprite. The complete code for the main program is as follows:
Putting it all together
Running the code in Codea, we can actually play the game. Below is a sample video of the game in action.
You can see that the game runs smoothly and does everything as it is expected to.
This was a fun little exploration into coding video games. It was a practical application to OOP that allowed me to teach my child some things about coding, like the RGB color palette, x and y coordinates, and the basic structure of an OOP code. My child now wants me to sell the game in the App Store, lol!
I'm a big fan of coding, but I get the sense that my college education may have failed me. Don't get me wrong--I'm proud of my degrees from Pitt and Virginia Tech, and my education was more than enough to propel me into an academic career at the University of Michigan. But the state of the art for computer analysis in civil engineering in the early 2000s was the brute force structure of procedural languages like FORTRAN. Object-oriented programming (OOP) was a topic left to the computer scientists.
Twenty years later, I still follow the old habits that were drilled into my head as an undergrad. However, I've been working more and more in modern languages like Python due to courses I teach on computational analysis. Python is an open source language with countless libraries that can be downloaded seamlessly. It is a powerful tool that can do so much more than the languages from the days of yore. Python's capacity for OOP has always intrigued me, but I wasn't prompted to teach myself OOP until my eleven year old asked me to help program a video game (more on that later).
I'm sharing what I learned so other engineers (especially older folks like me) are encouraged to realize the power behind OOP. To follow along, you need Python 3. I recommend downloading Anaconda for scientific computing in general, and Jupyter Notebook is the quickest way to get started with coding in Python.
In OOP, the properties and functions attributed to an instance are based on classes. Classes are defined in Python according to the following convention:
The first line defines a class with the name "ClassName." Note that the first letter of the class name is capitalized per convention. The function __init__(self, x1, x2, ..., xN) initializes an instance of the class with properties x1, x2, ..., xN. Every instance is characterized by its name ("self") and its properties.
Suppose we want to create a class TrussElements to do linear analysis of trusses. The properties that we associate with a truss element are the x and y coordinates of its two nodes, the cross-sectional area, and Young's modulus. The class definition would be as follows:
Creating Instances of Objects
Once the class is defined, we then create instances of truss elements by calling on the class. Consider the truss in the figure below. Member AB has an area of 2.5 in.^2 and member BC has an area of 1 in.^2. Both members have Young's modulus of 30,000 ksi. A force of 5 k is applied at joint B, and we might be interested in calculating the displacements at the nodes, the reaction forces at the supports, and the axial force in each member of the truss (we won't do all that here, but we will set the problem up).
We create instances of truss elements by calling on TrussElements with the appropriate values for x1, y1, x2, y2, area, and E. Note that we do not need to pass "self" into the class because it is implied by the name we give the instance. For our two bars, AB and BC, we have:
Now that we have created the instances, we can call on their properties. For example, if we want to print the area of element AB, we can do that. If we want to print Young's modulus of element BC, we would do the following:
This returns the values 2.5 and 30000. You can see that it is now convenient for us to call on the instance and its properties using the "." notation.
In addition to ascribing attributes to classes, we can also ascribe methods (or functions) to classes. In the example of the truss, we may want to define a function to compute the length of the truss based on the coordinates of the end nodes. In the class definition, we will add a function Length that does this. Note that we need to import the math library in this case to use the square root function.
Now, calling on the Length function under each instance reports the length of the element:
Here we see the length of AB to be 60.0 and the length of BC to be 36.0. Note that the Length function does not have any arguments because the nodal coordinates are already a part of the class definition.
Adding Output Statements to the Class
So far, we have used print statements outside of the class definition to print output. It may be convenient to format output to be the same for all instances of a class. This can be done using functions, but one particular function that may be of interest is the __str__() function, which modifies what appears when you print(Instance).
Consider our TrussElements class. We add the __str__() function to print as follows:
Now when we print the instance, e.g., BC,
We obtain the following output: "Element goes from (48,36) to (48,0). It has A = 1 and E = 30000"
Arrays of Classes
One final trick is that we can create arrays of classes. In the analysis of trusses, for example, we often loop over the elements in the structure and use an index to indicate the element under consideration. From the previous example, suppose we number AB as element 1 and BC as element 2. Joint A is node 1, joint B is node 2, and joint C is node 3. We will store coordinate information in a "coords" array.
To use arrays, we must call upon the numpy library.
Since we will loop over all elements (nels = 2), it is also convenient to store the areas in an array. We can also use a "connectivity" array to store the inode and jnode for each element. The connectivity array is as follows:
In Python, the input looks as follows:
We initialize the truss array and enter a loop over the elements. For each element, we extract the nodal coordinates, area, and Young's modulus, and enter them into the TrussElement class definition. We append the information for element i to the truss array.
We can then print whatever output we need. For example, the following prints the length, area, and E for each element.
In this blog, we studied basic concepts of classes, instances, methods, and arrays as they apply to OOP. We looked at how these concepts can be applied to structural analysis by considering how data is stored for computational analysis of trusses. We have just scratched the surface of OOP, but hopefully you are inspired to learn more.