<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>David Swanlund</title>
    <description>David is a software developer and GIScience researcher who focuses on geospatial privacy issues.</description>
    <link>http://swanlund.dev/</link>
    <atom:link href="http://swanlund.dev/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Mon, 25 Dec 2023 18:50:55 +0000</pubDate>
    <lastBuildDate>Mon, 25 Dec 2023 18:50:55 +0000</lastBuildDate>
    <generator>Jekyll v3.9.3</generator>
    
      <item>
        <title>Upgrading My Home Server: A Build Log</title>
        <description>&lt;p&gt;Over the past three weeks I’ve been completely overhauling my home server setup, and I’ve learned a lot throughout the process that I thought I’d document it here in a build log.&lt;/p&gt;

&lt;p&gt;For context, I’ve had my current home-server since 2015. It’s a low-powered i3-4160 running 32GB of ECC memory, with 27TB of usable storage spread across a hodge-podge of 3TB, 4TB, and 8TB drives all jammed into a cheap Rosewill 4U chassis. Until 2020 it was running FreeNAS, but like many others I made the switch over to Unraid and never looked back.&lt;/p&gt;

&lt;p&gt;Unraid was great for me, as its seamless Docker integration allowed me to add significant functionality to what was basically just a file server with Plex. With Unraid I was able to start self-hosting a lot of additional services, like Bookstack for keeping notes organized, a Unifi controller to manage my wifi access points, Teedy to organize my important documents, a few containers for archiving podcasts and Youtube channels, etc.&lt;/p&gt;

&lt;p&gt;With this growth in functionality, however, my poor little i3 was struggling to keep up. I had bought it initially because it was cheap and supported ECC memory, a feature you otherwise have to go Xeon to get. And so for a while now I’ve been looking to upgrade.&lt;/p&gt;

&lt;p&gt;Between some unexpected income, some timely sales, and some other PC upgrades I’ve done around the house, the time to upgrade has finally arrived. It certainly wasn’t cheap, but by shuffling around some components between my existing systems, I was able to not only build a significantly upgraded home server, but squeeze out an offsite backup as well! Here’s how it came together.&lt;/p&gt;

&lt;h2 id=&quot;the-specs&quot;&gt;The Specs&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;: &lt;a href=&quot;https://www.amd.com/en/products/cpu/amd-ryzen-5-3600&quot;&gt;Ryzen 3600&lt;/a&gt;. This was pulled straight from my gaming PC, which I just upgraded to a 5900x. It gives me 6 hyperthreaded cores (e.g. 12 threads) of computational goodness. It’s not the fastest chip out there, but it’s certainly more than enough for a home server, supports ECC memory (more on this later), and doesn’t suck back a tonne of power.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cooler&lt;/strong&gt;: &lt;a href=&quot;https://www.amazon.ca/gp/product/B01N9X2YYN/ref=ppx_yo_dt_b_asin_title_o05_s00?ie=UTF8&amp;amp;psc=1&quot;&gt;Noctua NH-U12S&lt;/a&gt;. Can’t go wrong with Noctua, and at this point we’re a Noctua household. I probably could have gotten away with the stock cooler, but given that this system will run 24/7/365 I wanted to keep the CPU as frosty as possible.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Motherboard&lt;/strong&gt;: &lt;a href=&quot;https://www.amazon.ca/gp/product/B088VSTS9H/ref=ppx_yo_dt_b_asin_title_o04_s01?ie=UTF8&amp;amp;psc=1&quot;&gt;Asus B550-F&lt;/a&gt;. Solid board with exceptional &lt;a href=&quot;https://linustechtips.com/topic/1137619-motherboard-vrm-tier-list-v2-currently-amd-only/&quot;&gt;power delivery&lt;/a&gt;. Bonus is that it has 2.5Gb ethernet built right in, which goes well with the &lt;a href=&quot;https://www.amazon.ca/gp/product/B08XWK4HNT/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&amp;amp;psc=1&quot;&gt;2.5Gb switch&lt;/a&gt; I just picked up. This puts my whole home network at speeds fast enough to saturate the drives without having to fork over big money for 10Gb.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt;: &lt;a href=&quot;https://www.amazon.ca/gp/product/B08KTT4867/ref=ppx_yo_dt_b_asin_title_o09_s01?ie=UTF8&amp;amp;psc=1&quot;&gt;Timetec 2x16GB ECC&lt;/a&gt;. A bit slow and boring without any RGB, but this is a server dammit!&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Power Supply&lt;/strong&gt;: &lt;a href=&quot;https://www.newegg.ca/seasonic-focus-plus-650-gold-ssr-650fx-650w/p/N82E16817151186?Item=N82E16817151186&quot;&gt;Seasonic Focus GX-650W 80+ Gold&lt;/a&gt;. Like the motherboard, this gives great power delivery and comes with a 10 year warranty.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Case&lt;/strong&gt;: &lt;a href=&quot;https://www.newegg.ca/black-fractal-design-define-7-xl-atx-full-tower/p/N82E16811352120?Description=define%207%20xl&amp;amp;cm_re=define_7%20xl-_-11-352-120-_-Product&quot;&gt;Fractal Define 7 XL&lt;/a&gt;. An absolute chonker of a  unit with the ability to store &lt;strong&gt;18&lt;/strong&gt; hard drives. Clean aesthetic as well with sound dampening to keep things quiet. I made sure to buy lots of extra hard drive sleds so that I can keep adding drives long after Fractal drops support for it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Host Bus Adapter&lt;/strong&gt;: &lt;a href=&quot;https://www.ebay.ca/itm/163563898712&quot;&gt;LSI 9210-8i&lt;/a&gt; (used off Ebay). The motherboard I bought only has 6 SATA ports, so this essentially allows me to add 8 extra hard drives using a couple of SAS to SATA breakout cables&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;GPU&lt;/strong&gt;: &lt;a href=&quot;https://www.hp.com/us-en/shop/pdp/nvidia-quadro-p400-2gb-graphics&quot;&gt;HP Quadro P400&lt;/a&gt; (used off Ebay, still waiting for it to arrive). At the time of ordering, I wasn’t sure if the motherboard I got supported headless boot. Coming in at around $120 and paired with a &lt;a href=&quot;https://www.amazon.ca/gp/product/B08C7XPZX2/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&amp;amp;psc=1&quot;&gt;dummy plug&lt;/a&gt;, this rather inexpensive GPU solves that problem and while also helping significantly with Plex transcoding, all without drawing much extra power.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Hard Drives&lt;/strong&gt;: &lt;a href=&quot;https://www.newegg.ca/seagate-ironwolf-st8000vn004-8tb/p/N82E16822184796?Item=N82E16822184796&quot;&gt;3x8TB Seagate Ironwolfs&lt;/a&gt; as well as 3x8TB WD Reds I had from the old server. Nothing special here, just some standard CMR NAS drives.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cache Drives&lt;/strong&gt;: &lt;a href=&quot;https://www.amazon.ca/Samsung-Internal-MZ-76E1T0B-AM-Version/dp/B078DPCY3T/ref=sr_1_3?crid=38HHMBF9QSKBW&amp;amp;keywords=860+evo&amp;amp;qid=1636162336&amp;amp;s=electronics&amp;amp;sprefix=860+evo%2Celectronics%2C123&amp;amp;sr=1-3&quot;&gt;2x1TB Samsung 860 Evo SSDs&lt;/a&gt;. I actually got these a couple years ago, but these are relatively affordable and have decent write endurance at 600TBW, so they’ll do just fine as a cache for Unraid.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Blu-Ray Drive&lt;/strong&gt;: &lt;a href=&quot;https://www.amazon.ca/gp/product/B00E7B08MS/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;amp;psc=1&quot;&gt;LG WH16NS40&lt;/a&gt;. This actually came from my desktop PC, as the new case I got for it didn’t support an optical drive. I’ll be feeding it into a Windows VM so that I can archive data using BD-R HTL discs (which contrary to popular opinion are great for archival purposes due to their use of inorganic dyes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using Unraid this gives me 32TB of usable storage, with the ability to lose two drives from the pool without losing any data. I also shucked an old external 10TB drive and bought a &lt;a href=&quot;https://www.newegg.ca/toshiba-mg07aca12te-12tb/p/1B0-0011-000E7?Item=1B0-0011-000E7&quot;&gt;12TB Toshiba Enterpise drive&lt;/a&gt; to add to all the 3TB and 4TB drives on my old server. This left me with 29TB of usable offsite storage, with the 12TB drive being used for parity. I went with a slightly larger 12TB drive as the parity so that when all those 6-year-old 3TB drives start drppping like flies I can replace several of them with a single drive and cut down the overall amount of rust I have spinning at once.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/servergore.jpg&quot; alt=&quot;Obligatory photo of the inside of the case&quot; /&gt;
(I’ve got an old Nvidia 660ti in there right now while I wait for the P400 to arrive)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/harddrives.jpg&quot; alt=&quot;Excute the horrendous cable management&quot; /&gt;
(Excuse the horrendous cable management, but it seems wrong to include photos without showing the hard drives.)&lt;/p&gt;

&lt;h2 id=&quot;the-problems&quot;&gt;The Problems&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. ECC Memory:&lt;/strong&gt; I usually love building PCs, but this one was straight up &lt;em&gt;stressful&lt;/em&gt;. Let’s start with the memory. Ryzen desktop CPUs are wonderful because they support ECC memory, whereas Intel likes to lock that feature down to their more costly Xeon line. Great, right? Well, it’s not that simple. While ECC is technically supported by the chip, the implementation on the motherboard supply is… inconsistent. There’s lots of mixed information out there about whether particular motherboards actually support ECC with Ryzen, and that’s muddied even more by the fact that many motherboards have the ECC &lt;em&gt;silently&lt;/em&gt; correcting errors without reporting them to the operating system. This isn’t great, since your RAM may be failing and correcting lots of errors, but you’d never know. In short, ECC seems great on Ryzen, but in practice is an absolute shitshow.&lt;/p&gt;

&lt;p&gt;So after banging my head against the keyboard, I finally decided to just plunk the ECC memory that had already arrived into my partner’s new Asus B550-f motherboard. Unfortuantely, she was running a 5700G which &lt;em&gt;does not&lt;/em&gt; support ECC, but the motherboard BIOS nevertheless had an option for enabling ECC. So I took a leap of faith and ordered the same board from Amazon, knowing I could return it if I had to. Once it arrived, I booted up &lt;a href=&quot;https://www.memtest86.com/&quot;&gt;memtest&lt;/a&gt; and literally started dancing when I saw that it reported ECC polling enabled!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. LSI &amp;amp; Ironwolf:&lt;/strong&gt; So I built the server, spent a day getting 18TB of data transferred over, and was in the process of moving a few hard drives from the old server to the new one. This involved recalculating parity a couple of times as I added the drives, which is always a bit nerve-racking. Already anxious, my stomach then dropped like a rock when Unraid gave me an error message saying that one of my drives had reported read-errors and had been taken offline. &lt;em&gt;Shit&lt;/em&gt;. If one more drive goes down I have to restart and move 18TB of data all over again. So I frantically start Googling and discover that as of the most recent version of Unraid, LSI controllers had started to have problems with 8TB and 10TB Ironwolf drives causing them to drop out of the array randomly. Fortunately, some users over on the &lt;a href=&quot;https://forums.unraid.net/topic/103938-69x-lsi-controllers-ironwolf-disks-disabling-summary-fix/&quot;&gt;Unraid forum had figured out a fix&lt;/a&gt; involving disabling low current spinup and EPC on the drive firmware itself. This turned out to be only about 20 minutes of work, but I certainly could have done without the stress that caused.&lt;/p&gt;

&lt;h2 id=&quot;the-backup-strategy&quot;&gt;The Backup Strategy&lt;/h2&gt;
&lt;p&gt;With those issues out of the way, I was ready to start figuring out how exactly I was going to do my backups. Up until very recently, I had used &lt;a href=&quot;https://www.duplicati.com/&quot;&gt;Duplicati&lt;/a&gt; to back up my data to &lt;a href=&quot;https://www.backblaze.com/b2/cloud-storage.html&quot;&gt;Backblaze B2&lt;/a&gt;, but there are two caveats here. The first is that at some point during the summer my Duplicati backup had corrupted and was rendered useless. My experience with Duplicati hadn’t been great so far, but losing 1.5TB of backups made me start looking elsewhere. The second issue is that I had only backed up 1.5TB of data, when I had around 18TB overall. At $5 per terabyte per month, B2 certainly isn’t expensive, but paying $90USD per month for backups was simply not an option for me. And so I had made the decision to only back up my critical data, such as photos and documents. This left a tonne of Bluray rips, GoPro footage, and other large datasets I’ve archived like various Youtube channels and &lt;a href=&quot;https://bluemaxima.org/flashpoint/downloads/&quot;&gt;Flashpoint&lt;/a&gt; at  risk of being lost entirely should something happen to my server, such as theft or fire. I often told myself that most of this data &lt;em&gt;was&lt;/em&gt; replaceable; I could always redownload Flashpoint, for instance. But the reality is that it would take me &lt;em&gt;years&lt;/em&gt; to accumulate the same collection.&lt;/p&gt;

&lt;p&gt;This is why I was so eager to get that offsite backup server. It would allow me to finally back up &lt;em&gt;all&lt;/em&gt; of my data on a nightly basis. Unfortunately, my plan was to leave it at a relatives house, but that relative only has 1TB of bandwidth a month. While I doubt my nightly backups would exceed that limit, there have been a few months here and there where I’ve added 1-2TB of data to my hoard. And so while I now had offsite storage available, I didn’t have a good way of getting data onto it. I thought about physically moving the server back and forth once in a while to update it, but knew that there was no way I’d actually follow through on that and could leave myself open to losing several months of data. So instead, I developed a three-prong strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Big(ish) Data&lt;/strong&gt;: For all of those extra-large datasets that might exceed that 1TB bandwidth cap, I’ll use an external SSD to move data over whenever I visit. Fortunately, I go there about twice a month so it should stay fairly up to date. To make this easier, I developed a Python tool called Waterlock (&lt;a href=&quot;https://github.com/TheTinHat/Waterlock&quot;&gt;Github&lt;/a&gt;) that takes care of all the hard work involved. I wrote about it &lt;a href=&quot;https://swanlund.space/waterlock&quot;&gt;just recently&lt;/a&gt;, but essentially when it’s run on the source system it will fill up the external drive with as much data as it can, and when it’s rerun on the destination it will move all that over, keeping a record of everything that’s been transferred. It also uses checksums to verify file copies, and I’m in the process of adding versioning and a few other handy features as well. This allows me to easily and incrementally move all that data without touching their bandwidth cap whatsoever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Smaller Data&lt;/strong&gt;: With the extra-large datasets taken care of, I decided to use &lt;a href=&quot;https://rsnapshot.org/&quot;&gt;rsnapshot&lt;/a&gt; for nightly backups of everything else. I had initially considered rsync, but if my home-server were to be hit by ransomware this would end up just syncing the damage over. rsnapshot, on the other hand, gives me some versioning that sovles this issue. In this case, I’ll be doing nightly versions that are held onto for 7 days, and then weekly versions afterwards that are held onto for a month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Failsafe&lt;/strong&gt;: Just to be safe, I’m also using Duplicacy (not Duplicati, whose name is annoyingly similar) to back up all of that core data to B2 every night as well, with versions gradually being pruned over the course of about 6 months. Again, this is about 1.5TB of data, so only $7.50 a month to store on B2. So far Duplicacy has been rock solid and exponentially faster than Duplicati; hopefully it is more stable and less prone to corruption as well. It &lt;em&gt;is&lt;/em&gt; paid software, but personal licenses are dirt cheap for what you get, which by the way includes deduplication, versioning, and client-side encryption. So far I’m loving it and have even bought extra licenses so that I can do more frequent backups of the computers in my house to the home-server as well. As for why I don’t use Duplicacy for backing up to the offsite server, well I want to keep at least one backup as standard, native files rather than cut into chunks, compressed, encrypted, etc. Suffice to say that corupted Duplicati backup has put the fear of god into me.&lt;/p&gt;

&lt;h2 id=&quot;the-management-solution&quot;&gt;The Management Solution&lt;/h2&gt;
&lt;p&gt;Finally, I needed some way to manage the offsite server. I’m fairly adamant about using full-disk encryption, but that means that when the server reboots I need to enter a passphrase to start it back up properly, which I can’t easily do without being on-site. I am using &lt;a href=&quot;https://www.wireguard.com/&quot;&gt;Wireguard&lt;/a&gt; to connect the on-site and off-site servers together for the rsnapshot job to run and had considered extending this to my desktop so that I could access Unraid’s management interface, but couldn’t figure out how to configure it to run exactly how I wanted without opening ports on their end. Moreover, if the server reset and I didn’t notice, it would mean backups might not run properly. So as a compromise I decided to use &lt;a href=&quot;https://www.youtube.com/watch?v=TSlHEBR1yfY&quot;&gt;SpaceInvaderOne’s tutorial&lt;/a&gt; for automatically fetching a keyfile when Unraid boots, only saving it to memory, and using that to unlock the encrypted disks. The upside here is that it allows the server to fully reboot on its own, and if it ever gets stolen I can just take down that keyfile. However, whereas SpaceInvaderOne uses an SFTP server to host the key, I decided to leverage that existing Wireguard connection so that I can keep the keyfile securely on my home-server and just rsync it over via the VPN connection.&lt;/p&gt;

&lt;p&gt;Great, the server now boots up on its own, but I still want &lt;em&gt;some&lt;/em&gt; way to access the management interface without driving over there. For this I am using &lt;a href=&quot;https://tailscale.com/&quot;&gt;Tailscale&lt;/a&gt;. Tailscale is a really incredible tool insofar as it’s dead simple to use. After installing the program, you just need to log in to your Google or Github account. Repeat this for each of your devices, and one by one they begin to form a mesh network powered by Wireguard. I was shocked at just how simple and effective it was, and it even has an iOS app, allowing me to even manage all my systems while I’m on the go. Best of all, it uses relay servers so that you don’t have to forward any ports. I couldn’t be happier with it.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;So after weeks of planning, smashing my face into my keyboard, drives randomly dropping out of the array, cut fingers from the heatsink fins slicing through my flesh, and endless data transfer and parity calculations, I’ve finally got a home server I can really take full advantage of (or at least I will once this P400 arrives). And with that, it’s time to pack up that old Rosewill chassis and officially retire it to the offsite-backup location.&lt;/p&gt;
</description>
        <pubDate>Sat, 06 Nov 2021 00:00:00 +0000</pubDate>
        <link>http://swanlund.dev/server-build-log</link>
        <guid isPermaLink="true">http://swanlund.dev/server-build-log</guid>
        
        <category>datahoarding</category>
        
        <category>unraid</category>
        
        <category>backups</category>
        
        
      </item>
    
      <item>
        <title>Introducing Waterlock: Making Incremental, Offsite &amp; Offline Backups Easy</title>
        <description>&lt;p&gt;Right now I’m in the middle of a whole-house PC upgrade, and the result of that process is that my current home-server will become an off-site backup server. That means I’ll have a new, much more powerful 32TB server at home, and a low-power 29TB server at a relatives house. Ideally my home-server would back up to the offsite server nightly, and for the most part it will. However, when it comes to large files (like my GoPro footage), nightly backups could end up exceeding my relatives’ internet cap, which is set at 1TB a month.&lt;/p&gt;

&lt;p&gt;Given that I don’t want to pay an extra $15 a month to upgrade them to unlimited data, I thought about how I might use an external hard drive to transfer data over to the backup server whenever I go (about twice a month). The difficulty of this plan, however, is figuring out how the heck I’m going to keep track of what’s &lt;em&gt;already on&lt;/em&gt; the backup server when I prepare the drive. Not only that, but with two file copies with each move, I want to make sure that nothing gets corrupted as it’s being transferred.&lt;/p&gt;

&lt;p&gt;I was feeling a tad motivated one evening trying to figure out how to do this when I decided, ‘screw it, I’ll write a &lt;strong&gt;quick&lt;/strong&gt; Python script’. Well, about &lt;strong&gt;20 hours+&lt;/strong&gt; of coding later, including entirely refactoring all the code, and we have what I call &lt;a href=&quot;https://github.com/TheTinHat/Waterlock&quot;&gt;Waterlock&lt;/a&gt;: a Python script for *incremental, offline backups using external hard drives. Named after the marine ‘locks’ that boats use to navigate through river systems in separate stages, Waterlock moves data between a source, middle, and end device. You can download it &lt;a href=&quot;https://github.com/TheTinHat/Waterlock&quot;&gt;here from GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;*I should note that I use ‘incremental backup’ perhaps a tad loosely here. It doesnt backup actual deltas or let you restore to a certain point in time (though I may add this feature in the future). But it works great for those ‘write-once and hopefully read-never’ scenarios, like moving your movie collection or photo library to an off-site computer and keeping it up to date.&lt;/p&gt;

&lt;h2 id=&quot;a-quick-note-on-development&quot;&gt;A Quick Note on Development&lt;/h2&gt;
&lt;p&gt;I’m still hammering away on Waterlock, so it is definitely ‘alpha’ software. It seems to work well and the tests I’ve written haven’t thrown any errors, but regardless, &lt;strong&gt;I am not responsible for any lost data&lt;/strong&gt;. Additionally, one iteration of the script may be incompatible with another due to changes to the database structure and whatnot, so please keep this in mind when re-downloading the script for use with existing deployments. But most of all, feel free to contribute! I’m definitely just an amateur Python developer so any help or feedback would be much appreciated.&lt;/p&gt;

&lt;h2 id=&quot;how-waterlock-works-step-by-step&quot;&gt;How Waterlock Works, Step by Step&lt;/h2&gt;
&lt;p&gt;A bit of warning: I’m going to go into quite a bit of detail with this, but know that overall the usage of this tool is fairly straightforward and makes maintaining your backups incredibly easy, as you’ll see in the following section. With that said, here’s roughly how it works under the hood:&lt;/p&gt;

&lt;p&gt;After saving the &lt;a href=&quot;https://github.com/TheTinHat/Waterlock&quot;&gt;script&lt;/a&gt; to an external hard drive (i.e. the ‘middle’ location), you then feed it the paths to your desired source and end directories, each of course resting on two separate systems. Waterlock automatically detects how far along the transfer is based on whether it can see the source or end directory, so it won’t work if it can see both at the same time.&lt;/p&gt;

&lt;p&gt;Second, Waterlock will create two folders in the same directory as the script: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo/&lt;/code&gt;. In the config folder, a SQLite database will be created storing the path of all the files in the source directory, the last time they were modified, a record of whether the file has been moved to the middle or end directories yet, while also leaving room to store a hash of each file. Of course, if you’ve already run the script it will skip this step.&lt;/p&gt;

&lt;p&gt;Third, Waterlock will generate a blake2 hash of each file to store in the database. Note that if the script has already been run previously then it will check the database to avoid re-hashing the file. Then (ignoring some string manipulation that proved to be quite a headache), it will copy the file from the source to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo/&lt;/code&gt; folder (again, the ‘middle’ step in the process), before hashing the file once more to check that nothing got corrupted during the transfer. If everything went to plan, it will mark it in the database as having been moved to the middle step and move on to the next file. If, however, the hashes did not match, it will retry moving and hashing the file five times before quitting. Otherwise, it will keep moving files until the middle drive gets filled up. The default configuration will leave 1GB left on the drive, but you can change this in the script settings if you want to leave more space.&lt;/p&gt;

&lt;p&gt;Finally, we move onto the stage where we transfer files to the destination. Waterlock will check the database for all the files that have been marked as having been moved to the middle drive but not the destination, and will begin to transfer those in much the same way as the third step. Obviously it won’t rehash the file on the middle drive, instead just pulling it from the database. If everything goes smoothly, the files should end up safely on the destination and the database will be updated to reflect everything that safely made it across. Now at this point, there’s a function in the script called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dump_cargo()&lt;/code&gt; that will run if you’ve enabled it, which will delete all the data in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo/&lt;/code&gt; folder. Note that you’ll need to confirm this by typing “Yes” (case sensitive).&lt;/p&gt;

&lt;p&gt;The next time you run the script on the source folder, the following will occur (though not necessarily in this particular order):&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Waterlock will once again scan all the files, adding anything that is new or that didn’t get moved last time to the database.&lt;/li&gt;
  &lt;li&gt;If a file is in the database but can no longer be found on the source, Waterlock will give you the option to mark it either to be skipped or to be removed from the destination. If you select the latter, the next time Waterlock is run on the destination, it will confirm whether you want to delete the file. If you select no, it will simply mark it to be skipped instead.&lt;/li&gt;
  &lt;li&gt;Waterlock will also scan the file modification times, and if a file has been updated it will get marked it as unmoved and its hash will be recalculated.&lt;/li&gt;
  &lt;li&gt;If a file is already on the middle drive (or destination for that matter), Waterlock will check the size of the file and if it doesn’t match what’s on the source (or middle drive in case of moving to the destination) it will be deleted and replaced. This helps solve the issue of files being cancelled half way through being copied.&lt;/li&gt;
  &lt;li&gt;Finally, Waterlock will check for any files that made it onto the middle drive, but not the destination. This may happen if you forgot to run it on the end destination or if you deleted the data before it got transferred. If the files are no longer on the middle drive, it will again mark them for copying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Waterlock also has two additional functions you can call to verify all the files on the middle or destination drives. These are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify_middle()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify_destination()&lt;/code&gt;. This will compare the hashes stored in the database with all the files on the middle or destination, of course depending on what function you called.&lt;/p&gt;

&lt;p&gt;Again, this sounds like a lot but using the tool is easy and takes care of all the difficult parts of the process for you. All you have to do is edit two lines, run it before you go to your off-site backup, and then again when you arrive.&lt;/p&gt;

&lt;h2 id=&quot;setting-it-up&quot;&gt;Setting it up&lt;/h2&gt;
&lt;p&gt;With how it works out of the way, setting up Waterlock is fairly straightforward. Just download the script &lt;a href=&quot;https://github.com/TheTinHat/Waterlock&quot;&gt;from GitHub&lt;/a&gt; and save it onto the external hard drive you plan to use. Then, open it in a text editor and enter the &lt;strong&gt;absolute&lt;/strong&gt; file paths for your source and destination directories at the top of the script (see below). Do not use relative file paths. Note that &lt;em&gt;you can add multiple paths&lt;/em&gt;, but make sure to do so in the same order between the source and destination directories. You can also configure how much reserved space you want at this time.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;'''===== IF RUNNING AS SCRIPT CHANGE THE FOLLOWING FOLDERS ====='''
# Absolute File Paths Only! Add comma-separated paths (e.g. a list of strings) to support multiple directories
# If using multiple source and end directories, ensure they are in the same order! See example in comment below.
source_directory = ['/ABSOLUTE/PATH/TO/FOLDER/'] # ['/ABSOLUTE/PATH/ONE', '/ABSOLUTE/PATH/TWO']
end_directory = ['/ABSOLUTE/PATH/TO/FOLDER/'] # ['/ABSOLUTE/PATH/ONE', '/ABSOLUTE/PATH/TWO']
reserved_space = 1 # Enter value in Gibibytes

'''============================================================='''
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you want to enable additional functions like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dump_carg()&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify_destination()&lt;/code&gt;, then just scroll down to the bottom of the script and uncomment the corresponding line of code. For instance, in the example below I’ve enabled the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dump_carg()&lt;/code&gt; function:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;if __name__ == &quot;__main__&quot;:

    if len(source_directory) != len(end_directory):
        raise Exception(&quot;Error: different number of source and end directories.&quot;)

    for i in range(len(source_directory)):
        wl = Waterlock( source_directory=source_directory[i],
                        end_directory=end_directory[i], 
                        reserved_space=reserved_space
                        )
        wl.start()

        #wl.verify_middle()
        #wl.verify_destination()
        wl.dump_cargo()

        del wl
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And you’re ready to go! Run the script with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python waterlock.py&lt;/code&gt; and fire away!&lt;/p&gt;

</description>
        <pubDate>Thu, 28 Oct 2021 00:00:00 +0000</pubDate>
        <link>http://swanlund.dev/waterlock</link>
        <guid isPermaLink="true">http://swanlund.dev/waterlock</guid>
        
        <category>python</category>
        
        <category>datahoarding</category>
        
        <category>backups</category>
        
        
      </item>
    
      <item>
        <title>The Cheap &amp; Easy Audio Upgrade</title>
        <description>&lt;p&gt;Welcome to Zoom University, where students listen to lectures recorded through a Pringles can stuffed with tinfoil and people yell “Sorry I missed that” every 30 seconds in meetings because someone’s dog started barking.&lt;/p&gt;

&lt;p&gt;Except, it doesn’t have to be like that. Some quick changes to your audio setup can really, really make your videos and meetings have a far more professional feel to them. In fact, audio quality makes a much bigger difference to the production quality of a video than the image quality. In other words, you’re far better off investing in a decent mic than a decent webcam if you want to make high quality videos.&lt;/p&gt;

&lt;p&gt;Rather than get bogged down in audiophile nonsense and trying to sell $400 cables because they “have better shielding” and XLR interfaces, here’s some straightforward and generally affordable (if not free) ways to seriously ugprade your audio.&lt;/p&gt;

&lt;p&gt;Note: if you’re a sound engineer, look away now. This is not for you.&lt;/p&gt;

&lt;h2 id=&quot;1-position-your-mic&quot;&gt;1. Position Your Mic&lt;/h2&gt;

&lt;p&gt;This is the easiest fix you can make. Just position your mic better, about 3-4 inches away from the corner of your mouth, and the audio will sound far less distant and echo-ey than if it’s just sitting of your desk. In fact, even if you buy a nice $200 mic, it will probably still sound bad unless it’s positioned correctly. Take a listen &lt;a href=&quot;/assets/mic-sample.mp3&quot;&gt;to this mic sample I’ve recorded&lt;/a&gt; comparing just how much of a difference mic positioning can make.&lt;/p&gt;

&lt;p&gt;To get the right position, you’ll of course need to use a mic that’s external to your laptop or webcam. Personally, I run a &lt;a href=&quot;https://www.amazon.ca/gp/product/B06XQ39XCY/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;amp;psc=1&quot;&gt;$40 Fifine USB mic&lt;/a&gt;, and mount it on a &lt;a href=&quot;https://www.amazon.ca/gp/product/B07JB68T1C/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;amp;psc=1&quot;&gt;$40 boom arm&lt;/a&gt;, both purchased off Amazon. The arm lets me easily position the mic for meetings and video recording, and afterwards a quick push lets me get it out of the way. It also includes a pop filter, which helps mitigate those loud ‘P’ sounds.&lt;/p&gt;

&lt;h2 id=&quot;2-use-rtx-voice&quot;&gt;2. Use RTX Voice&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://www.nvidia.com/en-us/geforce/guides/nvidia-rtx-voice-setup-guide/&quot;&gt;RTX Voice&lt;/a&gt; is dark magic that would tempt even Albus Dumbledore himself to join Voldemort. It does an amazing job cancelling out background noise, to the point that you can literally &lt;a href=&quot;https://www.youtube.com/watch?v=uWUHkCgslNE&quot;&gt;run a vacuum in the background&lt;/a&gt; and maintain relatively clear audio. It requires an Nvidia graphics card, which is the only downside, but it’s worth going Nvidia over. When using RTX Voice, you’ll direct your microphone audio into the software, and then select “RTX Voice” as your microphone on Zoom, OBS, or whatever other recording software you’re using. This will give your listeners crystal clear audio that’s free of barking dogs, screaming children, lawn mowers, or loud laundry machines. As an additional benefit, RTX Voice can also cut out &lt;em&gt;other people’s&lt;/em&gt; background noise so that you can hear them better as well.&lt;/p&gt;

&lt;h2 id=&quot;3-buy-a-better-mic&quot;&gt;3. Buy A Better Mic&lt;/h2&gt;

&lt;p&gt;This doesn’t mean buy an &lt;em&gt;expensive&lt;/em&gt; mic. As I mentioned, I achieve pretty decent results with just a &lt;a href=&quot;https://www.amazon.ca/gp/product/B06XQ39XCY/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;amp;psc=1&quot;&gt;$40 Fifine mic&lt;/a&gt; and would highly recommend it (though the price seems to have gone up due to COVID). In fact, it is leaps and bounds better than the mic on my $300 &lt;a href=&quot;https://www.amazon.ca/Logitech-BRIO-Digital-Recording-Streaming/dp/B01N5UOYC4/ref=sr_1_2?dchild=1&amp;amp;keywords=brio+logitech&amp;amp;qid=1603829890&amp;amp;sr=8-2&quot;&gt;Logitech Brio&lt;/a&gt; webcam (in case you’re wondering, you should probably just get a &lt;a href=&quot;https://www.amazon.ca/Logitech-C920-Webcam-Pro-960-000764/dp/B006JH8T3S/ref=sr_1_5?dchild=1&amp;amp;keywords=logitech+c920&amp;amp;qid=1603830953&amp;amp;sr=8-5&quot;&gt;C920&lt;/a&gt; instead of the Brio). &lt;a href=&quot;/assets/mic-sample-brio.mp3&quot;&gt;Here’s an audio sample&lt;/a&gt; comparing the two microphones.&lt;/p&gt;

&lt;p&gt;If you’re willing to drop a little bit of extra cash, the &lt;a href=&quot;https://www.amazon.ca/dp/B07ZPBFVKK/?coliid=ICTCYX93WYVA6&amp;amp;colid=1L72ELLFKAPYD&amp;amp;psc=1&amp;amp;ref_=lv_ov_lig_dp_it_im&quot;&gt;$130 Audio Technica ATR2100x-USB&lt;/a&gt; is an excellent option that will do a much better job of not picking up background noise (though you really will need to postion it close to your mouth as it is a &lt;a href=&quot;https://www.neumann.com/homestudio/en/what-is-a-dynamic-microphone#:~:text=Dynamic%20microphones%2C%20thus%2C%20are%20microphones,surrounded%20by%20a%20permanent%20magnet.&quot;&gt;dynamic microphone&lt;/a&gt;). The Blue Yeti is also very popular, though more expensive. Personally, I’d recommend just buying the Audio Technica and investing the savings into a &lt;a href=&quot;https://www.amazon.ca/gp/product/B07JB68T1C/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;amp;psc=1&quot;&gt;boom arm&lt;/a&gt; because once again, mic positioning is everything.&lt;/p&gt;

&lt;h2 id=&quot;4-pay-attention-to-gain-and-clipping&quot;&gt;4. Pay Attention to Gain and Clipping&lt;/h2&gt;

&lt;p&gt;This is a big one. If you’ve ever heard audio that sounds loud and crunchy, it’s probably because of clipping. Take a listen to &lt;a href=&quot;/assets/mic-sample-gain.mp3&quot;&gt;this sample I’ve recorded comparing the difference gain makes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you don’t watch out for clipping, even an expensive mic will still sound horrible. Gain is essentially how much your microphone volume is boosted. If it’s boosted too much, then the louder parts of your speech will ‘clip’ and sound crunchy. In order to control this, talk into your mic and watch the little sound meter in your recording software. As you’re talking, dial back the gain until the sound meter just &lt;em&gt;barely&lt;/em&gt; hits the red (upper end) at the loudest parts of your speech. Some mics have built-in gain dials, so use that if you have one, otherwise use the microphone settings in Windows, MacOS, or whatever program you’re using to dial these levels back.&lt;/p&gt;

&lt;p&gt;One catch that I’ve noticed on Windows is that programs behave differently with regard to gain and clipping, so don’t assume that just because you’ve set it to sound good in Zoom that it will also sound good in OBS.&lt;/p&gt;

&lt;h2 id=&quot;5-fiddle-with-your-software&quot;&gt;5. Fiddle With Your Software&lt;/h2&gt;

&lt;p&gt;There are a lot of other optimizations you can make in various programs. Here are a handful of tips and suggestions:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Audacity: &lt;a href=&quot;https://gist.github.com/pgburt/d6917b5c827f907298cc&quot;&gt;follow this workflow&lt;/a&gt; to give your audio that podcasty feel.&lt;/li&gt;
  &lt;li&gt;OBS: experiment with filters, especially the compressor. See &lt;a href=&quot;https://www.youtube.com/watch?v=0m5jFdV9i4M&quot;&gt;this video&lt;/a&gt; for some great tips on how to do that.&lt;/li&gt;
  &lt;li&gt;Zoom: try turning on Original Audio, which is &lt;a href=&quot;https://support.zoom.us/hc/en-us/articles/115003279466-Enabling-option-to-preserve-original-sound&quot;&gt;well documented here&lt;/a&gt;, and allows you to bypass some of Zoom’s own processing (don’t do this unless you already have good audio).&lt;/li&gt;
  &lt;li&gt;Davinci Resolve: watch &lt;a href=&quot;https://www.youtube.com/watch?v=uUXG8XkhyEk&quot;&gt;this tutorial&lt;/a&gt; on improving the audio in Resolve. Pay particular attention to the compressor and de-esser.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 27 Oct 2020 00:00:00 +0000</pubDate>
        <link>http://swanlund.dev/audio-upgrade</link>
        <guid isPermaLink="true">http://swanlund.dev/audio-upgrade</guid>
        
        <category>audio</category>
        
        <category>zoom</category>
        
        <category>obs</category>
        
        <category>microphones</category>
        
        
      </item>
    
      <item>
        <title>Parallelizing GIS with Geopandas and Multiprocessing in Python</title>
        <description>&lt;p&gt;I recently found myself having to iteratively perform a complicated series of buffers, intersects, and joins over a large geodataframe. This isn’t necessarily a problem for one-off operations where you can afford to wait a while, but if you plan on running this script often or want to distribute it publicly, it helps to squeeze every ounce of performance you can out of it.&lt;/p&gt;

&lt;p&gt;One obvious way to do this is to parallelize it, meaning to run the program simultaneously across more than just one CPU core. Unfortunately, a lot of tools in GIS only utilize a single core, whereas most of us are now are equipped with at least four. Hell, you can get what is effectively a &lt;a href=&quot;https://www.newegg.ca/amd-ryzen-5-3600/p/N82E16819113569&quot;&gt;12 core processor&lt;/a&gt; now for ~$200. With that said, I should briefly note that parallelizing code isn’t a silver bullet: many smaller tasks may actually run slower after parallelization, and you should always optimize code in other ways before just stretching it across more CPU cores.&lt;/p&gt;

&lt;p&gt;Unfortunately, for the cases where parallelizing makes sense, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;multiprocessing&lt;/code&gt; (the standard Python package for parallelizing code) can be a bit complicated to figure out (it certainly was for me). Hopefully this post helps illustrate how you might use it in a GIS context.&lt;/p&gt;

&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;

&lt;p&gt;Here’s what you’re going to need to run this tutorial. Start off by importing these five packages. They are all fairly standard for GIS, and multiprocessing should come already installed with any recent Python installation.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;geopandas&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpd&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;pandas&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;multiprocessing&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mp&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;statistics&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, we need to fetch our data. We’re going to analyze the average distance from each intersection to every other intersection in Vancouver, a task that is relatively straightforward but requires lots of iteration. There are probably specific tools for doing this, but the actual analysis here doesn’t really matter since the main goal is parallelizing whatever real-world analysis we may have.&lt;/p&gt;

&lt;p&gt;Intersection data is freely available from the &lt;a href=&quot;https://data.vancouver.ca/datacatalogue/index.htm&quot;&gt;Vancouver Open Data Catalogue&lt;/a&gt;, so go ahead and &lt;a href=&quot;ftp://webftp.vancouver.ca/OpenData/shape/street_intersections_shp.zip&quot;&gt;download it&lt;/a&gt; as a shapefile and unzip it into wherever you’re running this Python code from.&lt;/p&gt;

&lt;p&gt;Next, load the data into geopandas as usual into a geodataframe:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;intersections&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'street_intersections.shp'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With the data loaded, there are essentially three broad steps to analyzing it in parallel:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Create a function to process the data&lt;/li&gt;
  &lt;li&gt;Create a function to parallelize our processing function. This will have to:
    &lt;ul&gt;
      &lt;li&gt;Split the geodataframe into chunks&lt;/li&gt;
      &lt;li&gt;Process each chunk&lt;/li&gt;
      &lt;li&gt;Reassemble the chunks back into a geodataframe&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Run the parallelizing function&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;creating-the-function-to-be-parallelized&quot;&gt;Creating the function to be parallelized&lt;/h2&gt;

&lt;p&gt;First we need to define the function that we want to parallelize. Essentially, we want to be able to call a single function that will house all the tasks that we want each CPU core to run. In this case, we want our function to take in a geodataframe and calculate the distance from each point to every other point, before averaging these measurements and saving that average in a new column.&lt;/p&gt;

&lt;p&gt;Since we are calculating the distance between &lt;em&gt;each point to every other point&lt;/em&gt;, our function will require two parameters:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;a smaller geodataframe (chunk) containing &lt;em&gt;which&lt;/em&gt; points each CPU core is responsible for processing&lt;/li&gt;
  &lt;li&gt;a geodataframe containing the entire set of points that each point in (1) will be measured against&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don’t worry too much about how this function works, as it’s just a placeholder for whatever complicated thing you want to do.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;neighbour_distance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gdf_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gdf_complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gdf_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iterrows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Iterate over the chunk
&lt;/span&gt;    
        &lt;span class=&quot;n&quot;&gt;distances&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gdf_complete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;geometry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;distance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;geometry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Calculate distances from each row in the complete geodataframe to each row in the chunked geodataframe.
&lt;/span&gt;        
        &lt;span class=&quot;n&quot;&gt;gdf_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'distance'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;distances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Enter the mean of the distances into a column called 'distances' in the chunked geodataframe.
&lt;/span&gt;        
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gdf_chunk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;creating-the-parallelizing-function&quot;&gt;Creating the parallelizing function&lt;/h2&gt;

&lt;p&gt;Now we need to write a separate function that will run our first function in parallel. This isn’t strictly necessary on Linux, but Windows will spit out errors in a never-ending loop unless we do it this way.&lt;/p&gt;

&lt;p&gt;Our parallelizing function will need to split our geodataframe into smaller chunks, process those chunks, and reassemble them back into a single geodataframe. The whole function will look like this:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;parallelize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cpus&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cpu_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;intersection_chunks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;array_split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;intersections&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cpus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;pool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;processes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cpus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;chunk_processes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply_async&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;neighbour_distance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;intersections&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;intersection_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;intersection_results&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chunk_processes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;intersections_dist&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GeoDataFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;intersection_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;intersections&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;crs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;intersections_dist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The first line within our function uses multiprocessing’s &lt;a href=&quot;https://docs.python.org/2/library/multiprocessing.html#multiprocessing.cpu_count&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cpu_count()&lt;/code&gt;&lt;/a&gt; to tell us how many CPU cores our system has. This is how many chunks we will need to create, and how many cpu cores our program will be spread across. We &lt;em&gt;could&lt;/em&gt; use a fixed number (e.g. 4), but on systems with more cores we’ll underutilize their hardware.&lt;/p&gt;

&lt;p&gt;Next, we need to actually split the geodataframe into chunks, and this is what &lt;a href=&quot;https://docs.scipy.org/doc/numpy/reference/generated/numpy.array_split.html#numpy.array_split&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;array_split&lt;/code&gt;&lt;/a&gt; does. It takes an array (the first argument, i.e. ‘intersections’) and splits it into a set number of chunks/buckets (the second argument, i.e. ‘cpus’). In this case, we’ve split the geodataframe into as many chunks as we have CPU cores. &lt;em&gt;(Note that there are numerous ways to actually do this, including ways that wouldn’t require entering two geodataframes into our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;neighbour_distance&lt;/code&gt; function. I just find this way to be the best and cleanest for most situations).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pool = mp.Pool(processes=cpus)&lt;/code&gt; constructs a “Pool” that contains all of our processes, and in this case we’ve specified that we want as many processes as we have CPU cores.&lt;/p&gt;

&lt;p&gt;The next two lines are important. What we’re doing here is telling multiprocessing to run our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;neighbour_distance&lt;/code&gt; function in a separate processes for each of the chunks we created using a &lt;a href=&quot;https://www.pythonforbeginners.com/basics/list-comprehensions-in-python&quot;&gt;list comprehension&lt;/a&gt;. Notice too that we specified our arguments separately using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;args=(arg1, arg2)&lt;/code&gt;. The next line after that retrieves the results of those processes using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.get()&lt;/code&gt; and adds them a list that we’ve called ‘intersection_results’.&lt;/p&gt;

&lt;p&gt;The penultimate row reassembles each of the results back into a single geodataframe in two steps. First, it concatenates each of our chunks into a single &lt;strong&gt;pandas&lt;/strong&gt; dataframe (i.e. not a &lt;em&gt;geo&lt;/em&gt;dataframe), as geopandas doesn’t support concatenation. Second, it turns this &lt;strong&gt;dataframe&lt;/strong&gt; back into a &lt;strong&gt;geodataframe&lt;/strong&gt;, and sets the coordinate reference system (CRS) to the CRS of our original intersections layer. We have to specify the CRS because it was lost when we used pandas. Finally, we return the ‘intersections_dist’ geodataframe.&lt;/p&gt;

&lt;h2 id=&quot;running-the-parallelizing-function&quot;&gt;Running the Parallelizing Function&lt;/h2&gt;

&lt;p&gt;To string all of this together and execute it, we’ll do the following, which is again necessary to prevent Python on Windows from getting stuck:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'__main__'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    
    &lt;span class=&quot;n&quot;&gt;intersections_dist&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parallelize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;intersections_dist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now just go your terminal and execute the script: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python script_name.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you open the task manager you should now see your CPU pinned at 100% as it crunches these distance calculations across all cores. In my case, it runs on all twelve CPUs cores, which drastically decrease execution time.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;To adapt this to your own project, you just need to swap out the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;neighbour_distance&lt;/code&gt; function and respective geodataframes with your own.&lt;/p&gt;

&lt;p&gt;To learn more, definitely visit &lt;a href=&quot;https://sebastianraschka.com/Articles/2014_multiprocessing.html&quot;&gt;Sebastian Raschka’s blog on multiprocessing&lt;/a&gt;, which is where I learned most of this myself. And if you have any suggestions on how to improve this tutorial, go ahead and leave it in the comments.&lt;/p&gt;

</description>
        <pubDate>Mon, 14 Oct 2019 00:00:00 +0000</pubDate>
        <link>http://swanlund.dev/parallelizing-python</link>
        <guid isPermaLink="true">http://swanlund.dev/parallelizing-python</guid>
        
        <category>geopandas</category>
        
        <category>python</category>
        
        <category>performance</category>
        
        
      </item>
    
  </channel>
</rss>
