Dan Ivanovich Cybersecurity Engineer https://ivanovi.ch/ Fri, 08 Aug 2025 15:58:46 +0000 Fri, 08 Aug 2025 15:58:46 +0000 Jekyll v3.10.0 Introducing the EA Dashboard! <h3 id="what-is-the-ea-dashboard">What is the EA Dashboard?</h3> <p>Throughout this year, I have been working on a project for Energize Andover. I call it the “<strong>Energize Andover Dashboard</strong>,” or “EA Dashboard” for short. It is a web application that allows its users to customize a dashboard that displays various types of town data.</p> <p>On the front end, the dashboard is composed of two essential components: the layout and the widgets. Widgets are unique graphs (or other forms of displaying data) for a specific subset of town data, such as a line graph of the total electricity used by Andover High School at the end of every day in the past week. Although I intend on creating many types of widgets, the majority will be similar to the previous example: interactive charts that update with new data. The layout, on the other hand is the arrangement and sizes of these widgets on the dashboard. Widgets have different aspect ratios (some are square, some are rectangular, etc.) and thus the layout will determine what widgets are displayed, and where on the dashboard those widgets lie.</p> <h3 id="application-structure">Application Structure</h3> <p>I have done my best to design the application so that the server sustains the lightest loads possible, generating the graphs, layout, and more in the client’s browser and minimizing interactions between the clients and the server as much as possible.</p> <p>On the server side, a Flask application hosts a web server. This server retrieves data from our BACnet API using the requests package, but this data-retrieval process will eventually be replaced by one that uses MySQL to query the server that Ayush is currently building.</p> <p>This server takes the data it gathers and makes it accessible by the clients and also generates a configuration JSON file that details the requirements of each widget that may appear on the dashboard (such as the “aspect ratio” of the grid element’s size, what subset of the data it will be using, the chart type and option overrides, etc.). Thus, each client’s browser handles much of the computation and rendering of the data. Bulma, a CSS framework, is used to style the site and create much of the layout. FontAwesome icons are used throughout the site to make the UI more intuitive, and jQuery is used for most of the JavaScript functionality and interactivity. Gridster.js, along with lots of my own code that I wrote to correct its flaws (the library is no longer being developed), is used to create an intuitive layout configuration system that involves draggable, stretchable rectangles on a 4x4 grid. This layout is stored in the client’s browser-specific LocalStorage, to avoid the need for a separate database for this project, along with which widgets are to be populated in which of the grid’s boxes. This information is later pulled from the LocalStorage and is used to generate the containers and layout for the dashboard’s widgets on the main page. The JavaScript libraries are then used to pull the data, pull the requirements for each widget, and gather all the information needed to generate a widget. Finally, Plotly.js is used to generate clean, interactive charts for the widgets in each grid element. Overall, this structure was designed to reduce the load on the server and to be fully functional without a server-side database to store configuration information or the expiration of cookie-based data storage. <br /><br /> <img src="/images/blog post images/EA Dashboard/Dashboard_High_Level_Architecture.png" alt="Dashboard High Level Architecture Chart" class="centered-image" /></p> <center>A diagram showing the structure of the EA Dashboard</center> <p><br /></p> <h3 id="the-present">The Present</h3> <p>So far, I have done much of the front-end work for the dashboard. I have built a full layout customization system by implementing gridster.js and writing a good amount of custom JavaScript to fix its flaws and mold it to my purposes. I have set up the integration with LocalStorage, designed a clean and intuitive interface, and have had the layout apply to the dashboard page. I have also configured and set up Plotly.js with some test widgets and experimented with various layouts to test the connection between layouts and widgets. I have also set up most of the data collection process for the server, but I have not made a connection between the server’s data and the front-end Plotly.js.</p> <h3 id="the-future">The Future</h3> <p>I will do my best to finish this project by the end of the summer. The next step for me is to complete the data-collecting code on the server side and run thorough testing. When that is complete, I will work on connecting the data from the backend to Plotly.js.</p> Sat, 20 Jun 2020 00:00:00 +0000 https://ivanovi.ch/blog/2020/06/introducing-ea-dashboard/ https://ivanovi.ch/blog/2020/06/introducing-ea-dashboard/ blog posts EA Dashboard Scripting Python Package Maintenance <p>Python’s accessible and vast library of user-made packages is arguably one of its most powerful features. When developing a project with Python, it is often not long before you find your project dependent on a large list of libraries, and specific versions of those libraries, at that. Communicating such a long list of libraries to potential users of your own code base (or even a Python package of your own) thus could have been an arduous process, were it not for <a href="https://www.idkrtm.com/what-is-the-python-requirements-txt/">requirements.txt</a>. By generating a <code class="language-plaintext highlighter-rouge">requirements.txt</code> file and distributing it with your project, you can make it far, far easier for other users or developers to install the correct versions of the packages required for your code to execute properly. In this guide, I’ll break down how to generate a <code class="language-plaintext highlighter-rouge">requirements.txt</code> and share two particularly useful scripts to automate working with them.</p> <h2 id="generating-requirementstxt">Generating <code class="language-plaintext highlighter-rouge">requirements.txt</code></h2> <p>To generate a <code class="language-plaintext highlighter-rouge">requirements.txt</code> file for your project, open your project’s directory in the terminal. Ensure that you have installed the currently-required packages in your current Python environment (virtual environment, system-wide installation, or otherwise) using <a href="https://pip.pypa.io/en/stable/">pip</a>. Then, simply execute the following command:</p> <p><code class="language-plaintext highlighter-rouge">pip freeze &gt; requirements.txt</code></p> <p>Here, you could replace <code class="language-plaintext highlighter-rouge">requirements.txt</code> with the path to the file you want to generate, but conventionally the generated file is named <code class="language-plaintext highlighter-rouge">requirements.txt</code> and placed within the root directory of the project. If you open the file, you should find something in this format:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cycler==0.10.0 # via matplotlib kiwisolver==1.2.0 # via matplotlib matplotlib==3.2.1 numpy==1.18.5 pandas==1.0.4 pyparsing==2.4.7 # via matplotlib python-dateutil==2.8.1 # via matplotlib, pandas pytz==2020.1 # via pandas scipy==1.4.1 # via seaborn seaborn==0.10.1 six==1.15.0 # via cycler, python-dateutil </code></pre></div></div> <p>Note how the specific versions of the packages are listed, but keep in mind that version numbers could also be listed using <code class="language-plaintext highlighter-rouge">&gt;=</code>, <code class="language-plaintext highlighter-rouge">&lt;=</code>, and other operators.</p> <h2 id="installing-all-required-packages">Installing All Required Packages</h2> <p>To install the specified versions of all required packages, once again open your project’s directory in the terminal and ensure that you have the proper Python environment opened. Then, run the following command:</p> <p><code class="language-plaintext highlighter-rouge">pip install -r requirements.txt</code></p> <p>This will install the proper version of all packages listed in <code class="language-plaintext highlighter-rouge">requirements.txt</code>, all in one command. Isn’t that convenient?</p> <h2 id="updating-packages">Updating Packages</h2> <p>Python packages are updated quite frequently. As anyone who frequently uses <code class="language-plaintext highlighter-rouge">requirements.txt</code> on GitHub can tell you, frequent updates are often required to fix security flaws in many widely-used packages. However, as the version of each package is explicitly defined in <code class="language-plaintext highlighter-rouge">requirements.txt</code>, updating is a multi-step process.</p> <p>To update all of your project’s packages, open your project’s directory in the terminal and ensure that you are inside the proper Python environment. Then, execute the following command:</p> <p><code class="language-plaintext highlighter-rouge">pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip install -U</code></p> <p>This will fetch and install any available updates to the project’s packeges, along with any new package dependencies. However, you may notice that <code class="language-plaintext highlighter-rouge">requirements.txt</code> remained untouched. Double check that your code still executes properly and, if it does, regenerate the requirements file by repeating the command from above:</p> <p><code class="language-plaintext highlighter-rouge">pip freeze &gt; requirements.txt</code></p> <p>You should now be able to see the new version numbers reflected in the file.</p> <h2 id="automating-these-processes">Automating These Processes</h2> <p>You can use the scripts that I have published to <strong><a href="https://github.com/Energize-Andover/Maintenance-Scripts">this GitHub repository</a></strong> to simplify the processes of installing packages from <code class="language-plaintext highlighter-rouge">requirements.txt</code> and updating your packages. The following shell scripts are included:</p> <center><strong>install_packages.sh</strong></center> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#! /bin/bash</span> pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt </code></pre></div></div> <center><strong>Usage: </strong><code class="highlighter-rouge">./install_packages.sh</code> - output of installation from <code class="highlighter-rouge">requirements.txt</code> will be shown.</center> <center><strong>update_all_packages.sh</strong></center> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#! /bin/bash</span> pip list <span class="nt">--outdated</span> <span class="nt">--format</span><span class="o">=</span>freeze | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s1">'^\-e'</span> | <span class="nb">cut</span> <span class="nt">-d</span> <span class="o">=</span> <span class="nt">-f</span> 1 | xargs <span class="nt">-n1</span> pip <span class="nb">install</span> <span class="nt">-U</span> <span class="c"># Prompt</span> <span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Would you like to update requirements.txt? (Y/N)"</span> <span class="nt">-n</span> 1 <span class="nt">-r</span> <span class="nb">echo</span> <span class="c"># move to new line</span> <span class="k">if</span> <span class="o">[[</span> <span class="o">!</span> <span class="nv">$REPLY</span> <span class="o">=</span>~ ^[Yy]<span class="nv">$ </span><span class="o">]]</span> <span class="k">then </span><span class="nb">exit </span>1 <span class="k">fi </span>pip freeze <span class="o">&gt;</span> requirements.txt </code></pre></div></div> <center><strong>Usage: </strong><code class="highlighter-rouge">./update_all_packages.sh</code> - enter Y or N after the updating has been completed if you would like to update the versions of your <code class="highlighter-rouge">requirements.txt</code> file.</center> <p><br /> Implementing these scripts into your workflow will greatly simplify your Python package updates and installs. I hope you find them as useful as I do!</p> Sat, 18 Jan 2020 00:00:00 +0000 https://ivanovi.ch/blog/2020/01/pthon-requirements-maintenance/ https://ivanovi.ch/blog/2020/01/pthon-requirements-maintenance/ blog posts miscellaneous OpenCV and AHS Heatmap | Autonomous Room Detection in Floor Plans With OpenCV (Part 3) <p><em>This is part 3 of a series of blog posts about the computer vision behind the AHS Heatmap. <a href="/blog/2019/09/how-heatmap-works/">Read part one here</a>; <a href="/blog/2019/10/how-heatmap-works-2/">Read part two here</a></em></p> <p>In this final part of this series, I will explain how OpenCV is used in combination with the files and data obtained from the steps described in parts one and two in order to generate the final heatmap.</p> <h2 id="processing-the-generated-png-with-opencv">Processing the Generated PNG With OpenCV</h2> <p>By analyzing the filename of the original SVG, the Python was able to detect the floor of the school that it is currently generating a map for. The first digit of room numbers in AHS indicate their floor (for example, room <strong>2</strong>03 is on the <strong>second</strong> floor). Thus, the program can use the floor number to filter the <a href="https://pandas.pydata.org/">pandas</a> dataframe associating the addresses of AHS’s sensors and their room numbers. The program then iterates through this cut-down dataset and compares it with the dataset of text boxes and coordinates <a href="/blog/2019/10/how-heatmap-works-2/">obtained in part two</a>. If a sensor’s room is not labeled on the floor plan (that is to say, if a room number from the sensor dataframe is not found in the PDF dataframe), the room is skipped. If not, the temperature or CO2 level of the sensor is pulled, a proper color between blue, green, and red is chosen, and the following process is performed to detect the shape of the room:</p> <h2 id="detecting-room-shapes">Detecting Room Shapes</h2> <p>To demonstrate this process, let us say that the program is <strong>currently looking for room 217</strong> in the following floor plan (listed as “level 3” because the field house, a part of the first floor, was separately considered as “level 1” by the floor plan’s creators):</p> <p><img src="/images/projects/AHS-Heatmap/level_3_plan.png" alt="Floor plan of the second floor" class="centered-image" /></p> <p>First the PNG of the floor plan is loaded into OpenCV. Then a series of image processing and computer vision techniques are employed to detect the shape of the room, as follows:</p> <p><img src="/images/projects/AHS-Heatmap/binarized.png" alt="Binarized (grayscale) form of the map shown above" class="centered-image" /></p> <p>First, the image is <strong>binarized</strong>. This means that it is converted from standard 3-channel RGB color codes, with 3 values between 0 and 255 defining a color, into an image with 1-channel color codes, dictated by a single value between 0 and 255. This results in what is more commonly known as a “gray-scale” effect, as 0 represents black, 255 represents white, and everything else determining a certain shade of intermediate gray. Binarization makes the detection of objects (in our case, rectangles) much easier in the future.</p> <p><img src="/images/projects/AHS-Heatmap/floodfill.png" alt="Flood filled binarized image" class="centered-image" id="floodfill-image" /></p> <p>Next, the coordinates of the room’s label’s text box, filtered out from the dataframe obtained by the PDF extraction process shown in part 2, are used. As it is known that the room labels, however they are placed, will be <em>within the room</em>, we can begin a <strong><a href="https://www.freecodecamp.org/news/flood-fill-algorithm-explained/">flood fill algorithm</a></strong> (think of the paint bucket feature from MS Paint) from one pixel outside the border of the textbox. This will now be guaranteed to result in the room surrounding the text being filled with a neutral gray, exactly between white and black to minimize its chances of being grouped in with the black and white pixels around it when the image is processed further.</p> <p><img src="/images/projects/AHS-Heatmap/floodfill_binarized.png" alt="Binarized form of the previous step's result. The image is now fully black and white." class="centered-image" /></p> <p>The flood filled image is now binarized again, converting the intermediate gray that filled the room with to white and all other colors to black (a 0-tolerance binarization). As you can see, this has isolated the shape of the room surrounding the text box and has made it much easier to spot the room in the image. However, the shape of the room number’s text still remains and posed a problem to future steps.</p> <p><img src="/images/projects/AHS-Heatmap/text_hole_sealed.png" alt="The previous image, only now with the 217-shaped hole sealed with white" class="centered-image" /></p> <p>Thus, using the coordinates, width, and height of the text box that were extracted from the PDF, a white rectangle is drawn over the former location of the text box using OpenCV. As you can see, this process fills in the hole and does so consistently without impacting the rest of the shape.</p> <p>Now, the shape of the room has been isolated very well. It is now possible to quickly detect the outline of the shape, known as the “contour” in OpenCV:</p> <p><img src="/images/projects/AHS-Heatmap/detected_contour.png" alt="The contour detected from the previous image, overlayed over the original floor plan." class="centered-image" /></p> <p>Here, I have displayed the contour (blue) that my OpenCV-based computer vision code detected over the original floor plan.</p> <p>Although I could have used the countours generated by this process to describe the shape of my rooms on the heatmap, I decided to opt for a consistent, rectangular style that matches the style of the rooms in the building and the floor plan. Thus, the bounding rectangle of the countour is calculated.</p> <p><img src="/images/projects/AHS-Heatmap/contour_bounding_box.png" alt="The same contour, only now displayed alongside its bounding box." class="centered-image" /></p> <p>Here, the bounding rectangle (green) of the contour (still blue) is shown.</p> <p><strong>This bounding box is returned by the function that performs all of this analysis as the detected shape of the room</strong>.</p> <h3 id="the-code">The Code</h3> <p>Here is the code that performs the OpenCV component of the process described (located in <code class="language-plaintext highlighter-rouge">openCV_tools.py</code>):</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_room_max_contour</span><span class="p">(</span><span class="n">room_text_coords</span><span class="p">):</span> <span class="n">bottom_left</span> <span class="o">=</span> <span class="p">(</span><span class="n">room_text_coords</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">room_text_coords</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">top_right</span> <span class="o">=</span> <span class="p">(</span><span class="n">room_text_coords</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="n">room_text_coords</span><span class="p">[</span><span class="mi">3</span><span class="p">])</span> <span class="n">background_color</span> <span class="o">=</span> <span class="p">(</span><span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">255</span><span class="p">)</span> <span class="c1"># Runs much, much, much faster if we don't try to detect the dominant colors </span> <span class="n">lower_bound</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">clip</span><span class="p">([</span><span class="n">value</span> <span class="o">-</span> <span class="mi">5</span> <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">background_color</span><span class="p">],</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">))</span> <span class="n">upper_bound</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">clip</span><span class="p">([</span><span class="n">value</span> <span class="o">+</span> <span class="mi">5</span> <span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">background_color</span><span class="p">],</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">255</span><span class="p">))</span> <span class="n">binary</span> <span class="o">=</span> <span class="n">cv2</span><span class="p">.</span><span class="n">inRange</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">lower_bound</span><span class="p">,</span> <span class="n">upper_bound</span><span class="p">)</span> <span class="c1"># cv2.imshow('binary 1', binary) </span> <span class="c1"># cv2.waitKey(0) </span> <span class="c1"># cv2.destroyAllWindows() </span> <span class="n">replacement_color</span> <span class="o">=</span> <span class="mi">128</span> <span class="n">cv2</span><span class="p">.</span><span class="n">floodFill</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="bp">None</span><span class="p">,</span> <span class="n">bottom_left</span><span class="p">,</span> <span class="n">replacement_color</span><span class="p">)</span> <span class="n">binary</span> <span class="o">=</span> <span class="n">cv2</span><span class="p">.</span><span class="n">inRange</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="n">replacement_color</span><span class="p">,</span> <span class="n">replacement_color</span><span class="p">)</span> <span class="c1"># cv2.imshow('binary 2', binary) </span> <span class="c1"># cv2.waitKey(0) </span> <span class="c1"># cv2.destroyAllWindows() </span> <span class="n">cv2</span><span class="p">.</span><span class="n">rectangle</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="n">bottom_left</span><span class="p">,</span> <span class="n">top_right</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># Fill in the room-number-shaped hole for shape recognition </span> <span class="c1"># cv2.imshow('binary 3', binary) </span> <span class="c1"># cv2.waitKey(0) </span> <span class="c1"># cv2.destroyAllWindows() </span> <span class="n">ret</span><span class="p">,</span> <span class="n">thresh</span> <span class="o">=</span> <span class="n">cv2</span><span class="p">.</span><span class="n">threshold</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="mi">127</span><span class="p">,</span> <span class="mi">255</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="n">contours</span><span class="p">,</span> <span class="n">hierarchy</span> <span class="o">=</span> <span class="n">cv2</span><span class="p">.</span><span class="n">findContours</span><span class="p">(</span><span class="n">thresh</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="n">contour</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">contours</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="n">cv2</span><span class="p">.</span><span class="n">contourArea</span><span class="p">)</span> <span class="c1"># grab largest contours </span> <span class="k">return</span> <span class="n">contour</span> </code></pre></div></div> <h2 id="drawing-rooms-on-the-svg">Drawing Rooms on the SVG</h2> <p>The <a href="https://pypi.org/project/svgwrite/">svgwrite package</a>, taking advantage of SVG’s “programmablity,” is utilized to add custom SVG rectangles that take the size of the contours’ bounding boxes.</p> <h3 id="the-code-1">The Code</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contour</span> <span class="o">=</span> <span class="n">get_room_contour</span><span class="p">(</span><span class="n">room</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">media_box</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">text_and_coords</span><span class="p">)</span> <span class="k">if</span> <span class="n">contour</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="n">path_text</span> <span class="o">=</span> <span class="s">"M"</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">contour</span><span class="p">)):</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">contour</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="c1"># Invert y axis (OpenCV measures in the opposite direction) </span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">media_box</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">-</span> <span class="n">y</span> <span class="n">svg_coords</span> <span class="o">=</span> <span class="n">get_svg_coords</span><span class="p">((</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="bp">self</span><span class="p">.</span><span class="n">view_box</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">media_box</span><span class="p">)</span> <span class="n">path_text</span> <span class="o">+=</span> <span class="s">'{0} {1} '</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">svg_coords</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">svg_coords</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="p">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">temp_path</span><span class="p">)</span> <span class="n">dwg</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="p">.</span><span class="n">path</span><span class="p">(</span><span class="n">d</span><span class="o">=</span><span class="n">path_text</span><span class="p">,</span> <span class="n">fill</span><span class="o">=</span><span class="n">color_hex_code</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="n">opacity</span><span class="p">,</span> <span class="nb">id</span><span class="o">=</span><span class="s">"room-rect-{0}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">room</span><span class="p">),</span> <span class="n">onmouseover</span><span class="o">=</span><span class="s">"showRoomData(this, {0}, '{1}')"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">units</span><span class="p">),</span> <span class="n">onmouseout</span><span class="o">=</span><span class="s">"hideRoomData(this)"</span><span class="p">))</span> </code></pre></div></div> <p>Notice that <strong>I add custom <code class="language-plaintext highlighter-rouge">onmouseover</code> and <code class="language-plaintext highlighter-rouge">onmouseout</code> properties to the SVG rectangle</strong>. This was my sneaky way of simplifying the creation of the interactivity behind the heatmap (when you hover over a room, a popup appears with the room number and an exact reading of its temperature or CO2 level). <code class="language-plaintext highlighter-rouge">showRoomData()</code> and <code class="language-plaintext highlighter-rouge">hideRoomData()</code> are JavaScript functions that manage this functionality, and <code class="language-plaintext highlighter-rouge">onmouseover</code> and <code class="language-plaintext highlighter-rouge">onmouseout</code> make detecting when a certain room is hovered over far easier.</p> <h2 id="the-completed-fill_room-function">The Completed <code class="language-plaintext highlighter-rouge">fill_room</code> Function</h2> <p>The complete code for the <code class="language-plaintext highlighter-rouge">fill_room()</code> function is shown below. Notice that it includes code to manage duplicating, saving, and merging the generated SVG rectangles (each saved into its own file) into a <code class="language-plaintext highlighter-rouge">_filled_rooms_temperature.svg</code> or <code class="language-plaintext highlighter-rouge">_filled_rooms_co2.svg</code> file, which are the files displayed by the web interface.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">fill_room</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">room</span><span class="p">,</span> <span class="n">color_hex_code</span><span class="p">,</span> <span class="n">opacity</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">units</span><span class="p">,</span> <span class="n">is_temperature</span><span class="p">):</span> <span class="n">temp_path</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">svg_path</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="o">-</span><span class="mi">4</span><span class="p">]</span> <span class="o">+</span> <span class="s">'_temp_rect.svg'</span> <span class="n">output_path</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">svg_path</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="o">-</span><span class="mi">4</span><span class="p">]</span> <span class="o">+</span> <span class="s">'_filled_rooms_{0}.svg'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="s">'temperature'</span> <span class="k">if</span> <span class="n">is_temperature</span> <span class="k">else</span> <span class="s">'co2'</span><span class="p">)</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="n">output_path</span><span class="p">):</span> <span class="n">shutil</span><span class="p">.</span><span class="n">copy</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">svg_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">)</span> <span class="n">contour</span> <span class="o">=</span> <span class="n">get_room_contour</span><span class="p">(</span><span class="n">room</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">media_box</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">text_and_coords</span><span class="p">)</span> <span class="k">if</span> <span class="n">contour</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span> <span class="n">path_text</span> <span class="o">=</span> <span class="s">"M"</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">contour</span><span class="p">)):</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">contour</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span> <span class="c1"># Invert y axis (OpenCV measures in the opposite direction) </span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">media_box</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">-</span> <span class="n">y</span> <span class="n">svg_coords</span> <span class="o">=</span> <span class="n">get_svg_coords</span><span class="p">((</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="bp">self</span><span class="p">.</span><span class="n">view_box</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">media_box</span><span class="p">)</span> <span class="n">path_text</span> <span class="o">+=</span> <span class="s">'{0} {1} '</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">svg_coords</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">svg_coords</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="p">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">temp_path</span><span class="p">)</span> <span class="n">dwg</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="p">.</span><span class="n">path</span><span class="p">(</span><span class="n">d</span><span class="o">=</span><span class="n">path_text</span><span class="p">,</span> <span class="n">fill</span><span class="o">=</span><span class="n">color_hex_code</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="n">opacity</span><span class="p">,</span> <span class="nb">id</span><span class="o">=</span><span class="s">"room-rect-{0}"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">room</span><span class="p">),</span> <span class="n">onmouseover</span><span class="o">=</span><span class="s">"showRoomData(this, {0}, '{1}')"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">units</span><span class="p">),</span> <span class="n">onmouseout</span><span class="o">=</span><span class="s">"hideRoomData(this)"</span><span class="p">))</span> <span class="n">dwg</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> <span class="c1"># Save the path to a temporary file </span> <span class="c1"># Merge the files </span> <span class="n">floor_plan</span> <span class="o">=</span> <span class="n">st</span><span class="p">.</span><span class="n">fromfile</span><span class="p">(</span><span class="n">output_path</span><span class="p">)</span> <span class="n">second_svg</span> <span class="o">=</span> <span class="n">st</span><span class="p">.</span><span class="n">fromfile</span><span class="p">(</span><span class="n">temp_path</span><span class="p">)</span> <span class="n">floor_plan</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">second_svg</span><span class="p">)</span> <span class="n">floor_plan</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">output_path</span><span class="p">)</span> <span class="n">os</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">temp_path</span><span class="p">)</span> </code></pre></div></div> <p>Thus, I could now programmatically fill in the room number of my choice with the color of my choice. The output of calling <code class="language-plaintext highlighter-rouge">fill_room()</code> 4 times with dummy colors of my choosing is shown below:</p> <p><img src="/images/projects/AHS-Heatmap/fill_room_with_color_of_choice.png" alt="Adjacent rooms 217, 219, 221, and 223 - each of varying sizes - filled in with chosen dummy colors" class="centered-image" /></p> <h3 id="putting-it-all-together">Putting it All Together</h3> <p>By connecting the completed <code class="language-plaintext highlighter-rouge">fill_room()</code> function with live sensor data and code that determines the proper color for a room based off of its temperature or CO2 concentration, the heatmaps seen in the final product are generated.</p> <p>The driver code for filling an entire floor’s heatmap:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">fill_from_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">is_temperature_value</span><span class="p">):</span> <span class="n">value</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="s">'temperature'</span><span class="p">]</span> <span class="k">if</span> <span class="n">is_temperature_value</span> <span class="k">else</span> <span class="n">data</span><span class="p">[</span><span class="s">'co2'</span><span class="p">]</span> <span class="n">units</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="s">'temperature units'</span><span class="p">]</span> <span class="k">if</span> <span class="n">is_temperature_value</span> <span class="k">else</span> <span class="n">data</span><span class="p">[</span><span class="s">'co2 units'</span><span class="p">]</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">math</span><span class="p">.</span><span class="n">isnan</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="ow">and</span> <span class="n">units</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="ow">and</span> <span class="n">units</span> <span class="o">!=</span> <span class="s">''</span><span class="p">:</span> <span class="n">color</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_value_color</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">is_temperature_value</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">fill_room</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s">'room'</span><span class="p">],</span> <span class="n">color</span><span class="p">,</span> <span class="n">FILLED_PATH_OPACITY</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">units</span><span class="p">,</span> <span class="n">is_temperature_value</span><span class="p">)</span> </code></pre></div></div> <h3 id="ending-notes">Ending Notes</h3> <p>Thank you very much for reading this series! This specific aspect of the AHS Heatmap took me the most time to design and develop and involved a great deal of learning on my part. I hope that this explanation may serve others well and save them time in the future. Best of luck with your OpenCV adventures, and Happy New Year!</p> Sat, 28 Dec 2019 00:00:00 +0000 https://ivanovi.ch/blog/2019/12/how-heatmap-works-3/ https://ivanovi.ch/blog/2019/12/how-heatmap-works-3/ blog posts EA Heatmap OpenCV and AHS Heatmap | Working with SVGs (Part 2) <p><em>This is part 2 of a series of blog posts about the computer vision behind the AHS Heatmap. <a href="/blog/2019/09/how-heatmap-works/">Read part one here</a></em></p> <p>When I was experimenting with SVGs and OpenCV, I realized that I would have to convert my floor plans to PNGs to perform computer-vision-based analysis to detect rooms. This CV-based room-detection algorithm was not something I realized early into the project - it was maybe my third or fourth idea, but it certainly was my best and most-thought-out one.</p> <h2 id="problem-1-extracting-room-number-text-box-coordinates">Problem 1: Extracting Room Number Text Box Coordinates</h2> <p>In order to detect the shape of the room around a room number, I would first have to determine where that room number was in the converted PNG. PNGs, unlike SVGs, do not have particular “selectable” text elements - PNGs are simply pixels that make no distinction between lines, shapes, text, or anything else. The SVG floor plan I received, however, stored its room numbers in a rather unapproachable manner, placing them in a seemingly-random order throughout the file on a character-by-character basis. This would make it very difficult to distinguish programmatically which character corresponded to which digit of which room number in which location, so I found myself faced with the challenge of devising a separate method of determining which rooms were on a map, then determining where on the map their room numbers were.</p> <p>I decided to brainstorm other types of files that distinguish the text contained within them. PDFs came to mind, and luckily <a href="https://inkscape.org/">Inkscape</a>’s command-line tool can convert SVGs not only to PNGs, but also to PDFs!</p> <p>I will not go into the details of the PDF text-box-coordinate-extracting algorithm in this post, as <a href="/blog/2018/09/pdf-text-extraction/">there is already a post on my blog dedicated to it</a>. However, I will explain how I used Inkscape to perform the conversions: I used Python’s built-in <code class="language-plaintext highlighter-rouge">os</code> package to call <code class="language-plaintext highlighter-rouge">os.system()</code>, which executes commands. My SVG conversion code is below:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span> <span class="kn">from</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="n">platform</span> <span class="kn">import</span> <span class="nn">os</span> <span class="k">def</span> <span class="nf">svg_to_png</span><span class="p">(</span><span class="n">svg_path</span><span class="p">,</span> <span class="n">png_path</span><span class="p">,</span> <span class="n">dpi</span><span class="p">):</span> <span class="c1"># Delete the file if it exists, as inkscape won't overwrite </span> <span class="n">remove_file</span><span class="p">(</span><span class="n">png_path</span><span class="p">)</span> <span class="c1"># run the command to convert the svg, adding '&gt; /dev/null' on the end to silence the output by storing it into null </span> <span class="n">options</span> <span class="o">=</span> <span class="s">'--without-gui --export-area-page --export-background="#ffffff"'</span> <span class="k">if</span> <span class="n">platform</span> <span class="o">==</span> <span class="s">"linux"</span> <span class="ow">or</span> <span class="n">platform</span> <span class="o">==</span> <span class="s">"linux2"</span><span class="p">:</span> <span class="c1"># run command to convert the svg, adding '&gt; /dev/null' on the end to silence the output by storing it into null </span> <span class="n">os</span><span class="p">.</span><span class="n">system</span><span class="p">(</span><span class="s">'inkscape %s "%s" --export-dpi=%s --export-png="%s" &gt; /dev/null'</span> <span class="o">%</span> <span class="p">(</span><span class="n">options</span><span class="p">,</span> <span class="n">svg_path</span><span class="p">,</span> <span class="n">dpi</span><span class="p">,</span> <span class="n">png_path</span><span class="p">))</span> <span class="k">else</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">system</span><span class="p">(</span> <span class="s">'inkscape %s "%s" --export-dpi=%s --export-png="%s"'</span> <span class="o">%</span> <span class="p">(</span><span class="n">options</span><span class="p">,</span> <span class="n">svg_path</span><span class="p">,</span> <span class="n">dpi</span><span class="p">,</span> <span class="n">png_path</span><span class="p">))</span> <span class="n">wait_for_creation</span><span class="p">(</span><span class="n">png_path</span><span class="p">)</span> <span class="k">def</span> <span class="nf">svg_to_pdf</span><span class="p">(</span><span class="n">svg_path</span><span class="p">,</span> <span class="n">pdf_path</span><span class="p">):</span> <span class="c1"># Delete the file if it exists, as inkscape won't overwrite </span> <span class="n">remove_file</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">)</span> <span class="n">options</span> <span class="o">=</span> <span class="s">'--without-gui --export-area-page'</span> <span class="k">if</span> <span class="n">platform</span> <span class="o">==</span> <span class="s">"linux"</span> <span class="ow">or</span> <span class="n">platform</span> <span class="o">==</span> <span class="s">"linux2"</span><span class="p">:</span> <span class="c1"># run command to convert the svg, adding '&gt; /dev/null' on the end to silence the output by storing it into null </span> <span class="n">os</span><span class="p">.</span><span class="n">system</span><span class="p">(</span><span class="s">'inkscape %s "%s" --export-pdf="%s" &gt; /dev/null'</span> <span class="o">%</span> <span class="p">(</span><span class="n">options</span><span class="p">,</span> <span class="n">svg_path</span><span class="p">,</span> <span class="n">pdf_path</span><span class="p">))</span> <span class="k">else</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">system</span><span class="p">(</span><span class="s">'inkscape %s "%s" --export-pdf="%s"'</span> <span class="o">%</span> <span class="p">(</span><span class="n">options</span><span class="p">,</span> <span class="n">svg_path</span><span class="p">,</span> <span class="n">pdf_path</span><span class="p">))</span> <span class="n">wait_for_creation</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">)</span> <span class="k">def</span> <span class="nf">remove_file</span><span class="p">(</span><span class="n">path</span><span class="p">):</span> <span class="k">try</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">except</span> <span class="nb">OSError</span><span class="p">:</span> <span class="k">pass</span> <span class="k">def</span> <span class="nf">wait_for_creation</span><span class="p">(</span><span class="n">path</span><span class="p">):</span> <span class="k">while</span> <span class="ow">not</span> <span class="n">Path</span><span class="p">(</span><span class="n">path</span><span class="p">).</span><span class="n">is_file</span><span class="p">():</span> <span class="k">continue</span> <span class="c1"># Wait until it's completed </span></code></pre></div></div> <p>Something worth noticing is my use of the <code class="language-plaintext highlighter-rouge">--export-dpi</code> flag in the SVG to PNG conversion command. By specifying the proper DPI, I could result in <strong>PDFs and PNGs of matching pixel dimensions</strong>. Although not necessary, this was very useful to <a href="/blog/2019/12/how-heatmap-works-3/#detecting-room-shapes">using OpenCV to detect rooms</a>, as <strong>I did not have to scale the coordinates extracted from the PDF</strong> for the location of the text boxes to match up across files. Thus, I could simply use the coordinates of the PDF as coordinates on the PNG, and the extracted text box positions were guaranteed to match up with those of the image OpenCV was analyzing (this was essential to the floodfill algorithm described in <a href="/blog/2019/12/how-heatmap-works-3/#floodfill-image">part 3</a>).</p> <h2 id="problem-2-svg-to-png-coordinates">Problem 2: SVG to PNG Coordinates</h2> <p>Although both SVGs and PNGs can use pixel coordinate systems, the coordinates of a point on a floor plan’s SVG would be nowhere near the coordinates of the same point on the converted PNG.</p> <p><img src="/images/projects/AHS-Heatmap/png_vs_svg_coords.png" alt="Vast disparity between PNG and SVG coordinate systems" class="centered-image" /></p> <center><em>Here, "on screen" means in relation to the converted PNG.</em></center> <p>This was a serious problem because it meant that any room coordinates detected by OpenCV would be unusable when overlaying the colored rectangles on the filled SVG (see <a href="/blog/2019/12/how-heatmap-works-3/">part 3</a>).</p> <p>Eventually, I discovered two important things:</p> <ol> <li>When creating SVG elements, the y axes of PNGs and SVGs start at different points (one starts from the bottom of the screen, the other starts from the top of the screen)</li> <li>One can calculate what SVG coordinate a particular PNG coordinate corresponds to by forming a ratio that uses the SVG’s view box dimensions.</li> </ol> <p>By combining these two discoveries, I wrote the following Python code that converts PNG coordinates to SVG coordinates:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_svg_coords</span><span class="p">(</span><span class="n">png_coords</span><span class="p">,</span> <span class="n">svg_view_box</span><span class="p">,</span> <span class="n">pdf_media_box</span><span class="p">):</span> <span class="n">x_coord</span> <span class="o">=</span> <span class="n">png_coords</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">svg_view_box</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">/</span> <span class="n">pdf_media_box</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="n">y_coord</span> <span class="o">=</span> <span class="n">svg_view_box</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">-</span> <span class="n">png_coords</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">svg_view_box</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">/</span> <span class="n">pdf_media_box</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="n">coords</span> <span class="o">=</span> <span class="p">[</span><span class="n">x_coord</span><span class="p">,</span> <span class="n">y_coord</span><span class="p">]</span> <span class="k">return</span> <span class="nb">tuple</span><span class="p">(</span><span class="n">coords</span><span class="p">)</span> </code></pre></div></div> Sat, 12 Oct 2019 00:00:00 +0000 https://ivanovi.ch/blog/2019/10/how-heatmap-works-2/ https://ivanovi.ch/blog/2019/10/how-heatmap-works-2/ blog posts EA Heatmap OpenCV and AHS Heatmap | Automatically Finding Rooms in an Image (Part 1) <p><em>This is part 1 of a series of blog posts about the computer vision behind the AHS Heatmap.</em></p> <p>As I was designing the <a href="https://heatmap.energizeandover.com">AHS Heatmap</a>, one of my main goals was to create software that was not bound to any specific floor map. That is to say, I did not want the software to be designed for a specific set of SVG floor plans or rely on a manually-labeled list of room coordinates to know which rooms were on the current floor, which color to fill them with, and where to place the colored rectangle. By designing it in this way, the program would have the potential to be applied to all of Andover’s public schools, not solely Andover High School. Implementing this, however, proved to be quite complicated.</p> <p>Throughout this post, I will introduce the tools behind this process and the file format that will be worked with.</p> <h2 id="input-format">Input Format</h2> <p>AHS Heatmap takes in a folder of SVG floor maps. These equate to enormous vector images (those of AHS are roughly 11,000 by 8,500 pixels). SVGs are quite difficult to work with programmatically - although their status as essentially “programmed” images makes them great to work with directly or within websites, the vast difference between the content of an SVG and a standard image format, such as a PNG or a JPG, makes SVGs unusable by many image-processing tools and libraries.</p> <p>A sample floor plan from AHS is shown below: <img src="/images/blog post images/EA Heatmap/Andover HS Level 4.png" alt="Andover High School Level 4 Floor Plan" class="centered-image" /></p> <h2 id="opencv">OpenCV</h2> <p><a href="https://opencv.org/">OpenCV</a> is a massively popular computer vision library with implementations in many languages, including Python. It is free-to-use and very well-documented, so I decided to employ it for the heatmap’s computer vision.</p> <h2 id="converting-svgs-to-pngs">Converting SVGs to PNGs</h2> <p>The heatmap depends on <a href="https://inkscape.org/">Inkscape</a> a free vector graphic design tool. Inkscape uniquely has a command-line interface that allows the usage of Inkscape to convert SVGs to PNG images (alongside many other formats) with a great degree of flexibility. This worked out very well for the project the ability to utilize Inkscape’s conversion functionality without GUI meant that the heatmap could be deployed to SSH-only machines, such as the Google Compute Engine virtual machine upon which it is currently hosted.</p> <h2 id="converting-svgs-to-pdfs">Converting SVGs to PDFs</h2> <p>Although the reasoning behind my need for this functionality may not be clear just yet, it will be explained in <a href="/blog/2019/10/how-heatmap-works-2/">part 2 of this series</a>. It turns out that Inkscape’s CLI is also capable of performing this kind of conversion.</p> Sat, 14 Sep 2019 00:00:00 +0000 https://ivanovi.ch/blog/2019/09/how-heatmap-works/ https://ivanovi.ch/blog/2019/09/how-heatmap-works/ blog posts EA Heatmap AHS Heatmap Presentation Recap <p>Earlier today, I presented to town officials and leaders in energy alongside fellow members of Energize Andover. Specifically, I presented a year-long project of mine, the <a href="https://heatmap.energizeandover.com">AHS Heatmap</a>. I gave a brief overview of the process of creating the heatmap, one of its essential underlying algorithms, and shared a demo. Afterwards, I answered questions and discussed the future of the project and what it could be used for. As the heatmap was designed flexibly, there is the potential for this codebase to be applied to all of Andover Public Schools, not solely Andover High School. This, however, will require the installation of many more sensors into other school buildings and the expansion of our BACnet API (Andover High School has far more accessible sensors inside its building than any other public school). This is the direction that I would love to take this project, but, as of now, it will continue to be employed for AHS alone. Overall, this presentation was a great experience and is yet another motivator to continue my work for Energize Andover. I am looking forward to completing projects of a similar scale after concluding this one.</p> <p>This is the slideshow that I presented:</p> <div class="google-slides-container"> <iframe src="https://docs.google.com/presentation/d/e/2PACX-1vRnXBZGGHXRhoHAsI-RIUQSdDQm-Nq-VU-DQeqfnq48sc5OtH4jDSBnRAqVKp5ZPtwdgzu4xH5rZjtj/embed?start=false&amp;loop=false&amp;delayms=3000" frameborder="0" width="1440" height="839" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe> </div> Thu, 08 Aug 2019 00:00:00 +0000 https://ivanovi.ch/blog/2019/08/heatmap-presentation/ https://ivanovi.ch/blog/2019/08/heatmap-presentation/ blog posts EA Heatmap Building Energy Boot Camp 2019 - Day 6 <p>Today, I spend the day working on the <a href="https://heatmap.energizeandover.com">AHS Heatmap</a>, improving its ability to fill rooms, reconfiguring the VM that hosts it, and installing an SSL Certificate for HTTPS support. I successfully managed to make the program draw the room’s <em>contours</em> (the outlines of the rooms) instead of the <em>bounding box</em> (the smallest rectangle containing the contour). The following is a demonstration of the difference between the terms:</p> <p><br /></p> <p>This is the <em>contour</em> of the room:</p> <p><img src="/images/blog post images/EA Bootcamps/2019/6/Contour.png" alt="Outlined Contour" class="centered-image" /></p> <p>It is the outline of the room. It <em>can</em> be irregularly shaped. It is a more precise representation of the room, but is much harder to work with for calculations.</p> <p>The following is the <em>bounding box</em> of said contour:</p> <p><img src="/images/blog post images/EA Bootcamps/2019/6/Bounding-Box.png" alt="Outlined Contour and Bounding Box" class="centered-image" /></p> <p>It is the smallest <em>rectangular</em> shape that contains the contour. It is a less precise approximation of the room, but its rectangular shape makes it much easier to work with in calculations.</p> <h2 id="what-changed">What Changed?</h2> <p>My code used to exclusively use the <em>bounding box</em> to both color rooms and perform client-side calculations with JavaScript (to move the information box that displays the room number and sensor reading). This meant that the application would not work with irregularly shaped rooms (rooms that were not rectangular) without drawing an overly-large rectangle. Now, it uses <strong>both the bounding box and the contour</strong> in combination. The contour is used to color the rooms, meaning the application can now work with rooms of any shape. The bounding box, however, still is used to calculate the desired position of the information box, as SVG paths are very difficult to work with in these calculations without such approximations. Although this very slightly increased the time required to generate a map (as contours are more difficult to draw), it greatly increased the accuracy of the map, allowing it to avoid overlapping colored areas and to fill the non-rectangular rooms in the building, such as the library (room 270 in the image below).</p> <p><img src="/images/blog post images/EA Bootcamps/2019/6/Generated-Map.png" alt="Generated Map" class="centered-image" /></p> <center><em>A map of the second floor of the building. Notice the non-rectangular shapes used to color the rooms.</em></center> <h2 id="ssl-certificate-and-https">SSL Certificate and HTTPS</h2> <p>After deploying my updates to the virtual machine (VM) that hosts the map on <a href="https://heatmap.energizeandover.com">heatmap.energizeandover.com</a>, I began to look into ways to secure my site and protect its users with HTTPS security. To do this, I needed to secure an SSL License and install it into <a href="https://www.nginx.com/">Nginx</a>, the web server the VM uses. I ended up securing a free license from <a href="https://letsencrypt.org/">Let’s Encrypt</a>, a non-profit certificate authority providing. I did this by using <a href="https://certbot.eff.org/">certbot</a>, a command line tool that can automatically secure, renew, and install an SSL License to a variety of web servers, Nginx included. Finally, I tested the renewal of the license and eventually scheduled a daily check for an expiring license, so certbot will automatically renew the license for me 30 days before it expires. I tested my license, and secured an A+ rating from a widely-used SSL testing site!</p> <p><img src="/images/blog post images/EA Bootcamps/2019/6/SSL Test.png" alt="A+ SSL Score" class="centered-image" /></p> <center><em>SSL report by <a href="https://www.ssllabs.com/" target="_blank">Qualys SSL Labs</a>.</em></center> <p>At the end of the day, the heatmap was in a much better state than it was before the bootcamp meeting.</p> Mon, 05 Aug 2019 14:00:00 +0000 https://ivanovi.ch/blog/2019/08/ea-day-6/ https://ivanovi.ch/blog/2019/08/ea-day-6/ blog posts building energy boot camp 2019 Building Energy Boot Camp 2019 - Day 5 <p>Today, I returned my focus to the <a href="https://matplotlib.org/">matplotlib</a> project we were <a href="/blog/2019/ea-day-2">assigned on Day 2 of the boot camp</a>. I made tremendous progress, and nearly finished the entire project. I successfully implemented a multi-threaded data-retrieval system. Most notably, the threading class is <em>completely and safely killable</em>, something I have found terribly difficult to achieve with Python in the past. See <em>Killable Threads - Background</em> and <em>Killable Threads - The Code</em> for a detailed explanation of how I achieved this. I also learned about many of matplotlib’s features that I was previously unaware of, including the abilities to set custom tick marks along the x and y axes, scroll the graph without deleting off-screen points, and bind function to various user-inputted events in the opened window. By using these features, I was able to add much more functionality to the graph, such as panning and zooming with the mouse wheel. Throughout the rest of this post, I will describe in detail the process behind these additions.</p> <h2 id="killable-threads---background">Killable Threads - Background</h2> <p>In Python, there is no way to <em>safely</em> “kill” an instance of the <code class="language-plaintext highlighter-rouge">threading.Thread</code> class. That is to say, there is no way to stop the <code class="language-plaintext highlighter-rouge">Thread</code>’s execution of its target function without running the risk of leaving open critical resources that must be closed. You could always terminate the entire program by using <code class="language-plaintext highlighter-rouge">sys.exit()</code>, but, for the aforementioned reason, this is a very dangerous practice, particularly so in my case, as I sought to save all the data collected by the threaded function to a CSV file (a tabular format). However, I did not always want to wait for said function to finish whenever a user requested the termination of the program, as the process of requesting and storing sensor data typically takes anywhere between 10 and 15 seconds. When this long delay is added to the time required to output all stored data to a file, I was facing a shutdown time long enough to bore users into forcibly killing my program as a whole, which both be risky for their computers and my program’s chances of outputting its data to a file. With this in mind, I set about modifying my threaded function, <code class="language-plaintext highlighter-rouge">continuous_update</code>, to make it quickly and safely terminable. Here’s how I achieved this goal:</p> <p>Before I explain the implementation of my killable system, I will first explain the general structure of the Line Graph portion of my boot camp code (which is available on <a href="https://github.com/Ivanov1ch/Building-Energy-Bootcamp-2019">GitHub</a>!) and the relationship between its files. My code currently follows this file structure:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Building-Energy-Bootcamp-2019 ├── BuildingEnergyAPI # Python package containing the API used to get data from our sensors ├── csv # Contains csv files with the data needed to request from a sensor ├── LineGraph # Python package containing files specific to the Line Graph │ ├── app.py # The main file that runs the Line Graph │ └── update_thread.py # The file containing the code defining the DataUpdateThread class ├── pylive # Python package my modified version of pylive, used to plot data │ └── pylive.py # The file containing the code related to initializing and updating the graph └── ... # etc. (Other directories and files unrelated to the Line Graph) </code></pre></div></div> <p>To run the Line Graph, the file <code class="language-plaintext highlighter-rouge">LineGraph/app.py</code> is executed from the <code class="language-plaintext highlighter-rouge">Building-Energy-Bootcamp-2019</code> (root) directory. This file imports <code class="language-plaintext highlighter-rouge">DataUpdateThread</code>, the class that asynchronously manages the data-retrieval process, from <code class="language-plaintext highlighter-rouge">LineGraph/update_thread.py</code>, and instantiates a copy of the class, providing the <a href="https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html">pandas DataFrame</a> (in short, a table of data) used by the entire project to the class. It also manages the initialization and updates of the line graph, using methods from the file <code class="language-plaintext highlighter-rouge">pylive/pylive.py</code>. This entire process and relationships can be summarized by the following 3 code snippets:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">LineGraph.update_thread</span> <span class="kn">import</span> <span class="n">DataUpdateThread</span> <span class="kn">from</span> <span class="nn">pylive.pylive</span> <span class="kn">import</span> <span class="n">live_plotter_init</span><span class="p">,</span> <span class="n">live_plotter_update</span><span class="p">,</span> <span class="n">has_been_closed</span> <span class="p">...</span> <span class="n">update_thread</span> <span class="o">=</span> <span class="n">DataUpdateThread</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">csv_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">)</span> <span class="n">update_thread</span><span class="p">.</span><span class="n">begin_update_thread</span><span class="p">(</span><span class="n">update_interval</span><span class="p">)</span> <span class="p">...</span> <span class="n">live_plotter_init</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="p">...)</span> </code></pre></div></div> <p>Although the vast majority of the code in <code class="language-plaintext highlighter-rouge">app.py</code> has been omitted in these snippets, enough may be derived about the relationship between <code class="language-plaintext highlighter-rouge">app.py</code>, <code class="language-plaintext highlighter-rouge">update_thread.py</code>, and <code class="language-plaintext highlighter-rouge">pylive.py</code>. The important thing to notice about this code is that the <em>same</em> DataFrame instance is being passed to <code class="language-plaintext highlighter-rouge">pylive.py</code>’s methods and to the <code class="language-plaintext highlighter-rouge">DataUpdateThread</code>’s constructor. Following the rules of object oriented design and references to objects, this means that both the update thread and the plotter are referencing the <em>same table, with the same data</em>. Thus, <strong>when the <code class="language-plaintext highlighter-rouge">DataUpdateThread</code> instance requests new data and appends it to the end of the table, the plotter’s reference to the data table sees the same changes</strong>. To maintain this connection and communication, both <code class="language-plaintext highlighter-rouge">update_thread.py</code> and <code class="language-plaintext highlighter-rouge">pylive.py</code> are entirely dependent on <code class="language-plaintext highlighter-rouge">app.py</code>. <strong>The two files have no direct means of communication, but rather must rely on methods in app.py to relay any important information,</strong> such as the user’s request to close the graph, initiated by their press of the X button. Keep this in mind as I explain how I made my <code class="language-plaintext highlighter-rouge">DataUpdateThread</code>s terminable.</p> <h2 id="killable-threads---the-code">Killable Threads - The Code</h2> <p>As the <code class="language-plaintext highlighter-rouge">update_thread.py</code> and <code class="language-plaintext highlighter-rouge">pylive.py</code> files are completely blind to each other, I designed both to be centered around management by <code class="language-plaintext highlighter-rouge">app.py</code>. The first changes that I made were to the <code class="language-plaintext highlighter-rouge">DataUpdateThread</code> class, starting with its constructor:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">DataUpdateThread</span><span class="p">:</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">df</span><span class="p">,</span> <span class="n">csv_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">):</span> <span class="bp">self</span><span class="p">.</span><span class="n">data_df</span> <span class="o">=</span> <span class="n">df</span> <span class="bp">self</span><span class="p">.</span><span class="n">csv_path</span> <span class="o">=</span> <span class="n">csv_path</span> <span class="bp">self</span><span class="p">.</span><span class="n">output_path</span> <span class="o">=</span> <span class="n">output_path</span> <span class="bp">self</span><span class="p">.</span><span class="n">thread</span> <span class="o">=</span> <span class="bp">None</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="o">=</span> <span class="bp">False</span> <span class="bp">self</span><span class="p">.</span><span class="n">fully_stopped</span> <span class="o">=</span> <span class="bp">False</span> </code></pre></div></div> <p>Note the two fields <code class="language-plaintext highlighter-rouge">stopped</code>, and <code class="language-plaintext highlighter-rouge">fully stopped</code>. They are both booleans, and default to <code class="language-plaintext highlighter-rouge">False</code>. <code class="language-plaintext highlighter-rouge">stopped</code> represents whether or not a call to stop the <code class="language-plaintext highlighter-rouge">thread</code>’s execution has been made, and <code class="language-plaintext highlighter-rouge">fully_stopped</code> is set to <code class="language-plaintext highlighter-rouge">True</code> when the <code class="language-plaintext highlighter-rouge">thread</code> has been stopped and the information in <code class="language-plaintext highlighter-rouge">data_df</code> has been saved to <code class="language-plaintext highlighter-rouge">output_path</code>, signifying that a completely safe shutdown is ready. I also defined a <code class="language-plaintext highlighter-rouge">stop(self)</code> method and a <code class="language-plaintext highlighter-rouge">is_fully_stopped(self)</code> method, to allow <code class="language-plaintext highlighter-rouge">app.py</code> to remotely trigger the shutdown process and to provide an accessor that informs <code class="language-plaintext highlighter-rouge">app.py</code> when it can safely stop.</p> <p>Next, I turned to <code class="language-plaintext highlighter-rouge">continuous_update</code>, a method that repeatedly calls <code class="language-plaintext highlighter-rouge">get_new_values</code> (which requests new data and appends it as a row at the bottom of the <code class="language-plaintext highlighter-rouge">data_df</code> shared with <code class="language-plaintext highlighter-rouge">pylive.py</code>) until a stop is requested.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">continuous_update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">repeat_interval</span><span class="p">):</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="ow">or</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="k">break</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_new_values</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="ow">or</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="k">break</span> <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">repeat_interval</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">save_data</span><span class="p">()</span> <span class="bp">self</span><span class="p">.</span><span class="n">fully_stopped</span> <span class="o">=</span> <span class="bp">True</span> </code></pre></div></div> <p>Before and after it begins the process of requesting new data, this method first checks if a stop has been requested or if the <code class="language-plaintext highlighter-rouge">pylive</code> window has been closed (using <code class="language-plaintext highlighter-rouge">has_been_closed()</code>). If either of these conditions of met, it exits its infinite loop and saves its data before flagging the thread as <code class="language-plaintext highlighter-rouge">fully_stopped</code>.</p> <p>Finally, to allow the interruption of the 10-15 second long method <code class="language-plaintext highlighter-rouge">get_new_values</code>, I used a similar strategy:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_new_values</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">csv_path</span><span class="p">)</span> <span class="n">retrieved_data</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="n">iterrows</span><span class="p">():</span> <span class="n">value</span><span class="p">,</span> <span class="n">units</span> <span class="o">=</span> <span class="n">get_value</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="s">'Facility'</span><span class="p">],</span> <span class="n">row</span><span class="p">[</span><span class="s">'Meter'</span><span class="p">],</span> <span class="n">live</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">value</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">numbers</span><span class="p">.</span><span class="n">Number</span><span class="p">)</span> <span class="k">else</span> <span class="s">''</span> <span class="n">retrieved_data</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="ow">or</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="k">return</span> <span class="n">retrieved_data</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">dt</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">())</span> <span class="c1"># Add to the end of the DF </span> <span class="bp">self</span><span class="p">.</span><span class="n">data_df</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">data_df</span><span class="p">)]</span> <span class="o">=</span> <span class="n">retrieved_data</span> </code></pre></div></div> <p>After receiving data from a sensor, the method checks to see if a stop has been requested by either <code class="language-plaintext highlighter-rouge">app.py</code> or the user. If that is the case, it exits the function without bothering to request data from the remaining sensors, or appending the incomplete data to the end of the dataframe. This greatly reduced the shutdown times, as users now would have to wait, in the worst case scenario, for only one piece of data to be received instead of a piece of data from each sensor.</p> <p>Lastly, I defined the <code class="language-plaintext highlighter-rouge">proper_shutdown</code> method in <code class="language-plaintext highlighter-rouge">app.py</code>, which always ensures a safe shutdown of the program and thread.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">proper_shutdown</span><span class="p">():</span> <span class="k">print</span><span class="p">(</span><span class="s">"Shutting down, please wait..."</span><span class="p">)</span> <span class="n">update_thread</span><span class="p">.</span><span class="n">stop</span><span class="p">()</span> <span class="k">while</span> <span class="ow">not</span> <span class="n">update_thread</span><span class="p">.</span><span class="n">is_fully_stopped</span><span class="p">():</span> <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"Fully shut down"</span><span class="p">)</span> <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">()</span> </code></pre></div></div> <p>This method takes advantage of my new additions to the <code class="language-plaintext highlighter-rouge">DataUpdateThread</code> class to ensure the thread has safely stopped and has saved its data before ending the program. By utilizing the <code class="language-plaintext highlighter-rouge">atexit</code> library, I made sure that <code class="language-plaintext highlighter-rouge">proper_shutdown()</code> is run before the program terminates for any reason. To communicate a shutdown to <code class="language-plaintext highlighter-rouge">app.py</code> when the graph is closed, I used matplotlib’s event bindings to call a method that initiates <code class="language-plaintext highlighter-rouge">proper_shutdown</code>, <code class="language-plaintext highlighter-rouge">window_closed</code>:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">close_bind</span> <span class="o">=</span> <span class="n">fig</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">mpl_connect</span><span class="p">(</span><span class="s">'close_event'</span><span class="p">,</span> <span class="n">window_closed</span><span class="p">)</span> </code></pre></div></div> <p>And with that, my killable threads were done!</p> <h2 id="graph-interactivity-improvements">Graph Interactivity Improvements</h2> <p>Today, I also modified <code class="language-plaintext highlighter-rouge">pylive.py</code> to make my graph more complete and interactive. First, I made the view scroll with the graph by setting the limits of both the x and y axes to always fit the 10 most recent points of every line, which allowed me to plot the entirety of the <code class="language-plaintext highlighter-rouge">data_df</code> DataFrame. To make off-screen data visible, I programmed in the ability to scroll with the mouse wheel and planned on utilizing matplotlib’s prebuilt panning system. However, I quickly realized that the bound-setting code I had added for scrolling would always send the panned graph back to its original position. I fixed this by binding new methods to mouse events and adding a new boolean variable, <code class="language-plaintext highlighter-rouge">panning_allowed</code>, that stops the code from adjusting the limits of the visible portion of the x and y axes while the user’s mouse is inside the graph. This made my graph very interactive and easy-to-use.</p> <h2 id="screenshots">Screenshots</h2> <p><img src="/images/blog post images/EA Bootcamps/2019/5/AHS_Power_Usage_(Realtime).png" alt="Line Graph (Default View)" class="centered-image" /></p> <center>New Line Graph (Default, Auto-Scrolling View)</center> <p><br /></p> <p><img src="/images/blog post images/EA Bootcamps/2019/5/AHS Power Panned.png" alt="Line Graph (Panned and Zoomed)" class="centered-image" /></p> <center>New Line Graph (Panning and Zooming Demonstration)</center> <h2 id="code">Code</h2> <center><em>app.py</em></center> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span> <span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">import</span> <span class="nn">atexit</span> <span class="kn">import</span> <span class="nn">numbers</span> <span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span> <span class="kn">import</span> <span class="nn">datetime</span> <span class="k">as</span> <span class="n">dt</span> <span class="kn">from</span> <span class="nn">LineGraph.update_thread</span> <span class="kn">import</span> <span class="n">DataUpdateThread</span> <span class="kn">from</span> <span class="nn">BuildingEnergyAPI.building_data_requests_external</span> <span class="kn">import</span> <span class="n">get_value</span> <span class="kn">from</span> <span class="nn">pylive.pylive</span> <span class="kn">import</span> <span class="n">live_plotter_init</span><span class="p">,</span> <span class="n">live_plotter_update</span><span class="p">,</span> <span class="n">has_been_closed</span> <span class="c1"># Open dataframe </span><span class="n">csv_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">'csv'</span><span class="p">,</span> <span class="s">'ahs_power.csv'</span><span class="p">)</span> <span class="n">output_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">'LineGraph'</span><span class="p">,</span> <span class="s">'line_graph_out.csv'</span><span class="p">)</span> <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">csv_path</span><span class="p">)</span> <span class="k">def</span> <span class="nf">get_readings</span><span class="p">():</span> <span class="n">values</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">for</span> <span class="n">row_num</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_lines</span><span class="p">):</span> <span class="n">value</span><span class="p">,</span> <span class="n">units</span> <span class="o">=</span> <span class="n">get_value</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">row_num</span><span class="p">][</span><span class="s">'Facility'</span><span class="p">],</span> <span class="n">df</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">row_num</span><span class="p">][</span><span class="s">'Meter'</span><span class="p">],</span> <span class="n">live</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">value</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">numbers</span><span class="p">.</span><span class="n">Number</span><span class="p">)</span> <span class="k">else</span> <span class="s">''</span> <span class="n">values</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="k">return</span> <span class="n">values</span> <span class="c1"># Pull Labels </span><span class="n">labels</span> <span class="o">=</span> <span class="p">[</span><span class="n">row</span><span class="p">[</span><span class="s">'Label'</span><span class="p">]</span> <span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="n">iterrows</span><span class="p">()]</span> <span class="n">columns</span> <span class="o">=</span> <span class="n">labels</span> <span class="o">+</span> <span class="p">[</span><span class="s">'Time'</span><span class="p">]</span> <span class="c1"># How many seconds between updates </span><span class="n">update_interval</span> <span class="o">=</span> <span class="mi">2</span> <span class="c1"># How many lines to use (number of rows in df) </span><span class="n">num_lines</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">index</span><span class="p">)</span> <span class="c1"># Setup data storing dataframe </span><span class="n">initial_values</span> <span class="o">=</span> <span class="n">get_readings</span><span class="p">()</span> <span class="n">initial_values</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">dt</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">())</span> <span class="n">data_df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">([</span><span class="n">initial_values</span><span class="p">],</span> <span class="n">columns</span><span class="o">=</span><span class="n">columns</span><span class="p">)</span> <span class="n">data_df</span><span class="p">[</span><span class="s">'Time'</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">data_df</span><span class="p">[</span><span class="s">'Time'</span><span class="p">])</span> <span class="c1"># Index by datetime </span> <span class="n">update_thread</span> <span class="o">=</span> <span class="n">DataUpdateThread</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">csv_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">)</span> <span class="n">update_thread</span><span class="p">.</span><span class="n">begin_update_thread</span><span class="p">(</span><span class="n">update_interval</span><span class="p">)</span> <span class="k">def</span> <span class="nf">proper_shutdown</span><span class="p">():</span> <span class="k">print</span><span class="p">(</span><span class="s">"Shutting down, please wait..."</span><span class="p">)</span> <span class="n">update_thread</span><span class="p">.</span><span class="n">stop</span><span class="p">()</span> <span class="k">while</span> <span class="ow">not</span> <span class="n">update_thread</span><span class="p">.</span><span class="n">is_fully_stopped</span><span class="p">():</span> <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"Fully shut down"</span><span class="p">)</span> <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">()</span> <span class="k">try</span><span class="p">:</span> <span class="c1"># Properly shutdown when sys.exit() is called </span> <span class="n">atexit</span><span class="p">.</span><span class="n">register</span><span class="p">(</span><span class="n">proper_shutdown</span><span class="p">)</span> <span class="n">lines</span> <span class="o">=</span> <span class="p">[</span><span class="bp">None</span> <span class="k">for</span> <span class="n">line_num</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_lines</span><span class="p">)]</span> <span class="n">color_options</span> <span class="o">=</span> <span class="p">[</span><span class="s">'b'</span><span class="p">,</span> <span class="s">'g'</span><span class="p">,</span> <span class="s">'r'</span><span class="p">,</span> <span class="s">'c'</span><span class="p">,</span> <span class="s">'m'</span><span class="p">,</span> <span class="s">'y'</span><span class="p">,</span> <span class="s">'k'</span><span class="p">,</span> <span class="s">'w'</span><span class="p">]</span> <span class="c1"># Cycle through every color in the order shown in color_options </span> <span class="n">formats</span> <span class="o">=</span> <span class="p">[</span><span class="s">'{0}-o'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">color_options</span><span class="p">[</span><span class="n">format_num</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">color_options</span><span class="p">)])</span> <span class="k">for</span> <span class="n">format_num</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_lines</span><span class="p">)]</span> <span class="n">live_plotter_init</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">lines</span><span class="p">,</span> <span class="n">formats</span><span class="p">,</span> <span class="p">[</span><span class="n">item</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">'(kW)'</span><span class="p">,</span> <span class="s">''</span><span class="p">)</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">labels</span><span class="p">],</span> <span class="n">title</span><span class="o">=</span><span class="s">"AHS Power Usage (Realtime)"</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">'Elapsed Time (Hours:Minutes:Seconds)'</span><span class="p">,</span> <span class="n">ylabel</span><span class="o">=</span><span class="s">'Power (kW)'</span><span class="p">)</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="n">live_plotter_update</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">lines</span><span class="p">)</span> <span class="k">if</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">()</span> <span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span> <span class="n">proper_shutdown</span><span class="p">()</span> </code></pre></div></div> <center><em>update_thread.py</em></center> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">threading</span> <span class="kn">import</span> <span class="nn">numbers</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">import</span> <span class="nn">os</span> <span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span> <span class="kn">import</span> <span class="nn">datetime</span> <span class="k">as</span> <span class="n">dt</span> <span class="kn">from</span> <span class="nn">pylive.pylive</span> <span class="kn">import</span> <span class="n">has_been_closed</span> <span class="kn">from</span> <span class="nn">BuildingEnergyAPI.building_data_requests_external</span> <span class="kn">import</span> <span class="n">get_value</span> <span class="k">class</span> <span class="nc">DataUpdateThread</span><span class="p">:</span> <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">df</span><span class="p">,</span> <span class="n">csv_path</span><span class="p">,</span> <span class="n">output_path</span><span class="p">):</span> <span class="bp">self</span><span class="p">.</span><span class="n">data_df</span> <span class="o">=</span> <span class="n">df</span> <span class="bp">self</span><span class="p">.</span><span class="n">csv_path</span> <span class="o">=</span> <span class="n">csv_path</span> <span class="bp">self</span><span class="p">.</span><span class="n">output_path</span> <span class="o">=</span> <span class="n">output_path</span> <span class="bp">self</span><span class="p">.</span><span class="n">thread</span> <span class="o">=</span> <span class="bp">None</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="o">=</span> <span class="bp">False</span> <span class="bp">self</span><span class="p">.</span><span class="n">fully_stopped</span> <span class="o">=</span> <span class="bp">False</span> <span class="k">def</span> <span class="nf">get_new_values</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">csv_path</span><span class="p">)</span> <span class="n">retrieved_data</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df</span><span class="p">.</span><span class="n">iterrows</span><span class="p">():</span> <span class="n">value</span><span class="p">,</span> <span class="n">units</span> <span class="o">=</span> <span class="n">get_value</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="s">'Facility'</span><span class="p">],</span> <span class="n">row</span><span class="p">[</span><span class="s">'Meter'</span><span class="p">],</span> <span class="n">live</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">value</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">numbers</span><span class="p">.</span><span class="n">Number</span><span class="p">)</span> <span class="k">else</span> <span class="s">''</span> <span class="n">retrieved_data</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="ow">or</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="k">return</span> <span class="n">retrieved_data</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">dt</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">())</span> <span class="c1"># Add to the end of the DF </span> <span class="bp">self</span><span class="p">.</span><span class="n">data_df</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">data_df</span><span class="p">)]</span> <span class="o">=</span> <span class="n">retrieved_data</span> <span class="k">def</span> <span class="nf">continuous_update</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">repeat_interval</span><span class="p">):</span> <span class="k">while</span> <span class="bp">True</span><span class="p">:</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="ow">or</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="k">break</span> <span class="bp">self</span><span class="p">.</span><span class="n">get_new_values</span><span class="p">()</span> <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="ow">or</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="k">break</span> <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">repeat_interval</span><span class="p">)</span> <span class="bp">self</span><span class="p">.</span><span class="n">save_data</span><span class="p">()</span> <span class="bp">self</span><span class="p">.</span><span class="n">fully_stopped</span> <span class="o">=</span> <span class="bp">True</span> <span class="k">def</span> <span class="nf">begin_update_thread</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">repeat_interval</span><span class="p">):</span> <span class="bp">self</span><span class="p">.</span><span class="n">thread</span> <span class="o">=</span> <span class="n">threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">continuous_update</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">repeat_interval</span><span class="p">,))</span> <span class="bp">self</span><span class="p">.</span><span class="n">thread</span><span class="p">.</span><span class="n">start</span><span class="p">()</span> <span class="k">def</span> <span class="nf">save_data</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="c1"># if file does not exist write to file with header </span> <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">isfile</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">output_path</span><span class="p">):</span> <span class="bp">self</span><span class="p">.</span><span class="n">data_df</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">output_path</span><span class="p">,</span> <span class="n">header</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="c1"># else it exists so append without writing the header </span> <span class="bp">self</span><span class="p">.</span><span class="n">data_df</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">output_path</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s">'a'</span><span class="p">,</span> <span class="n">header</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="k">def</span> <span class="nf">stop</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="p">.</span><span class="n">stopped</span> <span class="o">=</span> <span class="bp">True</span> <span class="k">def</span> <span class="nf">is_fully_stopped</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">fully_stopped</span> </code></pre></div></div> <center><em>pylive.py</em></center> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sys</span> <span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span> <span class="kn">import</span> <span class="nn">datetime</span> <span class="k">as</span> <span class="n">dt</span> <span class="kn">import</span> <span class="nn">matplotlib</span> <span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span> <span class="c1"># use ggplot style for more sophisticated visuals </span><span class="n">plt</span><span class="p">.</span><span class="n">style</span><span class="p">.</span><span class="n">use</span><span class="p">(</span><span class="s">'ggplot'</span><span class="p">)</span> <span class="n">is_closed</span> <span class="o">=</span> <span class="bp">False</span> <span class="n">panning_allowed</span> <span class="o">=</span> <span class="bp">False</span> <span class="k">def</span> <span class="nf">enable_panning</span><span class="p">(</span><span class="n">event</span><span class="p">):</span> <span class="k">global</span> <span class="n">panning_allowed</span> <span class="n">panning_allowed</span> <span class="o">=</span> <span class="bp">True</span> <span class="k">def</span> <span class="nf">disable_panning</span><span class="p">(</span><span class="n">event</span><span class="p">):</span> <span class="k">global</span> <span class="n">panning_allowed</span> <span class="n">panning_allowed</span> <span class="o">=</span> <span class="bp">False</span> <span class="k">def</span> <span class="nf">has_been_closed</span><span class="p">():</span> <span class="k">return</span> <span class="n">is_closed</span> <span class="k">def</span> <span class="nf">window_closed</span><span class="p">(</span><span class="n">event</span><span class="p">):</span> <span class="k">global</span> <span class="n">is_closed</span> <span class="n">is_closed</span> <span class="o">=</span> <span class="bp">True</span> <span class="c1"># Converts a numpy.datetime64 to a datetime.datetime object by converting dt64 to UTC time (for later use) </span><span class="k">def</span> <span class="nf">datetime64_to_datetime</span><span class="p">(</span><span class="n">dt64</span><span class="p">):</span> <span class="k">return</span> <span class="n">dt</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">utcfromtimestamp</span><span class="p">((</span><span class="n">dt64</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="n">datetime64</span><span class="p">(</span><span class="s">'1970-01-01T00:00:00Z'</span><span class="p">))</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">timedelta64</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s">'s'</span><span class="p">))</span> <span class="c1"># Gets a list of x (time) and y (sensor reading) coordinates for the index-th column of data_df # Also returns the labels for the x ticks (strings in HH:MM:SS) format </span><span class="k">def</span> <span class="nf">get_coordinate_lists</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">index</span><span class="p">):</span> <span class="n">time_list</span> <span class="o">=</span> <span class="n">data_df</span><span class="p">[</span><span class="s">'Time'</span><span class="p">].</span><span class="n">tolist</span><span class="p">()</span> <span class="n">value_list</span> <span class="o">=</span> <span class="n">data_df</span><span class="p">.</span><span class="n">iloc</span><span class="p">[:,</span> <span class="n">index</span><span class="p">].</span><span class="n">tolist</span><span class="p">()</span> <span class="n">time_list</span> <span class="o">=</span> <span class="p">[</span><span class="n">datetime64_to_datetime</span><span class="p">(</span><span class="n">time</span><span class="p">)</span> <span class="k">for</span> <span class="n">time</span> <span class="ow">in</span> <span class="n">time_list</span><span class="p">]</span> <span class="c1"># Convert time_list to timedeltas, representing the time between each element of time_list and time_list[0] </span> <span class="n">time_list</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">time</span><span class="p">:</span> <span class="n">time</span> <span class="o">-</span> <span class="n">time_list</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">time_list</span><span class="p">))</span> <span class="c1"># Convert the timedeltas to seconds </span> <span class="n">time_list_seconds</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">timedelta</span><span class="p">:</span> <span class="nb">round</span><span class="p">(</span><span class="n">timedelta</span><span class="p">.</span><span class="n">total_seconds</span><span class="p">()),</span> <span class="n">time_list</span><span class="p">))</span> <span class="c1"># Convert the timedeltas to HH:MM:SS format </span> <span class="n">time_list_strings</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">map</span><span class="p">(</span><span class="k">lambda</span> <span class="n">timedelta</span><span class="p">:</span> <span class="s">"%.2d:%.2d:%.2d"</span> <span class="o">%</span> <span class="p">(</span> <span class="nb">int</span><span class="p">(</span><span class="n">timedelta</span><span class="p">.</span><span class="n">seconds</span> <span class="o">/</span> <span class="mi">3600</span><span class="p">),</span> <span class="p">(</span><span class="n">timedelta</span><span class="p">.</span><span class="n">seconds</span> <span class="o">//</span> <span class="mi">60</span><span class="p">)</span> <span class="o">%</span> <span class="mi">60</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">.</span><span class="n">seconds</span> <span class="o">%</span> <span class="mi">60</span><span class="p">),</span> <span class="n">time_list</span><span class="p">))</span> <span class="k">return</span> <span class="n">time_list_seconds</span><span class="p">,</span> <span class="n">value_list</span><span class="p">,</span> <span class="n">time_list_strings</span> <span class="k">def</span> <span class="nf">live_plotter_init</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">lines</span><span class="p">,</span> <span class="n">formats</span><span class="p">,</span> <span class="n">labels</span><span class="p">,</span> <span class="n">xlabel</span><span class="o">=</span><span class="s">'X Label'</span><span class="p">,</span> <span class="n">ylabel</span><span class="o">=</span><span class="s">'Y Label'</span><span class="p">,</span> <span class="n">title</span><span class="o">=</span><span class="s">'Title'</span><span class="p">):</span> <span class="n">plt</span><span class="p">.</span><span class="n">ion</span><span class="p">()</span> <span class="n">fig</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">figure</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">13</span><span class="p">,</span> <span class="mi">9</span><span class="p">))</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">fig</span><span class="p">.</span><span class="n">add_subplot</span><span class="p">(</span><span class="mi">111</span><span class="p">)</span> <span class="c1"># Set window title </span> <span class="n">gcf</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">gcf</span><span class="p">()</span> <span class="n">gcf</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">set_window_title</span><span class="p">(</span><span class="n">title</span><span class="p">)</span> <span class="c1"># Event bindings </span> <span class="n">close_bind</span> <span class="o">=</span> <span class="n">fig</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">mpl_connect</span><span class="p">(</span><span class="s">'close_event'</span><span class="p">,</span> <span class="n">window_closed</span><span class="p">)</span> <span class="n">enter_bind</span> <span class="o">=</span> <span class="n">fig</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">mpl_connect</span><span class="p">(</span><span class="s">'axes_enter_event'</span><span class="p">,</span> <span class="n">enable_panning</span><span class="p">)</span> <span class="n">exit_bind</span> <span class="o">=</span> <span class="n">fig</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">mpl_connect</span><span class="p">(</span><span class="s">'axes_leave_event'</span><span class="p">,</span> <span class="n">disable_panning</span><span class="p">)</span> <span class="c1"># Setup mouse wheel zooming </span> <span class="k">def</span> <span class="nf">zoom_factory</span><span class="p">(</span><span class="n">ax</span><span class="p">,</span> <span class="n">base_scale</span><span class="o">=</span><span class="mf">2.</span><span class="p">):</span> <span class="k">def</span> <span class="nf">zoom_fun</span><span class="p">(</span><span class="n">event</span><span class="p">):</span> <span class="c1"># get the current x and y limits </span> <span class="n">cur_xlim</span> <span class="o">=</span> <span class="n">ax</span><span class="p">.</span><span class="n">get_xlim</span><span class="p">()</span> <span class="n">cur_ylim</span> <span class="o">=</span> <span class="n">ax</span><span class="p">.</span><span class="n">get_ylim</span><span class="p">()</span> <span class="n">cur_xrange</span> <span class="o">=</span> <span class="p">(</span><span class="n">cur_xlim</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">cur_xlim</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">*</span> <span class="p">.</span><span class="mi">5</span> <span class="n">cur_yrange</span> <span class="o">=</span> <span class="p">(</span><span class="n">cur_ylim</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">cur_ylim</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">*</span> <span class="p">.</span><span class="mi">5</span> <span class="n">xdata</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="n">xdata</span> <span class="c1"># get event x location </span> <span class="n">ydata</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="n">ydata</span> <span class="c1"># get event y location </span> <span class="k">if</span> <span class="n">event</span><span class="p">.</span><span class="n">button</span> <span class="o">==</span> <span class="s">'up'</span><span class="p">:</span> <span class="c1"># deal with zoom in </span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">/</span> <span class="n">base_scale</span> <span class="k">elif</span> <span class="n">event</span><span class="p">.</span><span class="n">button</span> <span class="o">==</span> <span class="s">'down'</span><span class="p">:</span> <span class="c1"># deal with zoom out </span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="n">base_scale</span> <span class="k">else</span><span class="p">:</span> <span class="c1"># deal with something that should never happen </span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">print</span> <span class="n">event</span><span class="p">.</span><span class="n">button</span> <span class="c1"># set new limits </span> <span class="n">ax</span><span class="p">.</span><span class="n">set_xlim</span><span class="p">([</span><span class="n">xdata</span> <span class="o">-</span> <span class="n">cur_xrange</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">,</span> <span class="n">xdata</span> <span class="o">+</span> <span class="n">cur_xrange</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">])</span> <span class="n">ax</span><span class="p">.</span><span class="n">set_ylim</span><span class="p">([</span><span class="n">ydata</span> <span class="o">-</span> <span class="n">cur_yrange</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">,</span> <span class="n">ydata</span> <span class="o">+</span> <span class="n">cur_yrange</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">])</span> <span class="n">plt</span><span class="p">.</span><span class="n">draw</span><span class="p">()</span> <span class="c1"># force re-draw </span> <span class="n">fi</span> <span class="o">=</span> <span class="n">ax</span><span class="p">.</span><span class="n">get_figure</span><span class="p">()</span> <span class="c1"># get the figure of interest </span> <span class="c1"># attach the call back </span> <span class="n">fi</span><span class="p">.</span><span class="n">canvas</span><span class="p">.</span><span class="n">mpl_connect</span><span class="p">(</span><span class="s">'scroll_event'</span><span class="p">,</span> <span class="n">zoom_fun</span><span class="p">)</span> <span class="c1"># return the function </span> <span class="k">return</span> <span class="n">zoom_fun</span> <span class="n">zoom</span> <span class="o">=</span> <span class="n">zoom_factory</span><span class="p">(</span><span class="n">ax</span><span class="p">)</span> <span class="c1"># Plot initial data </span> <span class="k">for</span> <span class="n">index</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">lines</span><span class="p">)):</span> <span class="n">x_vec</span><span class="p">,</span> <span class="n">y_vec</span><span class="p">,</span> <span class="n">skip</span> <span class="o">=</span> <span class="n">get_coordinate_lists</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">index</span><span class="p">)</span> <span class="n">lines</span><span class="p">[</span><span class="n">index</span><span class="p">]</span> <span class="o">=</span> <span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">x_vec</span><span class="p">,</span> <span class="n">y_vec</span><span class="p">,</span> <span class="n">formats</span><span class="p">[</span><span class="n">index</span><span class="p">],</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="n">labels</span><span class="p">[</span><span class="n">index</span><span class="p">])</span> <span class="n">ax</span><span class="p">.</span><span class="n">legend</span><span class="p">(</span><span class="n">loc</span><span class="o">=</span><span class="s">'upper right'</span><span class="p">)</span> <span class="n">plt</span><span class="p">.</span><span class="n">xlabel</span><span class="p">(</span><span class="n">xlabel</span><span class="p">)</span> <span class="n">plt</span><span class="p">.</span><span class="n">ylabel</span><span class="p">(</span><span class="n">ylabel</span><span class="p">)</span> <span class="n">plt</span><span class="p">.</span><span class="n">title</span><span class="p">(</span><span class="n">title</span><span class="p">)</span> <span class="n">plt</span><span class="p">.</span><span class="n">gcf</span><span class="p">().</span><span class="n">subplots_adjust</span><span class="p">(</span><span class="n">bottom</span><span class="o">=</span><span class="mf">0.15</span><span class="p">)</span> <span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span> <span class="c1"># Update the line graph </span><span class="k">def</span> <span class="nf">live_plotter_update</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">lines</span><span class="p">,</span> <span class="n">pause_time</span><span class="o">=</span><span class="mf">0.01</span><span class="p">,</span> <span class="n">max_points_to_show</span><span class="o">=</span><span class="mi">10</span><span class="p">):</span> <span class="c1"># All x_vec and y_vec lists, used to set the bounds of the graph </span> <span class="n">x_vecs</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">y_vecs</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">last_x_vec</span> <span class="o">=</span> <span class="bp">None</span> <span class="c1"># Store the last x_vec, in full (not only the last max_points_to_show points), for time labeling </span> <span class="n">time_list_strings</span> <span class="o">=</span> <span class="bp">None</span> <span class="k">for</span> <span class="n">index</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">lines</span><span class="p">)):</span> <span class="n">x_vec</span><span class="p">,</span> <span class="n">y_vec</span><span class="p">,</span> <span class="n">list_strings</span> <span class="o">=</span> <span class="n">get_coordinate_lists</span><span class="p">(</span><span class="n">data_df</span><span class="p">,</span> <span class="n">index</span><span class="p">)</span> <span class="n">lines</span><span class="p">[</span><span class="n">index</span><span class="p">][</span><span class="mi">0</span><span class="p">].</span><span class="n">set_data</span><span class="p">(</span><span class="n">x_vec</span><span class="p">,</span> <span class="n">y_vec</span><span class="p">)</span> <span class="c1"># Add to x_vecs and y_vecs </span> <span class="n">x_vecs</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">x_vec</span><span class="p">[</span><span class="o">-</span><span class="n">max_points_to_show</span><span class="p">:])</span> <span class="n">y_vecs</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">y_vec</span><span class="p">[</span><span class="o">-</span><span class="n">max_points_to_show</span><span class="p">:])</span> <span class="c1"># Override time_list_strings </span> <span class="n">time_list_strings</span> <span class="o">=</span> <span class="n">list_strings</span> <span class="c1"># Override last_x_vec, so the time labels are properly applied to all points, not just those visible </span> <span class="n">last_x_vec</span> <span class="o">=</span> <span class="n">x_vec</span> <span class="k">if</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="k">return</span> <span class="c1"># Exit program early if closed </span> <span class="c1"># Do not adjust bounds if panning because it will send them back to the original view </span> <span class="k">if</span> <span class="ow">not</span> <span class="n">panning_allowed</span><span class="p">:</span> <span class="c1"># Adjust the bounds to fit all the lines on the screen and only show at most max_points_to_show at once </span> <span class="c1"># Find the smallest and largest x values (in the last max_points_to_show of each x_vec in x_vecs) </span> <span class="n">smallest_x</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">min</span><span class="p">(</span><span class="n">x_vecs</span><span class="p">)</span> <span class="n">largest_x</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">max</span><span class="p">(</span><span class="n">x_vecs</span><span class="p">)</span> <span class="c1"># Find the smallest and largest y values (in the last max_points_to_show of each y_vec in y_vecs) </span> <span class="n">smallest_y</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">min</span><span class="p">(</span><span class="n">y_vecs</span><span class="p">)</span> <span class="n">largest_y</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">max</span><span class="p">(</span><span class="n">y_vecs</span><span class="p">)</span> <span class="c1"># Update the x axis to use time_list_strings instead of values in seconds for easier reading (HH:MM:SS format) </span> <span class="n">plt</span><span class="p">.</span><span class="n">xticks</span><span class="p">(</span><span class="n">last_x_vec</span><span class="p">,</span> <span class="n">time_list_strings</span><span class="p">,</span> <span class="n">rotation</span><span class="o">=-</span><span class="mi">45</span><span class="p">,</span> <span class="n">ha</span><span class="o">=</span><span class="s">"left"</span><span class="p">,</span> <span class="n">rotation_mode</span><span class="o">=</span><span class="s">"anchor"</span><span class="p">)</span> <span class="c1"># Adjust the bounds to be a fraction of the standard deviation past the max and min points, to keep space </span> <span class="c1"># between the points and the borders </span> <span class="n">plt</span><span class="p">.</span><span class="n">xlim</span><span class="p">(</span><span class="n">smallest_x</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="n">std</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">x_vecs</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">))</span> <span class="o">/</span> <span class="mi">3</span><span class="p">,</span> <span class="n">largest_x</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">std</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">x_vecs</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">))</span> <span class="o">/</span> <span class="mi">3</span><span class="p">)</span> <span class="n">plt</span><span class="p">.</span><span class="n">ylim</span><span class="p">(</span><span class="n">smallest_y</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="n">std</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">y_vecs</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">))</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="n">largest_y</span> <span class="o">+</span> <span class="n">np</span><span class="p">.</span><span class="n">std</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">y_vecs</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">float32</span><span class="p">))</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> <span class="k">if</span> <span class="n">has_been_closed</span><span class="p">():</span> <span class="k">return</span> <span class="c1"># Exit program early if closed </span> <span class="n">plt</span><span class="p">.</span><span class="n">pause</span><span class="p">(</span><span class="n">pause_time</span><span class="p">)</span> </code></pre></div></div> Fri, 02 Aug 2019 14:00:00 +0000 https://ivanovi.ch/blog/2019/08/ea-day-5/ https://ivanovi.ch/blog/2019/08/ea-day-5/ blog posts building energy boot camp 2019 Building Energy Boot Camp 2019 - Day 4 <p>Today, we spent the entirety of the boot camp session working on the project we were assigned <a href="/blog/2019/ea-day-3">yesterday</a>. I managed to finish the gallons per day (GPD) calculations and output the number of houses with GPDs higher than 300, 500, and 1000. I also wrote code to calculate the GPD of each street in Andover and write this, along with the GPD data for each house, into .csv files and Excel spreadsheets. However, as this code dealt with such a vast amount of data, I quickly noticed its long run time. Thus, I spent much of the work time optimizing my code. First, I made sure to drop any and all unnecessary columns, to greatly increase the total amount of cells in the <a href="https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html">pandas DataFrame</a>. I also discovered <a href="https://cython.org">cython</a>, a tool that compiles Python code into C code, which runs many times faster than Python. Using cython, I managed to greatly decrease run times, but was still unable to bring them under 80 seconds. I have included the code and its output in the <em>Code and Output</em> section.</p> <h2 id="images">Images</h2> <p><img src="/images/blog post images/EA Bootcamps/2019/4/Household GPD.png" alt="Output Spreadsheet (Household)" class="centered-image" /></p> <center>The output spreadsheet displaying GPD per household (censored for privacy). Note the different sheets, <em>Household GPD</em> and <em>Street GPD</em>.</center> <p><br /></p> <p><img src="/images/blog post images/EA Bootcamps/2019/4/Street GPD.png" alt="Output Spreadsheet (Street)" class="centered-image" /></p> <center>The output spreadsheet displaying GPD per street (censored for privacy).</center> <p><br /></p> <h2 id="code-and-output">Code and Output</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span> <span class="kn">import</span> <span class="nn">time</span> <span class="kn">import</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span> <span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span> <span class="kn">import</span> <span class="nn">datetime</span> <span class="k">as</span> <span class="n">dt</span> <span class="c1"># Used to record elapsed time </span><span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="c1"># Define unit constants </span><span class="n">CUBE_IN_PER_GALLON</span> <span class="o">=</span> <span class="mi">231</span> <span class="n">CUBE_IN_PER_CUBIC_FT</span> <span class="o">=</span> <span class="mi">1728</span> <span class="c1"># The default length of time between 'prior_date' and 'current_date' if incomplete data is given </span><span class="n">DEFAULT_DAYS_BETWEEN</span> <span class="o">=</span> <span class="mi">90</span> <span class="c1"># The path to the sqlite file </span><span class="n">db_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">'SQLite'</span><span class="p">,</span> <span class="s">'student.sqlite'</span><span class="p">)</span> <span class="n">engine</span> <span class="o">=</span> <span class="n">sqlalchemy</span><span class="p">.</span><span class="n">create_engine</span><span class="p">(</span><span class="s">'sqlite:///'</span> <span class="o">+</span> <span class="n">db_path</span><span class="p">)</span> <span class="c1"># Read the sqlite file into sqlalchemy </span> <span class="c1"># Read the Water table into a dataframe </span><span class="n">df_water</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_sql_table</span><span class="p">(</span><span class="s">'Water'</span><span class="p">,</span> <span class="n">engine</span><span class="p">,</span> <span class="n">parse_dates</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="c1"># Delete unnecessary columns </span><span class="n">df_water</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">.</span><span class="n">drop</span><span class="p">(</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s">'service_id'</span><span class="p">,</span> <span class="s">'account_number'</span><span class="p">,</span> <span class="s">'service'</span><span class="p">,</span> <span class="s">'meter_number'</span><span class="p">,</span> <span class="s">'transaction_date'</span><span class="p">,</span> <span class="s">'transaction_type'</span><span class="p">,</span> <span class="s">'units'</span><span class="p">,</span> <span class="s">'description'</span><span class="p">,</span> <span class="s">'id'</span><span class="p">])</span> <span class="c1"># Convert the 'current_reading' column into floats </span><span class="n">df_water</span><span class="p">[</span><span class="s">'current_reading'</span><span class="p">]</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">[</span><span class="s">'current_reading'</span><span class="p">].</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span> <span class="c1"># Delete rows with 0 as a value for 'current_reading' </span><span class="n">df_water</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">[</span><span class="n">df_water</span><span class="p">[</span><span class="s">'current_reading'</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">]</span> <span class="c1"># Convert the 'prior_date' and 'current_date' into datetime or None </span><span class="k">def</span> <span class="nf">convert_to_datetime</span><span class="p">(</span><span class="n">s</span><span class="p">):</span> <span class="k">if</span> <span class="n">s</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="k">return</span> <span class="bp">None</span> <span class="k">try</span><span class="p">:</span> <span class="k">return</span> <span class="n">dt</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">strptime</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">'%Y-%m-%d %H:%M:%S'</span><span class="p">)</span> <span class="k">except</span> <span class="nb">ValueError</span><span class="p">:</span> <span class="k">return</span> <span class="bp">None</span> <span class="n">df_water</span><span class="p">[</span><span class="s">'prior_date'</span><span class="p">]</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">[</span><span class="s">'prior_date'</span><span class="p">].</span><span class="nb">apply</span><span class="p">(</span><span class="n">convert_to_datetime</span><span class="p">)</span> <span class="n">df_water</span><span class="p">[</span><span class="s">'current_date'</span><span class="p">]</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">[</span><span class="s">'current_date'</span><span class="p">].</span><span class="nb">apply</span><span class="p">(</span><span class="n">convert_to_datetime</span><span class="p">)</span> <span class="c1"># Create an empty column, filled with 0s by default, to store gpd for the time period # Data will be added to the column as it is calculated </span><span class="n">gpd_list</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span> <span class="k">for</span> <span class="n">counter</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">df_water</span><span class="p">.</span><span class="n">index</span><span class="p">))]</span> <span class="n">df_water</span><span class="p">[</span><span class="s">'GPD'</span><span class="p">]</span> <span class="o">=</span> <span class="n">gpd_list</span> <span class="c1"># Converts a numpy.datetime64 to a datetime.datetime object by converting dt64 to UTC time (for later use) </span><span class="k">def</span> <span class="nf">datetime64_to_datetime</span><span class="p">(</span><span class="n">dt64</span><span class="p">):</span> <span class="k">return</span> <span class="n">dt</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">utcfromtimestamp</span><span class="p">((</span><span class="n">dt64</span> <span class="o">-</span> <span class="n">np</span><span class="p">.</span><span class="n">datetime64</span><span class="p">(</span><span class="s">'1970-01-01T00:00:00Z'</span><span class="p">))</span> <span class="o">/</span> <span class="n">np</span><span class="p">.</span><span class="n">timedelta64</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s">'s'</span><span class="p">))</span> <span class="c1"># Convert values from cubic ft to gallons </span><span class="n">df_water</span><span class="p">[</span><span class="s">'current_reading'</span><span class="p">]</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">[</span><span class="s">'current_reading'</span><span class="p">].</span><span class="nb">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span> <span class="o">*</span> <span class="n">CUBE_IN_PER_CUBIC_FT</span> <span class="o">/</span> <span class="n">CUBE_IN_PER_GALLON</span><span class="p">)</span> <span class="c1"># Create a dataframe to store the average GPD for each house, but leave it blank </span><span class="n">df_base_dictionary</span> <span class="o">=</span> <span class="p">{</span><span class="s">'address_street_number'</span><span class="p">:</span> <span class="p">[],</span> <span class="s">'address_street_name'</span><span class="p">:</span> <span class="p">[],</span> <span class="s">'GPD'</span><span class="p">:</span> <span class="p">[]}</span> <span class="n">df_gpd</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">df_base_dictionary</span><span class="p">)</span> <span class="c1"># Mark the 'GPD' column as floats </span><span class="n">df_gpd</span><span class="p">[</span><span class="s">'GPD'</span><span class="p">]</span> <span class="o">=</span> <span class="n">df_gpd</span><span class="p">[</span><span class="s">'GPD'</span><span class="p">].</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">'Building table of average gallons used per day (average GPD)...'</span><span class="p">)</span> <span class="c1"># Iterate through every unique street </span><span class="n">street_names</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">[</span><span class="s">'address_street_name'</span><span class="p">].</span><span class="n">unique</span><span class="p">()</span> <span class="k">for</span> <span class="n">street</span> <span class="ow">in</span> <span class="n">street_names</span><span class="p">:</span> <span class="c1"># Get the list of all house numbers on that street </span> <span class="n">house_numbers</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">[</span><span class="n">df_water</span><span class="p">[</span><span class="s">'address_street_name'</span><span class="p">]</span> <span class="o">==</span> <span class="n">street</span><span class="p">][</span><span class="s">'address_street_number'</span><span class="p">].</span><span class="n">unique</span><span class="p">()</span> <span class="c1"># Create a row for each house </span> <span class="k">for</span> <span class="n">house</span> <span class="ow">in</span> <span class="n">house_numbers</span><span class="p">:</span> <span class="n">df_gpd</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">df_gpd</span><span class="p">)]</span> <span class="o">=</span> <span class="p">[</span><span class="n">house</span><span class="p">,</span> <span class="n">street</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span> <span class="k">print</span><span class="p">(</span><span class="s">"Done!</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"Calculating all average GPDs..."</span><span class="p">)</span> <span class="c1"># Go through every house and calculate the GPD </span><span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df_gpd</span><span class="p">.</span><span class="n">iterrows</span><span class="p">():</span> <span class="c1"># Grab the portion of the df_water that has to do with this house </span> <span class="n">df</span> <span class="o">=</span> <span class="n">df_water</span><span class="p">[</span><span class="n">df_water</span><span class="p">[</span><span class="s">'address_street_name'</span><span class="p">]</span> <span class="o">==</span> <span class="n">row</span><span class="p">[</span><span class="s">'address_street_name'</span><span class="p">]]</span> <span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="n">df</span><span class="p">[</span><span class="s">'address_street_number'</span><span class="p">]</span> <span class="o">==</span> <span class="n">row</span><span class="p">[</span><span class="s">'address_street_number'</span><span class="p">]]</span> <span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">sort_values</span><span class="p">(</span><span class="n">by</span><span class="o">=</span><span class="s">'prior_date'</span><span class="p">)</span> <span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">reset_index</span><span class="p">(</span><span class="n">drop</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">for</span> <span class="n">indx</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">index</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span> <span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">drop</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="n">indx</span><span class="p">)</span> <span class="c1"># Drop all but the first and last rows </span> <span class="n">df</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">reset_index</span><span class="p">(</span><span class="n">drop</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="n">difference_in_gallons</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="p">:][</span><span class="s">'current_reading'</span><span class="p">]</span> <span class="o">-</span> <span class="n">df</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:][</span><span class="s">'current_reading'</span><span class="p">]</span> <span class="n">start_date</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:][</span><span class="s">'current_date'</span><span class="p">]</span> <span class="n">end_date</span> <span class="o">=</span> <span class="n">df</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="p">:][</span><span class="s">'current_date'</span><span class="p">]</span> <span class="c1"># If the start_date is valid, convert it into a datetime </span> <span class="k">if</span> <span class="ow">not</span> <span class="n">pd</span><span class="p">.</span><span class="n">isnull</span><span class="p">(</span><span class="n">start_date</span><span class="p">):</span> <span class="n">start_date</span> <span class="o">=</span> <span class="n">datetime64_to_datetime</span><span class="p">(</span><span class="n">start_date</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="c1"># No start_date, use 90 days after the prior_date </span> <span class="n">start_date</span> <span class="o">=</span> <span class="n">datetime64_to_datetime</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:][</span><span class="s">'prior_date'</span><span class="p">])</span> <span class="o">+</span> <span class="n">dt</span><span class="p">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">90</span><span class="p">)</span> <span class="c1"># If the end_date is valid, convert it into a datetime </span> <span class="k">if</span> <span class="ow">not</span> <span class="n">pd</span><span class="p">.</span><span class="n">isnull</span><span class="p">(</span><span class="n">end_date</span><span class="p">):</span> <span class="n">end_date</span> <span class="o">=</span> <span class="n">datetime64_to_datetime</span><span class="p">(</span><span class="n">end_date</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="c1"># No end date, use 90 days after the prior_date </span> <span class="n">end_date</span> <span class="o">=</span> <span class="n">datetime64_to_datetime</span><span class="p">(</span><span class="n">df</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="p">:][</span><span class="s">'prior_date'</span><span class="p">])</span> <span class="o">+</span> <span class="n">dt</span><span class="p">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">90</span><span class="p">)</span> <span class="n">days_between</span> <span class="o">=</span> <span class="p">(</span><span class="n">end_date</span> <span class="o">-</span> <span class="n">start_date</span><span class="p">).</span><span class="n">days</span> <span class="n">gallons_per_day</span> <span class="o">=</span> <span class="n">difference_in_gallons</span> <span class="o">/</span> <span class="n">days_between</span> <span class="n">df_gpd</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="n">index</span><span class="p">,</span> <span class="s">'GPD'</span><span class="p">]</span> <span class="o">=</span> <span class="n">gallons_per_day</span> <span class="k">print</span><span class="p">(</span><span class="s">"Done!</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"Sorting table by street name, then by house number"</span><span class="p">)</span> <span class="n">df_gpd</span> <span class="o">=</span> <span class="n">df_gpd</span><span class="p">.</span><span class="n">sort_values</span><span class="p">(</span><span class="n">by</span><span class="o">=</span><span class="p">[</span><span class="s">'address_street_name'</span><span class="p">,</span> <span class="s">'address_street_number'</span><span class="p">])</span> <span class="k">print</span><span class="p">(</span><span class="s">"Done!</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"Creating table of street GPDs..."</span><span class="p">)</span> <span class="c1"># Create a dataframe for each street </span><span class="n">street_gpd_base_dict</span> <span class="o">=</span> <span class="p">{</span><span class="s">'street'</span><span class="p">:</span> <span class="p">[],</span> <span class="s">'GPD'</span><span class="p">:</span> <span class="p">[]}</span> <span class="n">street_gpd</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">street_gpd_base_dict</span><span class="p">)</span> <span class="k">for</span> <span class="n">street</span> <span class="ow">in</span> <span class="n">street_names</span><span class="p">:</span> <span class="n">house_data</span> <span class="o">=</span> <span class="n">df_gpd</span><span class="p">[</span><span class="n">df_gpd</span><span class="p">[</span><span class="s">'address_street_name'</span><span class="p">]</span> <span class="o">==</span> <span class="n">street</span><span class="p">]</span> <span class="n">gpd</span> <span class="o">=</span> <span class="n">house_data</span><span class="p">[</span><span class="s">'GPD'</span><span class="p">].</span><span class="nb">sum</span><span class="p">()</span> <span class="n">street_gpd</span><span class="p">.</span><span class="n">loc</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">street_gpd</span><span class="p">)]</span> <span class="o">=</span> <span class="p">[</span><span class="n">street</span><span class="p">,</span> <span class="n">gpd</span><span class="p">]</span> <span class="n">street_gpd</span> <span class="o">=</span> <span class="n">street_gpd</span><span class="p">.</span><span class="n">sort_values</span><span class="p">(</span><span class="n">by</span><span class="o">=</span><span class="p">[</span><span class="s">'street'</span><span class="p">])</span> <span class="k">print</span><span class="p">(</span><span class="s">"Done!</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span> <span class="n">elapsed_time</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span> <span class="c1"># Round elapsed time to 2 decimal places </span><span class="k">print</span><span class="p">(</span><span class="s">"Elapsed time: {0} seconds"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="nb">round</span><span class="p">(</span><span class="n">elapsed_time</span> <span class="o">*</span> <span class="mi">100</span><span class="p">)</span> <span class="o">/</span> <span class="mi">100</span><span class="p">))</span> <span class="c1"># Returns the number of houses with more than min_gpd gpd </span><span class="k">def</span> <span class="nf">count_by_min_gpd</span><span class="p">(</span><span class="n">min_gpd</span><span class="p">):</span> <span class="n">temp_df</span> <span class="o">=</span> <span class="n">df_gpd</span><span class="p">[</span><span class="n">df_gpd</span><span class="p">[</span><span class="s">'GPD'</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">min_gpd</span><span class="p">]</span> <span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="n">temp_df</span><span class="p">.</span><span class="n">index</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">'</span><span class="se">\n</span><span class="s">{0} households have an average GPD greater than 300'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">count_by_min_gpd</span><span class="p">(</span><span class="mi">300</span><span class="p">)))</span> <span class="k">print</span><span class="p">(</span><span class="s">'{0} households have an average GPD greater than 500'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">count_by_min_gpd</span><span class="p">(</span><span class="mi">500</span><span class="p">)))</span> <span class="k">print</span><span class="p">(</span><span class="s">'{0} households have an average GPD greater than 1000</span><span class="se">\n</span><span class="s">'</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">count_by_min_gpd</span><span class="p">(</span><span class="mi">1000</span><span class="p">)))</span> <span class="k">print</span><span class="p">(</span><span class="s">"Saving GPD data to 'household_gpd.csv'..."</span><span class="p">)</span> <span class="n">df_gpd</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">'household_gpd.csv'</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">'Done!</span><span class="se">\n</span><span class="s">'</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"Saving street GPD data to 'streets.csv'..."</span><span class="p">)</span> <span class="n">street_gpd</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">'streets.csv'</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">'Done!</span><span class="se">\n</span><span class="s">'</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"Saving all GPD data to 'gpd.xlsx'..."</span><span class="p">)</span> <span class="c1"># Write to Excel sheet </span><span class="k">with</span> <span class="n">pd</span><span class="p">.</span><span class="n">ExcelWriter</span><span class="p">(</span><span class="s">'gpd.xlsx'</span><span class="p">)</span> <span class="k">as</span> <span class="n">writer</span><span class="p">:</span> <span class="n">df_gpd</span><span class="p">.</span><span class="n">to_excel</span><span class="p">(</span><span class="n">writer</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">sheet_name</span><span class="o">=</span><span class="s">'Household GPD'</span><span class="p">)</span> <span class="n">street_gpd</span><span class="p">.</span><span class="n">to_excel</span><span class="p">(</span><span class="n">writer</span><span class="p">,</span> <span class="n">index</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">sheet_name</span><span class="o">=</span><span class="s">'Street GPD'</span><span class="p">)</span> <span class="k">print</span><span class="p">(</span><span class="s">"Done!"</span><span class="p">)</span> </code></pre></div></div> <center><em>main.py</em>, the program that loads the database's into pandas and performs the GPD calculations</center> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Building table of average gallons used per day (average GPD)... Done! Calculating all average GPDs... Done! Sorting table by street name, then by house number Done! Creating table of street GPDs... Done! Elapsed time: 86.3 seconds 403 households have an average GPD greater than 300 94 households have an average GPD greater than 500 14 households have an average GPD greater than 1000 Saving GPD data to 'household_gpd.csv'... Done! Saving street GPD data to 'streets.csv'... Done! Saving all GPD data to 'gpd.xlsx'... Done! </code></pre></div></div> <center>A sample output from <em>main.py</em></center> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span> <span class="kn">from</span> <span class="nn">distutils.core</span> <span class="kn">import</span> <span class="n">setup</span> <span class="kn">from</span> <span class="nn">Cython.Build</span> <span class="kn">import</span> <span class="n">cythonize</span> <span class="n">setup</span><span class="p">(</span><span class="n">ext_modules</span><span class="o">=</span><span class="n">cythonize</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="s">'SQLite'</span><span class="p">,</span> <span class="s">'main.py'</span><span class="p">)))</span> </code></pre></div></div> <center><em>cython-setup.py</em>, which is used by Cython to run <em>main.py</em>.<br />I run the code by running</center> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3.6 cython-setup.py build_ext <span class="nt">--inplace</span> </code></pre></div></div> <center>in the terminal and then calling</center> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">SQLite.main</span> </code></pre></div></div> <center>in a python terminal</center> Thu, 01 Aug 2019 14:00:00 +0000 https://ivanovi.ch/blog/2019/08/ea-day-4/ https://ivanovi.ch/blog/2019/08/ea-day-4/ blog posts building energy boot camp 2019 Building Energy Boot Camp 2019 - Day 3 <p>Today, we learned about <a href="https://www.sqlite.org/index.html">SQLite</a> and the <a href="https://sqlitebrowser.org/">SQLite Database Browser</a>. We were given a database containing several tables full of town data of every type, from census results to quarterly water usage. After learning about how to utilize the <a href="https://www.sqlalchemy.org/">sqlalchemy</a> and <a href="https://pandas.pydata.org/">pandas</a> packages together to load tables from the database as <a href="https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html">dataframes</a>. Armed with this knowledge, we were assigned a new project, in which we must calculate how many houses use, on average, more than 300 gallons of water per day. The difficulty in this project lies in the calculation of the gallons per day, as only the total amount of water used, in cubic feet, is stored in the database and assigned to a certain date range. This means that to calculate how many gallons a house uses per day, all of the rows corresponding to that house must be found (ranging from 2 to 6 rows), the total number of days accounted for must be calculated using the two date columns, <code class="language-plaintext highlighter-rouge">prior_date</code> and <code class="language-plaintext highlighter-rouge">current_date</code>, and this total, along with the converted difference between the largest and smallest measurements, will give the average GPD (gallons per day).</p> <p>Unfortunately, I was unable to finish the assignment today, and will do my best to finish it tomorrow.</p> Wed, 31 Jul 2019 14:00:00 +0000 https://ivanovi.ch/blog/2019/07/ea-day-3/ https://ivanovi.ch/blog/2019/07/ea-day-3/ blog posts building energy boot camp 2019