<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.3">Jekyll</generator><link href="https://lindt8.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://lindt8.github.io/" rel="alternate" type="text/html" /><updated>2026-04-28T01:12:06-07:00</updated><id>https://lindt8.github.io/feed.xml</id><title type="html">Hunter Ratliff</title><subtitle>Researcher</subtitle><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><entry><title type="html">Taming noisy random HDD spin-ups between Time Machine backups</title><link href="https://lindt8.github.io/posts/time-machine-hdd-spinup-minimization/" rel="alternate" type="text/html" title="Taming noisy random HDD spin-ups between Time Machine backups" /><published>2026-03-14T00:00:00-07:00</published><updated>2026-03-14T00:00:00-07:00</updated><id>https://lindt8.github.io/posts/blog-post-13</id><content type="html" xml:base="https://lindt8.github.io/posts/time-machine-hdd-spinup-minimization/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;p&gt;This article details my strategy for absolutely minimizing the amount of times I must hear my external hard disk drive (HDD), used for macOS Time Machine backups, noisily spin up to the outright bare minimum. It has been a ride…&lt;/p&gt;

&lt;p&gt;Fancy yourself comfortably in bed, slowly drifting off to sleep, when suddenly you’re jolted awake by:&lt;/p&gt;

&lt;figure&gt;  
  &lt;audio controls=&quot;&quot;&gt;
    &lt;source src=&quot;/files/HDD_spin-up_noises.mp3&quot; type=&quot;audio/mpeg&quot; /&gt;
    Your browser does not support the audio element.
  &lt;/audio&gt;
  &lt;figcaption&gt;
    bzzzzZTT...whhhrrrrrrrRRRRR-TICK-TICK-TICK-HHHRRRRRRRRRRRR-tick-RRRRR
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Imagine just casually watching a YouTube video or trying to focus on work, and then suddenly, out of the blue, BZZZT-WHIRRRRRR-TICK-TICK-HRRRR…&lt;/p&gt;

&lt;p&gt;Reducing this to the absolute minimum is the mission I set out to achieve and detail here.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;setting-the-stage&quot;&gt;Setting the stage&lt;/h1&gt;

&lt;p&gt;My home computing setup consists of a &lt;a href=&quot;https://en.wikipedia.org/wiki/MacBook_Pro_(Apple_silicon)#M4_models&quot;&gt;M4 MacBook Pro&lt;/a&gt; that largely lives lid-closed in clamshell mode docked to a &lt;a href=&quot;https://www.caldigit.com/thunderbolt-5-dock-ts5-plus/&quot;&gt;CalDigit TS5+ dock&lt;/a&gt;, which is then connected to two monitors, speakers, and other accessories. It is very convenient being able to have at minimum just the single Thunderbolt 5 cable to plug/unplug to switch between using my Mac like a desktop versus as a laptop (though, I often do also connect the &lt;a href=&quot;https://en.wikipedia.org/wiki/MagSafe#MagSafe_3&quot;&gt;MagSafe&lt;/a&gt; cable for AC power). In general, it lives this “desktop life”, and I have adjusted some settings to have it behave more like a desktop, most importantly being to disable the machine from automatically going to sleep when on AC power, allowing me to run simulations or any other task overnight without fear of the system interrupting it.&lt;/p&gt;

&lt;p&gt;Simultaneously, I also strongly believe in the &lt;a href=&quot;https://en.wikipedia.org/wiki/Backup#3-2-1_Backup_Rule&quot;&gt;3-2-1 backup rule&lt;/a&gt;, that my data should consist of at least 3 copies spread over 2 different types of storage media with 1 copy offsite. For years I have been a happy customer of &lt;a href=&quot;https://www.backblaze.com/&quot;&gt;Backblaze&lt;/a&gt; for that offsite component, but my history with the additional local copy has been a struggle, until &lt;a href=&quot;https://hratliff.com/posts/choosing-macOS/&quot;&gt;I moved to macOS&lt;/a&gt;, which has an excellent built-in utility exactly for this purpose: &lt;a href=&quot;https://en.wikipedia.org/wiki/Time_Machine_(macOS)&quot;&gt;Time Machine&lt;/a&gt;. All you need is an extra external drive with a bunch of extra space, ideally at least twice the Mac’s storage capacity. I happened to find a pretty good deal on an (overkill) &lt;a href=&quot;https://www.westerndigital.com/products/external-drives/wd-elements-desktop-usb-3-0-hdd?sku=WDBWLG0140HBK-NESN&quot;&gt;14 TB Western Digital external hard drive (HDD)&lt;/a&gt;, so that is what I have living connected to my dock at all times.&lt;/p&gt;

&lt;p&gt;Little did I know at the time what a rabbit hole this specific series of decisions was about to lead me down…&lt;/p&gt;

&lt;h1 id=&quot;the-initial-problem&quot;&gt;The initial problem&lt;/h1&gt;

&lt;p&gt;What I didn’t know at the time was:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;My HDD is really quite noisy when it spins up… &lt;audio controls=&quot;&quot;&gt;&lt;source src=&quot;files/HDD_spin-up_noises.mp3&quot; type=&quot;audio/mpeg&quot; /&gt; Your browser does not support the audio element. &lt;/audio&gt;&lt;/li&gt;
  &lt;li&gt;Time Machine’s default scheduling options weren’t ideal for me.&lt;/li&gt;
  &lt;li&gt;The system will regardless spin up the HDD randomly even outside of Time Machine backups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This had me feeling quite frustrated. While Time Machine is an absolutely lovely utility that makes good data backup habits incredibly easy, including encrypting your backups by selecting APFS encryption when configuring the volume, its user-facing options are extremely minimal. Your options for backup frequency are hourly, daily, weekly, or manually. For me personally, I think daily is &lt;em&gt;fine&lt;/em&gt;, hourly is probably overkill, but ideally I would be able to have something in between.&lt;/p&gt;

&lt;p&gt;Furthermore, even if I set Time Machine to only back up daily, for as long as the HDD remains mounted to my Mac, the system and/or the dock will prod at it randomly, causing it to spin up, even if the “Put hard disks to sleep when possible” setting is set to “Always”. Sometimes it would go an hour or few without spinning up; other times it would spin up several times within an hour. It was becoming extremely grating, and I was really starting to regret my HDD purchase. &lt;em&gt;“Had I just paid quite a bit more money for an SSD-based external drive for Time Machine it’d be silent and without any of these issues!”&lt;/em&gt; I lamented.&lt;/p&gt;

&lt;p&gt;It was too late for that solution now though. While I had bought it at a very good deal, that HDD still wasn’t so cheap that just spending my way out of the problem was remotely palatable. Surely I could work through these issues and somehow develop a solution, right?&lt;/p&gt;

&lt;h1 id=&quot;the-solution-as-designed&quot;&gt;The solution as designed&lt;/h1&gt;

&lt;h2 id=&quot;assumptions&quot;&gt;Assumptions&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;I outline here the solution implemented in my environment with my configuration: A M4 MacBook Pro running macOS 26 Tahoe connected to a dock (CalDigit TS5+) connected to an APFS-encrypted external HDD (WD Elements, 14 TB), where the MacBook by default lives connected to the dock unless I happen to be taking it out with me somewhere. I imagine this solution will be more broadly applicable than to this specific combination of equipment and configuration, but since I never pinpointed exactly every single different reason why my HDD spun up at all the various circumstances it did, just developed a robust solution that prevented it all, it is possible some of these steps may not be fully necessary nor complete on your own system. That said, I do feel this approach is quite robust.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;objective&quot;&gt;Objective&lt;/h2&gt;

&lt;p&gt;The HDD spin-ups should be absolutely minimized to the bare amount necessary. As a baseline, this means it should only spin up for Time Machine backups alone, though I consider additional spin-ups when I re-dock the MacBook (after being disconnected) or when I first wake the MacBook from sleep or restart it acceptable. The key here is that spin-ups should be consistent and predictable, not happening randomly at any hour and regardless of whether the MacBook is actively being used, locked and idle, or set to sleep. Furthermore, I want Time Machine to perform backups at a few set times of my choosing per day, but I would also like for these backups to be skipped if the MacBook is set to Sleep. The solution should be fully in software and automated; it should not require me to remember to unplug/replug some physical cable or to manually run some script or Shortcut.&lt;/p&gt;

&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;

&lt;p&gt;The solution I ultimately fell upon is to keep the HDD used for Time Machine unmounted at all times, only allowing it to be automatically mounted immediately before a Time Machine backup and promptly automatically ejected afterward. The small &lt;a href=&quot;https://tclementdev.com/timemachineeditor/&quot;&gt;TimeMachineEditor app&lt;/a&gt; is the perfect solution for the Time Machine scheduling part here; I specifically have settled on three scheduled backups per day at 11:00, 16:00, and 21:00. I then use two custom-made &lt;a href=&quot;https://en.wikipedia.org/wiki/Launchd#launchd&quot;&gt;LaunchDaemons&lt;/a&gt; to schedule the automation of mounting the HDD immediately before a scheduled Time Machine backup and to then, only after the backup is complete, eject the HDD once more.&lt;/p&gt;

&lt;p&gt;The LaunchDaemons essentially each just run a shell script. The mounting script is scheduled to run 2 minutes before each scheduled TimeMachine backup and is responsible for first ensuring that the MacBook is not set to be sleeping before proceeding and then unlocking and mounting the APFS-encrypted HDD used for Time Machine backups; it also creates a temporary file to signify it was responsible for mounting the HDD. The ejection script is scheduled to launch 1 minute after each scheduled Time Machine backup; it first checks for that temporary file (as to not errantly disconnect the HDD if I had manually mounted it myself) and, if present, proceeds to then enter a polling loop where it checks whether Time Machine is still running or is finished (every 20 seconds for 5 minutes followed by every 1 minute for 40 more minutes) and, once Time Machine is detected as finished, it then unmounts the HDD volume, deletes the temporary file, and terminates.&lt;/p&gt;

&lt;p&gt;I also created a &lt;a href=&quot;https://en.wikipedia.org/wiki/Shortcuts_(Apple)&quot;&gt;Shortcut&lt;/a&gt; that allows me to manually mount the HDD easily in the event I ever want to interact with it solely in software, rather than needing to do some clunky unplugging and replugging of cables. And, of course, when I’m done with it I can just eject it normally as done with any external media.&lt;/p&gt;

&lt;p&gt;Next, I’ll step through the exact implementation for getting this all set up.&lt;/p&gt;

&lt;h1 id=&quot;how-to-implement-the-solution&quot;&gt;How to implement the solution&lt;/h1&gt;

&lt;h2 id=&quot;step-1--identify-apfs-volume-uuid&quot;&gt;Step 1 — Identify APFS volume UUID&lt;/h2&gt;

&lt;p&gt;First, you’ll need to be able to consistently identify which attached volume is your drive used for Time Machine. Fortunately, each drive has a universally unique identifier (&lt;a href=&quot;https://en.wikipedia.org/wiki/Universally_unique_identifier&quot;&gt;UUID&lt;/a&gt;) associated with it that is constant that you can fetch.&lt;/p&gt;

&lt;p&gt;Run:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;diskutil apfs list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Locate the APFS &lt;strong&gt;Volume&lt;/strong&gt; named your Time Machine volume (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TimeMachine&lt;/code&gt; on my system), then copy its &lt;strong&gt;Volume UUID&lt;/strong&gt; (not the container UUID).  For example, on my system:&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/files/get_APFS_UUID.png&quot; /&gt;&lt;/div&gt;

&lt;p&gt;You will use it shortly in Step 5 (and later in Step 12) as:&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;TM_APFS_UUID=&quot;YOUR-UUID-HERE&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-2--create-required-directories&quot;&gt;Step 2 — Create required directories&lt;/h2&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /usr/local/etc/tm-dock
&lt;span class=&quot;nb&quot;&gt;sudo mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /usr/local/sbin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;These directories will be where the schedule file (tiny text file listing out the desired backup times), disk passphrase file (for unlocking the APFS-encrypted HDD), and scripts for the LaunchDaemons (and Shortcut) will live. These are made in the next few steps.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-3--create-schedule-file&quot;&gt;Step 3 — Create schedule file&lt;/h2&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;nano /usr/local/etc/tm-dock/times.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Example contents:&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;# One backup time per line (24-hour HH:MM)
11:00
16:00
21:00
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Save and exit.&lt;/p&gt;

&lt;p&gt;This is your single source of truth for scheduling for the LaunchDaemon side of things.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-4--store-disk-passphrase-ensure-root-only-access&quot;&gt;Step 4 — Store disk passphrase, ensure root-only access&lt;/h2&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;nano /usr/local/etc/tm-dock/passphrase
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Paste your Time Machine disk encryption password (one line only). You set this password when you first configured the volume with APFS encryption. Most likely you had selected to have it saved to the Mac when you created it. If so, it can be found in the Keychain Access app (search for it in Spotlight; yes, the password lives here and not in the separate Passwords app); searching for the name of your volume (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TimeMachine&lt;/code&gt; for me) in it is probably the fastest way to find it.&lt;/p&gt;

&lt;p&gt;Secure the passphrase file:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo chown &lt;/span&gt;root:wheel /usr/local/etc/tm-dock/passphrase
&lt;span class=&quot;nb&quot;&gt;sudo chmod &lt;/span&gt;600 /usr/local/etc/tm-dock/passphrase
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Security note&lt;/strong&gt;: Only root can read this file. If someone has root access to your computer, disk encryption is not your limiting defense anyway and you have far bigger worries.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-5--prepare-the-mount-script&quot;&gt;Step 5 — Prepare the mount script&lt;/h2&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;nano /usr/local/sbin/tm-dock-mount.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Paste:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;c&quot;&gt;#!/bin/zsh&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-euo&lt;/span&gt; pipefail

&lt;span class=&quot;c&quot;&gt;# ===== USER SETTINGS =====&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;TM_APFS_UUID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PUT_YOUR_APFS_VOLUME_UUID_HERE&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;VOL_PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/Volumes/TimeMachine&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;PASSFILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/usr/local/etc/tm-dock/passphrase&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;SCHEDULE_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/usr/local/etc/tm-dock/times.conf&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# =========================&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Exit only if the most recent system wake was positively identified as a&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# background DarkWake (PowerNap, maintenance, scheduled job).&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Any other outcome — full wake, unreadable log, unexpected format — proceeds,&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# since a missed backup is worse than an unnecessary HDD spin-up.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Parse the event-type label (field 4) from the pmset log:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#   &quot;Wake&quot;      = full user wake -&amp;gt; proceed&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#   &quot;DarkWake&quot;  = background/PowerNap wake -&amp;gt; exit early&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#   anything else / empty -&amp;gt; proceed (fail safe)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# The grep pre-filters to lines whose label is Wake or DarkWake followed by&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# the same word in the description, excluding noise like &quot;Wake Requests&quot;.&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /tmp/tm-dock

&lt;span class=&quot;nv&quot;&gt;last_wake_line&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;pmset &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; log 2&amp;gt;/dev/null | &lt;span class=&quot;nv&quot;&gt;LC_ALL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;C &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'^\S+ \S+ \S+ (Wake|DarkWake)\s+\t?(Wake|DarkWake)'&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;wake_label&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$last_wake_line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nv&quot;&gt;LC_ALL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;C &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{print $4}'&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;: wake_label='&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;wake_label&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;' last_wake_line='&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;last_wake_line&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;'&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/tm-dock/idle-debug.log

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$last_wake_line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$wake_label&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;DarkWake&quot;&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;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;: exiting early (DarkWake detected)&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/tm-dock/idle-debug.log
  &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;span class=&quot;k&quot;&gt;else
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;: proceeding with script (full Wake or no DarkWake detected)&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; /tmp/tm-dock/idle-debug.log
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# This job is scheduled at (scheduled_time - 2 minutes).&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Guard: only act if within +/- 3 minutes of any expected mount time&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;WINDOW_MINUTES&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3

&lt;span class=&quot;nv&quot;&gt;STATE_DIR&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/tmp/tm-dock&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;FLAG_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$STATE_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/mounted_by_script&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$STATE_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Convert current time to minutes since midnight&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;now_min&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#$(date +%H)*60 + 10#$(date +%M)))&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Parse times.conf -&amp;gt; minutes from midnight&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Each valid HH:MM entry is converted to minutes since midnight and collected&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# into sched_mins. Lines beginning with # or blank lines are skipped.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;sched_mins&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;r line&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ &lt;span class=&quot;o&quot;&gt;^[[&lt;/span&gt;:space:]]&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# ]] &amp;amp;&amp;amp; continue&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ &lt;span class=&quot;o&quot;&gt;^[[&lt;/span&gt;:space:]]&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue
  if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ &lt;span class=&quot;o&quot;&gt;^([&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]{&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;,2&lt;span class=&quot;o&quot;&gt;})&lt;/span&gt;:&lt;span class=&quot;o&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]{&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;})[[&lt;/span&gt;:space:]]&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &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;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[1]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[2]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;
    sched_mins+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#$h*60 + 10#$m)))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi
done&lt;/span&gt; &amp;lt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SCHEDULE_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# This script is scheduled 2 minutes before each backup time. Check that the&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# current time falls within WINDOW_MINUTES of the expected mount time for at&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# least one scheduled backup. The +1440 % 1440 arithmetic handles midnight&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# wraparound. If d &amp;gt; 720 we take the shorter arc around the clock face.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;in_window&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;s &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;sched_mins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;mount_min&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;now_min &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; mount_min &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; d &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;720&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; d&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; d &amp;lt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; WINDOW_MINUTES &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;in_window&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1
    &lt;span class=&quot;nb&quot;&gt;break
  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fi
done&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; in_window &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; 1 &lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0

&lt;span class=&quot;c&quot;&gt;# If already mounted, do nothing (and don't set flag).&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$VOL_PATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0

&lt;span class=&quot;c&quot;&gt;# Unlock (if locked). If already unlocked, this prints an error; we ignore it.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Note: unlocking may mount automatically, but not always.&lt;/span&gt;
/usr/sbin/diskutil apfs unlockVolume &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TM_APFS_UUID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-stdinpassphrase&lt;/span&gt; &amp;lt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PASSFILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# If still not mounted, determine the current disk identifier (e.g. disk7s2) and mount it.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$VOL_PATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&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;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISK_ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;/usr/sbin/diskutil apfs list | /usr/bin/awk &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TM_APFS_UUID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'
    $0 ~ uuid { getline; if ($1 == &quot;APFS&quot; &amp;amp;&amp;amp; $2 == &quot;Volume&quot; &amp;amp;&amp;amp; $3 == &quot;Disk&quot; &amp;amp;&amp;amp; $4 ~ /^\(Role\):$/) { print $5; exit }
             else if ($1 == &quot;APFS&quot; &amp;amp;&amp;amp; $2 == &quot;Volume&quot; &amp;amp;&amp;amp; $3 == &quot;Disk&quot;) { print $4; exit } }
  '&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Fallback parsing (more robust): look for &quot;APFS Volume Disk (Role): diskXsY&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISK_ID&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;:-}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&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;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISK_ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;/usr/sbin/diskutil apfs list | /usr/bin/awk &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TM_APFS_UUID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'
      $0 ~ uuid { found=1 }
      found &amp;amp;&amp;amp; /APFS Volume Disk/ { for (i=1;i&amp;lt;=NF;i++) if ($i ~ /^disk[0-9]+s[0-9]+$/) { print $i; exit } }
    '&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi

  if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISK_ID&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;:-}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&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;k&quot;&gt;then&lt;/span&gt;
    /usr/sbin/diskutil mount &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$DISK_ID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fi
fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Confirm mounted and set flag (so unmount job knows it’s safe to unmount)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$VOL_PATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&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;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;1&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FLAG_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Replace the UUID line with your UUID from Step 1.&lt;/p&gt;

&lt;p&gt;Make executable:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo chmod &lt;/span&gt;755 /usr/local/sbin/tm-dock-mount.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In the script, you can delete the lines echoing to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp/tm-dock/idle-debug.log&lt;/code&gt; if you’d like, but I was using this log file a ton when troubleshooting getting this active/locked/sleeping gate to behave just right, only skipping backups when the Mac was set to sleep (but allowing them to proceed if the system was only locked or in active use). It is still a nice sanity check to confirm the scripts are behaving as desired.&lt;/p&gt;

&lt;p&gt;To add, this was a huge rabbit hole all on its own. There were many methods I tried to use for querying the current system state, but none were consistent and many were difficult to exactly troubleshoot since the LaunchDaemons are executing in the system environment rather than the user environment. I ultimately found just querying the most recent Wake event from the &lt;a href=&quot;https://en.wikipedia.org/wiki/Pmset&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pmset&lt;/code&gt;&lt;/a&gt; log to see if it was a DarkWake or normal (Full)Wake was the best way to figure this out.&lt;/p&gt;

&lt;p&gt;Also, as stated early on, I do also have the “Prevent automatic sleeping on power adapter when the display is off” Battery setting enabled, meaning only locking my MacBook while docked should leave it in a full wake state even once the displays turn off automatically to save energy; only manually setting it to Sleep (which I do nightly or when leaving the MacBook for a while) should put it in a state where DarkWake events are occurring when these LaunchDaemons run.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-6--prepare-the-monitor--unmounteject-script&quot;&gt;Step 6 — Prepare the monitor + unmount/eject script&lt;/h2&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;nano /usr/local/sbin/tm-dock-monitor-unmount.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Paste:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;c&quot;&gt;#!/bin/zsh&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-euo&lt;/span&gt; pipefail

&lt;span class=&quot;nv&quot;&gt;VOL_PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/Volumes/TimeMachine&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;SCHEDULE_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/usr/local/etc/tm-dock/times.conf&quot;&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;STATE_DIR&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/tmp/tm-dock&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;FLAG_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$STATE_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/mounted_by_script&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$STATE_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Only unmount if we mounted it&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FLAG_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0

&lt;span class=&quot;c&quot;&gt;# Tight polling: fast for first 5 minutes, then slower&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;FAST_POLL_SECONDS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;20
&lt;span class=&quot;nv&quot;&gt;FAST_POLL_DURATION_SECONDS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;300   &lt;span class=&quot;c&quot;&gt;# 5 minutes&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;SLOW_POLL_SECONDS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;60
&lt;span class=&quot;nv&quot;&gt;MAX_MONITOR_SECONDS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;2700   &lt;span class=&quot;c&quot;&gt;# 45 minutes&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# This script is scheduled 1 minute after each backup time and monitors for up to &lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 45 minutes. These offsets define that window relative to each scheduled backup time.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;WINDOW_START_OFFSET&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1
&lt;span class=&quot;nv&quot;&gt;WINDOW_END_OFFSET&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;45

&lt;span class=&quot;c&quot;&gt;# Convert current time to minutes since midnight&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;now_min&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#$(date +%H)*60 + 10#$(date +%M)))&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Parse times.conf -&amp;gt; minutes from midnight, same as mount script.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;sched_mins&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;r line&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ &lt;span class=&quot;o&quot;&gt;^[[&lt;/span&gt;:space:]]&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# ]] &amp;amp;&amp;amp; continue&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ &lt;span class=&quot;o&quot;&gt;^[[&lt;/span&gt;:space:]]&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue
  if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ &lt;span class=&quot;o&quot;&gt;^([&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]{&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;,2&lt;span class=&quot;o&quot;&gt;})&lt;/span&gt;:&lt;span class=&quot;o&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]{&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;})[[&lt;/span&gt;:space:]]&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &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;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[1]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[2]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;
    sched_mins+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#$h*60 + 10#$m)))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi
done&lt;/span&gt; &amp;lt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SCHEDULE_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check whether the current time falls within [backup_time + 1, backup_time + 45]&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# for any scheduled backup. The if/else handles midnight wraparound: when start &amp;gt; end &lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# the window crosses midnight, so membership is tested with OR rather than AND.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;in_window&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;s &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;sched_mins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; WINDOW_START_OFFSET&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; WINDOW_END_OFFSET&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; start &amp;lt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; end &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; now_min &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; start &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; now_min &amp;lt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; end &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;in_window&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; now_min &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; start &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; now_min &amp;lt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; end &lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;in_window&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1
  &lt;span class=&quot;k&quot;&gt;fi
done&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; in_window &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; 1 &lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0

&lt;span class=&quot;c&quot;&gt;# Only proceed if the mount script set the flag, and the volume is still mounted.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# If the volume has already disappeared on its own, clean up the flag and exit.&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FLAG_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$VOL_PATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FLAG_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Returns true if Time Machine is actively running a backup.&lt;/span&gt;
is_tm_running&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  /usr/bin/tmutil status 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Running = 1&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Attempts to unmount the volume up to 12 times with 2-second pauses between&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# tries, to handle transient busy states (e.g. TM finishing a final write).&lt;/span&gt;
unmount_with_retries&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;i &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;1..12&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    if&lt;/span&gt; /usr/sbin/diskutil unmount &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$VOL_PATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
      return &lt;/span&gt;0
    &lt;span class=&quot;k&quot;&gt;fi
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;2
  &lt;span class=&quot;k&quot;&gt;done
  return &lt;/span&gt;1
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;begin_epoch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%s&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Poll until Time Machine finishes, then unmount. Uses a fast poll interval&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# for the first FAST_POLL_DURATION_SECONDS, then switches to a slower interval&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# to reduce unnecessary CPU wake-ups during long backups. Exits unconditionally&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# after MAX_MONITOR_SECONDS regardless of backup state, to avoid the script&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# running indefinitely if something goes wrong.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;elapsed&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%s&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; begin_epoch &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; elapsed &amp;lt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; MAX_MONITOR_SECONDS &lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0

  &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;is_tm_running&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; elapsed &amp;lt; FAST_POLL_DURATION_SECONDS &lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
      &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FAST_POLL_SECONDS&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else
      &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SLOW_POLL_SECONDS&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fi
  else
    &lt;/span&gt;unmount_with_retries &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
    rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$FLAG_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
  &lt;span class=&quot;k&quot;&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Make executable:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo chmod &lt;/span&gt;755 /usr/local/sbin/tm-dock-monitor-unmount.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note that the temporary “flag” file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;/tmp/tm-dock/mounted_by_script&quot;&lt;/code&gt; is the important piece of insurance here that prevents this script from ejecting the HDD unless the HDD was mounted automatically by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tm-dock-mount.sh&lt;/code&gt; in the first place (which creates the flag file).&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-7--prepare-the-script-that-builds-the-launchdaemons&quot;&gt;Step 7 — Prepare the script that builds the LaunchDaemons&lt;/h2&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;nano /usr/local/sbin/tm-dock-generate-plists.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Paste:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;c&quot;&gt;#!/bin/zsh&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-euo&lt;/span&gt; pipefail

&lt;span class=&quot;c&quot;&gt;# Reads backup times from times.conf and generates two LaunchDaemon plist&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# files: one to mount the Time Machine volume before each backup, and one&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# to monitor and unmount it afterward. Run this script with sudo whenever&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# times.conf is changed, then reload the daemons manually.&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Source of truth for backup schedule. Each line should be HH:MM format.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;SCHEDULE_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/usr/local/etc/tm-dock/times.conf&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Scripts invoked by the generated LaunchDaemons.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;MOUNT_SCRIPT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/usr/local/sbin/tm-dock-mount.sh&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;UNMOUNT_SCRIPT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/usr/local/sbin/tm-dock-monitor-unmount.sh&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# LaunchDaemons must live here to be loaded by launchd at boot.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;PLIST_DIR&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/Library/LaunchDaemons&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;MOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PLIST_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/com.tmdock.mount.plist&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;UNMOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PLIST_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/com.tmdock.unmount.plist&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PLIST_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Parse times.conf into a list of scheduled backup times in minutes since midnight. &lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Comments and blank lines are skipped. Hour and minute values are range-validated.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;mins&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; line&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ ^[[:space:]]&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# ]] &amp;amp;&amp;amp; continue&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ ^[[:space:]]&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue
  if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;~ ^&lt;span class=&quot;o&quot;&gt;([&lt;/span&gt;0-9]&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;1,2&lt;span class=&quot;o&quot;&gt;})&lt;/span&gt;:&lt;span class=&quot;o&quot;&gt;([&lt;/span&gt;0-9]&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;2&lt;span class=&quot;o&quot;&gt;})[[&lt;/span&gt;:space:]]&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &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;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[1]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[2]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; h &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; h &amp;lt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 23 &lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; m &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; m &amp;lt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 59 &lt;span class=&quot;o&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue
    &lt;/span&gt;mins+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#$h*60 + 10#$m)))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi
done&lt;/span&gt; &amp;lt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SCHEDULE_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Sort and deduplicate parsed times (@o = sort, @u = unique in zsh).&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Abort if no valid times were found.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;mins&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(@ou)mins&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;${#&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;mins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;No valid times in &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SCHEDULE_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Converts a time in minutes since midnight to a plist &amp;lt;dict&amp;gt; entry suitable&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# for use in a StartCalendarInterval array.&lt;/span&gt;
hm_dict&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;mm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;hh&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt; mm &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;mi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt; mm &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;      &amp;lt;dict&amp;gt;&amp;lt;key&amp;gt;Hour&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;hh&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;lt;/integer&amp;gt;&amp;lt;key&amp;gt;Minute&amp;lt;/key&amp;gt;&amp;lt;integer&amp;gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;mi&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;lt;/integer&amp;gt;&amp;lt;/dict&amp;gt;&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# For each scheduled backup time, derive the mount and unmount fire times:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#   mount:   2 minutes before the backup (ensures HDD is ready in time)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#   unmount: 1 minute after the backup (gives TM time to start before monitoring begins)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# +1440 % 1440 handles midnight wraparound for both offsets.&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;mount_dicts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;unmount_dicts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;s &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;mins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;mount_min&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;unmount_min&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;$((&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1440&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;))&lt;/span&gt;
  mount_dicts+&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;$'&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;hm_dict &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$mount_min&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  unmount_dicts+&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;$'&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;hm_dict &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$unmount_min&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Write the mount LaunchDaemon plist. StartCalendarInterval entries are&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# generated from the times derived above. RunAtLoad is false so the daemon&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# only fires at the scheduled times, not on boot.&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOFPL&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;com.tmdock.mount&amp;lt;/string&amp;gt;

    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;
      &amp;lt;string&amp;gt;/bin/zsh&amp;lt;/string&amp;gt;
      &amp;lt;string&amp;gt;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MOUNT_SCRIPT&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&amp;lt;/string&amp;gt;
    &amp;lt;/array&amp;gt;

    &amp;lt;key&amp;gt;StartCalendarInterval&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$mount_dicts&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    &amp;lt;/array&amp;gt;

    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;&amp;lt;false/&amp;gt;
    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;/tmp/com.tmdock.mount.out&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;/tmp/com.tmdock.mount.err&amp;lt;/string&amp;gt;
  &amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOFPL

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Write the unmount LaunchDaemon plist, structured identically to the mount&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# plist but using the unmount script and its derived fire times.&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$UNMOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOFPL&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;com.tmdock.unmount&amp;lt;/string&amp;gt;

    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;
      &amp;lt;string&amp;gt;/bin/zsh&amp;lt;/string&amp;gt;
      &amp;lt;string&amp;gt;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$UNMOUNT_SCRIPT&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&amp;lt;/string&amp;gt;
    &amp;lt;/array&amp;gt;

    &amp;lt;key&amp;gt;StartCalendarInterval&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$unmount_dicts&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    &amp;lt;/array&amp;gt;

    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;&amp;lt;false/&amp;gt;
    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;/tmp/com.tmdock.unmount.out&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;/tmp/com.tmdock.unmount.err&amp;lt;/string&amp;gt;
  &amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOFPL

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# LaunchDaemon plists must be owned by root:wheel with mode 644,&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# otherwise launchd will refuse to load them.&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;chown &lt;/span&gt;root:wheel &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$UNMOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;chmod &lt;/span&gt;644 &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$UNMOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Wrote:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$UNMOUNT_PLIST&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

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

&lt;p&gt;Then make executable:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo chmod &lt;/span&gt;755 /usr/local/sbin/tm-dock-generate-plists.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-8--generate-launchdaemons&quot;&gt;Step 8 — Generate LaunchDaemons&lt;/h2&gt;

&lt;p&gt;Run the script generated in Step 7:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo&lt;/span&gt; /usr/local/sbin/tm-dock-generate-plists.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-9--load-launchdaemons&quot;&gt;Step 9 — Load LaunchDaemons&lt;/h2&gt;

&lt;p&gt;Unregister old LaunchDaemons (important if updating them) and load new ones:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;launchctl bootout system /Library/LaunchDaemons/com.tmdock.mount.plist 2&amp;gt;/dev/null &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
sudo &lt;/span&gt;launchctl bootout system /Library/LaunchDaemons/com.tmdock.unmount.plist 2&amp;gt;/dev/null &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true

sudo &lt;/span&gt;launchctl bootstrap system /Library/LaunchDaemons/com.tmdock.mount.plist
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;launchctl bootstrap system /Library/LaunchDaemons/com.tmdock.unmount.plist
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootout&lt;/code&gt; commands are harmless on their first invocation (before you’ve registered any daemon to &lt;a href=&quot;https://en.wikipedia.org/wiki/Launchd&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launchd&lt;/code&gt;&lt;/a&gt;) and are critical to run before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootstrap&lt;/code&gt; for any future invocations when you want to reload the &lt;a href=&quot;https://en.wikipedia.org/wiki/Property_list&quot;&gt;plist&lt;/a&gt; files, which you would need to do if updating the backup schedule.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-10--configure-timemachineeditor&quot;&gt;Step 10 — Configure TimeMachineEditor&lt;/h2&gt;

&lt;p&gt;Open &lt;a href=&quot;https://tclementdev.com/timemachineeditor/&quot;&gt;TimeMachineEditor&lt;/a&gt; and set the same times as in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;times.conf&lt;/code&gt;, with the “Calendar Intervals” setting:&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/files/TimeMachineEditor_schedule.png&quot; /&gt;&lt;/div&gt;

&lt;p&gt;After all these steps, now the disk will:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mount 2 minutes before the scheduled backup&lt;/li&gt;
  &lt;li&gt;Run its Time Machine backup&lt;/li&gt;
  &lt;li&gt;Unmount immediately after completion&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-11---disable-standby-while-on-ac-power-optional&quot;&gt;Step 11 - disable “standby” while on AC power (optional?)&lt;/h2&gt;

&lt;p&gt;So, I have marked this as “optional?” because it was necessary for me to eliminate all remaining mystery random spin-ups (even with the HDD remaining unmounted…), but I never really identified if this was something unique to my specific dock and/or HDD or if this was a more general thing.&lt;/p&gt;

&lt;p&gt;Basically, even after all of the above, I was still having cases where I’d hear the HDD spin up randomly in the middle of the night when the MacBook was set to Sleep, sometimes even flashing on my external displays, despite ensuring the HDD was ejected/unmounted before setting the computer to Sleep. This weirdness would also sometimes leave me with “Disk not properly ejected” notifications when I next got back onto my MacBook, or even with the HDD mysteriously mounted.&lt;/p&gt;

&lt;p&gt;The culprit was identified as the “standby” power mode. So, this is more of a separate power management behavior issue rather than something related to Time Machine or the LaunchDaemons here at all.&lt;/p&gt;

&lt;p&gt;The solution was simply to disable standby (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;standby 0&lt;/code&gt;) only while on AC power (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-c&lt;/code&gt;):&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;pmset &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; standby 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After setting this, I no longer had any random unexpected HDD spin-ups while the MacBook should otherwise be set to Sleep.&lt;/p&gt;

&lt;p&gt;If you’re curious about your own standby power setting, you can execute:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pmset &lt;span class=&quot;nt&quot;&gt;-g&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;standby
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you ever need to restore it default behavior later:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;pmset &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; standby 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;step-12--manual-mount-shortcut-optional-but-strongly-recommended&quot;&gt;Step 12 — Manual mount Shortcut (optional but strongly recommended)&lt;/h2&gt;

&lt;p&gt;This step creates the Shortcut that you can simply run in macOS to manually mount the HDD without needing to unplug and replug anything physically, very useful if you ever want to mount it to just inspect the Time Machine backups or restore any files.&lt;/p&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;nano /usr/local/sbin/tm-dock-manual-mount.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Paste:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;c&quot;&gt;#!/bin/zsh&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-euo&lt;/span&gt; pipefail

&lt;span class=&quot;nv&quot;&gt;TM_APFS_UUID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PUT_YOUR_APFS_VOLUME_UUID_HERE&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;PASSFILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/usr/local/etc/tm-dock/passphrase&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;VOL_PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/Volumes/TimeMachine&quot;&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$VOL_PATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0

&lt;span class=&quot;c&quot;&gt;# Unlock (if locked). If already unlocked, this prints an error; we ignore it.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Note: unlocking may mount automatically, but not always.&lt;/span&gt;
/usr/sbin/diskutil apfs unlockVolume &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TM_APFS_UUID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-stdinpassphrase&lt;/span&gt; &amp;lt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PASSFILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# If still not mounted, determine the current disk identifier (e.g. disk7s2) and mount it.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$VOL_PATH&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&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;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISK_ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;/usr/sbin/diskutil apfs list | /usr/bin/awk &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TM_APFS_UUID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'
    $0 ~ uuid { getline; if ($1 == &quot;APFS&quot; &amp;amp;&amp;amp; $2 == &quot;Volume&quot; &amp;amp;&amp;amp; $3 == &quot;Disk&quot; &amp;amp;&amp;amp; $4 ~ /^\(Role\):$/) { print $5; exit }
             else if ($1 == &quot;APFS&quot; &amp;amp;&amp;amp; $2 == &quot;Volume&quot; &amp;amp;&amp;amp; $3 == &quot;Disk&quot;) { print $4; exit } }
  '&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Fallback parsing (more robust): look for &quot;APFS Volume Disk (Role): diskXsY&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISK_ID&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;:-}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&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;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISK_ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;/usr/sbin/diskutil apfs list | /usr/bin/awk &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$TM_APFS_UUID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'
      $0 ~ uuid { found=1 }
      found &amp;amp;&amp;amp; /APFS Volume Disk/ { for (i=1;i&amp;lt;=NF;i++) if ($i ~ /^disk[0-9]+s[0-9]+$/) { print $i; exit } }
    '&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi

  if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISK_ID&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;:-}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&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;k&quot;&gt;then&lt;/span&gt;
    /usr/sbin/diskutil mount &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$DISK_ID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fi
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Make sure to replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;PUT_YOUR_APFS_VOLUME_UUID_HERE&quot;&lt;/code&gt; with your UUID from Step 1.&lt;/p&gt;

&lt;p&gt;Make executable:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo chmod &lt;/span&gt;755 /usr/local/sbin/tm-dock-manual-mount.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Create a Shortcut that runs:&lt;/p&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo&lt;/span&gt; /usr/local/sbin/tm-dock-manual-mount.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’ve never made a shortcut before (and oh boy you’re in for a wold of automation goodness on macOS/iOS), then you can find the Shortcuts app via Spotlight, click the “+” to make a new Shortcut, search for the “Run Shell Script” action, and configure it as shown below, making sure to enable “Run as Administrator”.&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/files/mount_HDD_Shortcut.png&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Then, you can access the Shortcut however you please. I personally like having it in the Shortcuts icon in my menu bar, but you can always run it directly from the Shortcuts app, via Siri, via a dock icon, or even a custom keyboard shortcut.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;updating-the-scheduled-times&quot;&gt;Updating the scheduled times&lt;/h1&gt;

&lt;p&gt;If you ever want to change your backup schedule, then you essentially only need to repeat Steps 3, 8, 9, and 10. That is, you just need to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;update the times listed in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/etc/tm-dock/times.conf&lt;/code&gt; (edit with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo nano&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;and in TimeMachineEditor to whatever new times you want, then&lt;/li&gt;
  &lt;li&gt;run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/sbin/tm-dock-generate-plists.sh&lt;/code&gt; script (run with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo&lt;/code&gt;) to generate the updated LaunchDaemon files,&lt;/li&gt;
  &lt;li&gt;and finally reload them with:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-bash 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;nb&quot;&gt;sudo &lt;/span&gt;launchctl bootout system /Library/LaunchDaemons/com.tmdock.mount.plist 2&amp;gt;/dev/null &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
sudo &lt;/span&gt;launchctl bootout system /Library/LaunchDaemons/com.tmdock.unmount.plist 2&amp;gt;/dev/null &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true

sudo &lt;/span&gt;launchctl bootstrap system /Library/LaunchDaemons/com.tmdock.mount.plist
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;launchctl bootstrap system /Library/LaunchDaemons/com.tmdock.unmount.plist
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;serenity-achieved&quot;&gt;Serenity achieved&lt;/h1&gt;

&lt;p&gt;What began as a naive “surely it can’t be that hard to keep my HDD quiet except when minimally necessary?” question spiraled into a full-on macOS troubleshooting saga, but I am so pleased to have finally fully solved this issue, to have my noisy HDD spin-ups only occurring on a fully deterministic basis.  While I admittedly think this really should be easier to solve for the average user rather than solely for power-users comfortable with not only terminal commands but with invoking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo&lt;/code&gt; permissions too, I am glad it is at least &lt;em&gt;solvable&lt;/em&gt; without needing to resort to any horrifically janky solutions. (At some point I was contemplating the viability of having some physical switch on my desk to toggle the HDD’s connection to the dock or to power…)&lt;/p&gt;

&lt;p&gt;I will add that, as of writing this, I am no expert in macOS at all; I’m still very fresh to macOS but with a fair amount of professional experience operating on remote Linux systems. This was a huge learning experience for me, and I don’t necessarily regret being forced to learn all sorts of nitty-gritty details of &lt;a href=&quot;https://en.wikipedia.org/wiki/Pmset&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pmset&lt;/code&gt;&lt;/a&gt; power management and states, &lt;a href=&quot;https://en.wikipedia.org/wiki/Launchd#launchctl&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launchctl&lt;/code&gt;&lt;/a&gt; LaunchDaemons, &lt;a href=&quot;https://ss64.com/mac/diskutil.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;diskutil&lt;/code&gt;&lt;/a&gt; disk utility mounting with APFS-encrypted volumes, &lt;a href=&quot;https://ss64.com/mac/tmutil.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tmutil&lt;/code&gt;&lt;/a&gt; Time Machine utility, and other bits and pieces, as “learning through practical projects” is my preferred style, and maybe I’ll be able to apply this knowledge to future automation and scheduling tasks. That said, I &lt;em&gt;really&lt;/em&gt; hope any future problems I want to solve on macOS will have much simpler solutions. 😅&lt;/p&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="macOS" /><category term="Time Machine" /><category term="external HDD" /><category term="noise" /><category term="quiet" /><category term="LaunchDaemon" /><category term="shell scripting" /><category term="automation" /><category term="scheduling" /><category term="pmset" /><category term="APFS" /><summary type="html"></summary></entry><entry><title type="html">A primer on construction, selection, and filtering of neutron and gamma-ray event cones, applied to simple back projection imaging</title><link href="https://lindt8.github.io/posts/primer-physical-data-to-cones-to-images/" rel="alternate" type="text/html" title="A primer on construction, selection, and filtering of neutron and gamma-ray event cones, applied to simple back projection imaging" /><published>2026-03-11T00:00:00-07:00</published><updated>2026-03-11T00:00:00-07:00</updated><id>https://lindt8.github.io/posts/blog-post-12</id><content type="html" xml:base="https://lindt8.github.io/posts/primer-physical-data-to-cones-to-images/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/files/imaging-primer_physical-data-to-images.pdf&quot;&gt;[Direct link to full PDF of this article]&lt;/a&gt; &lt;a href=&quot;https://doi.org/10.5281/zenodo.18979385&quot;&gt;&lt;img src=&quot;https://zenodo.org/badge/DOI/10.5281/zenodo.18979385.svg&quot; alt=&quot;DOI&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Abstract:&lt;/em&gt;&lt;/strong&gt; This document details the methodology for building, selecting, and filtering neutron and gamma-ray event cones for imaging in the context of the detector system composed of long-form-factor organic plastic scintillators developed in the &lt;a href=&quot;https://www.novo-project.eu/&quot;&gt;NOVO project&lt;/a&gt; and as utilized in the &lt;a href=&quot;https://github.com/Lindt8/ng-imager&quot;&gt;ng-imager Python toolkit&lt;/a&gt; for neutron and gamma-ray imaging. A summary of the simple back projection method implemented is provided too. This work employs &lt;a href=&quot;https://phits.jaea.go.jp/&quot;&gt;PHITS&lt;/a&gt; simulations of a simple six-bar detector array, providing data free of resolution effects and accompanied by extra “Monte Carlo truth” information to benchmark reconstruction method results against.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;An experimental data pipeline, such as in the &lt;a href=&quot;https://www.novo-project.eu/&quot;&gt;NOVO project&lt;/a&gt;, can be generally divided into five distinct main stages, requiring analysis procedures to progress from one stage to the next:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Raw detector data (integrated charges, timestamps, waveforms)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Physical quantities derived (energy deposited, interaction position, times)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Coincident events (physical quantities from double neutron / triple gamma-ray interaction coincidences, fulfilling various criteria)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Event cones (vertex, direction, half-angle)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Imaging (projection of event cones onto an imaging plane)&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This work outlines the procedure for constructing neutron and gamma-ray event cones provided derived physical quantities from coincident events. Each particle contains some unique challenges in this regard. For neutrons, this includes determining the reaction’s nature (elastic scattering, inelastic scattering, or other nonelastic) and target (proton or carbon nucleus). For gamma rays, this includes determining the correct sequencing of the three reactions in coincidence to yield the most probable valid event cone. A condensed outline of the analytically-solved simple back projection imaging implementation used in &lt;a href=&quot;https://github.com/Lindt8/ng-imager&quot;&gt;ng-imager&lt;/a&gt; in “scan” mode is also provided.&lt;/p&gt;

&lt;h1 id=&quot;simulation-setup&quot;&gt;Simulation setup&lt;/h1&gt;

&lt;p&gt;The simulations were conducted in &lt;a href=&quot;https://phits.jaea.go.jp/&quot;&gt;PHITS&lt;/a&gt; (v3.29) [1] and set up as follows. Six parallel plastic scintillator bars of dimensions 12$\times$&lt;!-- --&gt;12$\times$&lt;!-- --&gt;140 mm$^3$ were arranged in a $3\times2$ pattern with a center-to-center spacing of 60 mm (48 mm gap width) to account for some real-world clearance for mounting hardware. The simulated design is pictured in Figure 1.&lt;/p&gt;

&lt;div id=&quot;geo_parallel_3d2t&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/geo_parallel_3d2t.png&quot; style=&quot;width:80%; display:block; margin:auto;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 1.&lt;/strong&gt; Simulated plastic scintillator array&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;In all neutron simulations, the source was an isotropic, 14.1 MeV monoenergetic neutron source placed 100 cm from the front face of the detector array and in line with its center. From the perspective of Fig. 1, the neutrons enter the picture from the lower left to then strike the array. While 100 million histories were simulated, the source was modeled as a cone with angle $\theta=5.5^\circ$ pointed toward the array, allowing simulation of only neutrons bound for the array. Owing to the cone’s 0.0289 sr solid angle, this was the equivalent of simulating 43.4 billion ($4.344\times10^{10}$) deuterium-tritium (DT) fusion events.&lt;/p&gt;

&lt;p&gt;Provided the array is placed far enough from the source such that this source-to-array cone is only a few degrees wide, this monoenergetic assumption is valid. Likewise, a DT source is close enough to being completely isotropic for this assumption to hold as well.&lt;/p&gt;

&lt;p&gt;Gamma-ray simulations employed a 2 MeV monoenergenic gamma-ray source otherwise identical to the neutron source described above. 1 billion histories (or an effective 434 billion emissions, corrected for solid angle) were simulated.&lt;/p&gt;

&lt;p&gt;In a list mode fashion, neutron and gamma-ray interactions were recorded to “dump” files, and in post multiple scattering events were constructed, sequenced, and put into a new list for further tallying and analysis. Furthermore, neutron simulations included two tallies for interactions in the bars: one of only elastic scattering reactions and one for all nuclear reactions (including elastic scattering).&lt;/p&gt;

&lt;p&gt;For the sake of events considered valid for imaging, only neutron double coincidences where each scatter deposited at least 500 keV were considered. This threshold was set to 100 keV for each of the three gamma-ray interactions.&lt;/p&gt;

&lt;p&gt;Nuclear data libraries (JENDL-4.0) were employed for transport of neutrons $&amp;lt;20$ MeV instead of models (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nucdata=1&lt;/code&gt;), and the event generator mode was enabled (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e-mode=2&lt;/code&gt;) for accurate event-wise reaction data.&lt;/p&gt;

&lt;h1 id=&quot;cone-construction-and-selection&quot;&gt;Cone construction and selection&lt;/h1&gt;

&lt;p&gt;Ultimately, each cone is defined by a vertex coordinate $O=(x_O,y_O,z_O)$ with an axis/direction vector $\hat{D}=\langle u,v,w \rangle$ and interior half-angle $\theta$. These are derived from the following physical quantities extracted from experimental data (and emulated in simulation):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;For neutron events:&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;
        &lt;p&gt;1&lt;sup&gt;st&lt;/sup&gt; scatter location $X_1=(x_1,y_1,z_1)$, time $t_1$, and energy deposited $E_{\mathrm{dep},1}$&lt;/p&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;2&lt;sup&gt;nd&lt;/sup&gt; scatter location $X_2=(x_2,y_2,z_2)$ and time $t_2$&lt;/p&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;For gamma-ray events:&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;
        &lt;p&gt;1&lt;sup&gt;st&lt;/sup&gt; scatter location $X_1=(x_1,y_1,z_1)$ and energy deposited $E_{\mathrm{dep},1}$&lt;/p&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;2&lt;sup&gt;nd&lt;/sup&gt; scatter location $X_2=(x_2,y_2,z_2)$ and energy deposited $E_{\mathrm{dep},2}$&lt;/p&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;3&lt;sup&gt;rd&lt;/sup&gt; scatter location $X_3=(x_3,y_3,z_3)$ (and energy deposited $E_{\mathrm{dep},3}$)&lt;/p&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The process of creating event cones from this experimental/simulated data is referred to here as “cone construction.” For both neutrons and gamma rays, the vertex coordinate $O$ is the location of the first scatter, the direction vector $\hat{D}$ is the unit vector pointing from the second scatter to the first scatter, and the cone half-angle $\theta$ is the scattering angle of the first collision. The $\theta$ calculation differs for neutrons and gamma rays.&lt;/p&gt;

&lt;h3 id=&quot;neutron-cone-half-angle-mathbftheta&quot;&gt;Neutron cone half-angle $\mathbf{\theta}$&lt;/h3&gt;

&lt;p&gt;For neutrons scattering elastically, the scattering angle in the center-of-mass (CoM) reference frame $\theta_{\mathrm{n,CoM}}$ is defined, with recoil nucleus energy $E_{\mathrm{recoil}}$, incident neutron energy $E_\mathrm{n}$, and ratio of the recoil nucleus’s mass to the neutron mass $A$ (Equation &lt;a href=&quot;#atomicmass&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;atomicmass&quot;&gt;(1)&lt;/a&gt;), as [2]:&lt;/p&gt;

&lt;p&gt;\begin{equation}
A = \frac{m_\mathrm{recoil}}{m_\mathrm{n}}
\label{atomicmass}
\end{equation}&lt;/p&gt;

&lt;p&gt;\begin{equation}
\theta_{\mathrm{n,CoM}} =
\arccos\left(
1 - \left(
\frac{E_\mathrm{recoil}}{E_\mathrm{n}}
\cdot
\frac{(1+A)^2}{2A}
\right)
\right)
\label{theta_CoM}
\end{equation}&lt;/p&gt;

&lt;p&gt;Experimentally, $E_\mathrm{recoil}$ is taken to be the energy deposited in the first scattering reaction $E_{\mathrm{dep},1}$. The scattered neutron’s energy $E_\mathrm{n}^\prime$ is determined with its time-of-flight (Equation &lt;a href=&quot;#neutron_tof&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;neutron_tof&quot;&gt;(4)&lt;/a&gt;) and flight distance $s_\mathrm{n}$ (Cartesian distance between $X_1$ and $X_2$, Equation &lt;a href=&quot;#neutron_flightpath&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;neutron_flightpath&quot;&gt;(5)&lt;/a&gt;) relativistically, with speed of light $c$, as:&lt;/p&gt;

&lt;p&gt;\begin{equation}
E_\mathrm{recoil} = E_{\mathrm{dep},1}
\label{neutron_Erecoil}
\end{equation}&lt;/p&gt;

&lt;p&gt;\begin{equation}
t_\mathrm{n,ToF} = t_2 - t_1
\label{neutron_tof}
\end{equation}&lt;/p&gt;

&lt;p&gt;\begin{equation}
s_\mathrm{n}
=
\left| \vec{X_1X_2} \right|
=
\sqrt{
(x_1-x_2)^2 +
(y_1-y_2)^2 +
(z_1-z_2)^2
}
\label{neutron_flightpath}
\end{equation}&lt;/p&gt;

&lt;p&gt;\begin{equation}
v_\mathrm{n} = \frac{s_\mathrm{n}}{t_\mathrm{n,ToF}}
\label{neutron_velocity}
\end{equation}&lt;/p&gt;

&lt;p&gt;\begin{equation}
E_\mathrm{n}^\prime =
\left(
\sqrt{\frac{1}{1-(v_\mathrm{n}/c)^2}} - 1
\right)
m_\mathrm{n}c^2
\label{neutron_KE}
\end{equation}&lt;/p&gt;

&lt;p&gt;For completeness’s sake, this would be calculated classically (non-relativistically) as shown in Equation &lt;a href=&quot;#neutron_KE_classic&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;neutron_KE_classic&quot;&gt;(8)&lt;/a&gt;. As shown in Figure 2, in the neutron energies relevant to proton therapy, the classical approach to calculating $E_\mathrm{n}$ deviates quite notably from the relativistic approach.&lt;/p&gt;

&lt;p&gt;\begin{equation}
E_\mathrm{n,\,non\text{-}relativistic}^\prime
=
\frac{1}{2}m_\mathrm{n}v_\mathrm{n}^2
\label{neutron_KE_classic}
\end{equation}&lt;/p&gt;

&lt;div id=&quot;En_classic-vs-relativistic&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/En_classic-vs-relativistic.svg&quot; style=&quot;width:95.0%&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 2.&lt;/strong&gt; What classical mechanics would predict a neutron’s kinetic energy to be at each true kinetic energy (as calculated relativistically using the same neutron velocity).&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;The incident neutron energy $E_\mathrm{n}$ is simply the sum of the scattered neutron energy $E_\mathrm{n}^\prime$ and the energy of the recoil product $E_\mathrm{recoil}$ (Equation &lt;a href=&quot;#neutron_incident_KE&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;neutron_incident_KE&quot;&gt;(9)&lt;/a&gt;). Note that this assumption fails when the recoil product does not deposit all of its energy in the detection volume (and also fails when the reaction is nonelastic).&lt;/p&gt;

&lt;p&gt;\begin{equation}
E_\mathrm{n} = E_\mathrm{n}^\prime + E_\mathrm{recoil}
\label{neutron_incident_KE}
\end{equation}&lt;/p&gt;

&lt;p&gt;The neutron scattering angle in the lab frame $\theta_{\mathrm{n,lab}}$ is found from $\theta_{\mathrm{n,CoM}}$ with:&lt;/p&gt;

&lt;p&gt;\begin{equation}
\theta_{\mathrm{n,lab}}
=
\arctan\left(
\frac{\sin(\theta_{\mathrm{n,CoM}})}
{\cos(\theta_{\mathrm{n,CoM}}) + (1/A)}
\right)
\label{theta_lab}
\end{equation}&lt;/p&gt;

&lt;p&gt;Strictly speaking, the $\mathop{\mathrm{atan2}}$ function, rather than $\arctan$, is used here as the desired output angle should range from 0 to $\pi$, not $-\pi/2$ to $\pi/2$. To avoid this, $\theta_{\mathrm{n,lab}}$ may also be equivalently expressed as [3]:&lt;/p&gt;

&lt;p&gt;\begin{equation}
\theta_{\mathrm{n,lab}}
=
\arccos\left(
\frac{1 + A\cos(\theta_{\mathrm{n,CoM}})}
{\sqrt{A^2 + 2A\cos(\theta_{\mathrm{n,CoM}}) + 1}}
\right)
\label{theta_lab_v2}
\end{equation}&lt;/p&gt;

&lt;p&gt;For the ease of discussion, when mentioning the neutron cone half-angle $\theta$ with no subscript, it is in reference to the lab frame $\theta_{\mathrm{n,lab}}$.&lt;/p&gt;

&lt;h3 id=&quot;gamma-ray-cone-half-angle-mathbftheta&quot;&gt;Gamma-ray cone half-angle $\mathbf{\theta}$&lt;/h3&gt;

&lt;p&gt;For a gamma ray undergoing solely Compton scattering in its first two interactions, the scattering angle of the first collision $\theta_{\gamma,1}$ can be found via Compton scattering kinematics [4]. First, the scattering angle of the second interaction $\theta_{\gamma,2}$ is calculated from the vectors $\overrightarrow{X_1X_2}$ and $\overrightarrow{X_2X_3}$ connecting the first to second scatter location and the second to third scatter location, respectively.&lt;/p&gt;

&lt;p&gt;\begin{equation}
\theta_{\gamma,2}
=
\arccos\left(
\frac{
\vec{X_1X_2}\cdot\vec{X_2X_3}
}{
|\vec{X_1X_2}||\vec{X_2X_3}|
}
\right)
\label{eq:theta_gamma2}
\end{equation}&lt;/p&gt;

&lt;p&gt;Assuming that the energy lost by the gamma ray in each scatter is equal to the energy deposited $\Delta E_i = E_{\mathrm{dep},i}$ (true of Compton scattering events whose recoil electrons do not escape), the initial incident gamma ray’s energy $E_\gamma$ can be calculated as:&lt;/p&gt;

&lt;p&gt;\begin{equation}
E_\gamma =
\Delta E_1
+
\frac{1}{2}
\left(
\Delta E_2 +
\sqrt{
\Delta E_2^2 +
\frac{4\Delta E_2 m_e c^2}{1-\cos(\theta_{\gamma,2})}
}
\right)
\label{eq:Egamma}
\end{equation}&lt;/p&gt;

&lt;p&gt;Then, the gamma ray’s energy after the first scatter $E_\gamma^\prime$ is simply the difference between this initial energy and the amount lost in the first scatter:&lt;/p&gt;

&lt;p&gt;\begin{equation}
E_\gamma^\prime = E_\gamma - \Delta E_1
\label{eq:E_gamma_prime}
\end{equation}&lt;/p&gt;

&lt;p&gt;And the first scattering angle $\theta_{\gamma,1}$ is:&lt;/p&gt;

&lt;p&gt;\begin{equation}
\theta_{\gamma,1}
=
\arccos\left(
1 + m_ec^2
\left[
\frac{1}{E_\gamma} - \frac{1}{E_\gamma^\prime}
\right]
\right)
\label{eq:theta_gamma}
\end{equation}&lt;/p&gt;

&lt;p&gt;And thus the gamma-ray cone half-angle $\theta$ is found. It should be noted that all gamma-ray math is already using relativistic mechanics.&lt;/p&gt;

&lt;p&gt;There are complications for both neutron and gamma-ray cone half-angle calculations. For neutrons, determining whether a scatter was with a proton or carbon nucleus is a challenge, and assuming that all interactions are elastic scatters is not always valid. In nonelastic reactions, the change in the neutron’s energy is no longer always equal to the energy deposited in the detector. For gamma rays, determining the actual order of the scattering events experimentally is a much greater challenge as the time values are so close to each other as to be unusable for correctly sequencing the scatters. These challenges are addressed in the following subsections.&lt;/p&gt;

&lt;h2 id=&quot;neutron-cone-generation&quot;&gt;Neutron cone generation&lt;/h2&gt;

&lt;p&gt;The vast majority of neutron scattering events fall into one of the following categories:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Elastic scatter with protons&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Elastic scatter with carbon nucleus&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Inelastic scatter / nuclear reaction with carbon nucleus&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other channels exist (such as &lt;sup&gt;1&lt;/sup&gt;H(n,$\gamma$)) but have significantly lower cross section, and that last category of nonelastic reactions with carbon can be further broken down into individual reaction channels. For simplicity’s sake, we’ll begin with the simulated results where only elastic scattering events were tallied. Figure 3 shows a comparison of the calculated values of $\theta$ versus the Monte Carlo (MC) true values of $\theta$, assuming &lt;em&gt;all&lt;/em&gt; reactions are with either protons or carbon nuclei.&lt;/p&gt;

&lt;div style=&quot;text-align:center;&quot;&gt;
  &lt;img src=&quot;/files/cones/theta-calculated-vs-mc-truth_elastic_only_assume_all_protons.svg&quot; style=&quot;width:95%;&quot; /&gt;
  &lt;p&gt;&lt;em&gt;(a) assuming all recoils with protons&lt;/em&gt;&lt;/p&gt;
  &lt;img src=&quot;/files/cones/theta-calculated-vs-mc-truth_elastic_only_assume_all_carbons.svg&quot; style=&quot;width:95%;&quot; /&gt;
  &lt;p&gt;&lt;em&gt;(b) assuming all recoils with carbon&lt;/em&gt;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;Figure 3.&lt;/strong&gt; Comparison of calculated and MC truth scattering angles $\theta$ when assuming all scatters occur with the same target nucleus (only including elastic scattering events).&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;There are two clear lines in each, with one matching the MC truth values, letting us know these lines do indeed correspond to elastic scattering with protons and carbon nuclei. (One should note that Figure 3b only looks “cleaner” owing to the fact that all protons of $\theta_{\mathrm{MC truth}}\gtrapprox0.56$ end up with an $\arccos$ argument in Equation &lt;a href=&quot;#theta_CoM&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;theta_CoM&quot;&gt;(2)&lt;/a&gt; $&amp;gt; \lvert 1 \rvert$, resulting in a NaN value of $\theta$ which propagates through the remainder of the calculation and is ultimately not plotted.) This presents a challenge though, without knowledge of the MC truth, how are we to decide whether a given event corresponds to a proton or carbon scattering?&lt;/p&gt;

&lt;p&gt;In most real situations, one should have some approximate idea of where the source is located. In this simulated case, it is a true point source, but at far enough distances or small enough sources, this assumption is transferable to the real world. Thus, we can simply calculate $\theta$ for both the proton and carbon scatters, compare those angles to the angle $\theta_\mathrm{est}$ between $\hat{D}$ and the vector from the cone vertex $O$ to the estimated source location, and pick whichever is closest. This procedure yields Figure 4.&lt;/p&gt;

&lt;div id=&quot;theta_elas_guess&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/theta-calculated-vs-mc-truth_elastic_only_guess_target.svg&quot; style=&quot;width:95.0%&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 4.&lt;/strong&gt; Comparison of calculated and MC truth scattering angles &lt;span class=&quot;math inline&quot;&gt;&lt;em&gt;θ&lt;/em&gt;&lt;/span&gt; when calculating angles for both proton and carbon scatters and picking the one closest to the estimated angle between the direction vector and the vector from the vertex to source location. &lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;At least in this case, the method works quite well! This discrimination approach certainly would need to be adjusted in more clinical scenarios where the source term is distributed and the detector array would be positioned closer to it. Perhaps such discrimination could incorporate additional experimental data.&lt;/p&gt;

&lt;p&gt;Moving on, Figure 5 shows this methodology applied to the simulation results containing &lt;em&gt;all&lt;/em&gt; nuclear reactions, not just elastic scattering.&lt;/p&gt;

&lt;div id=&quot;theta_nuclear_guess&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/theta-calculated-vs-mc-truth_nuclear_guess_target.svg&quot; style=&quot;width:95.0%&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 5.&lt;/strong&gt; Same as Figure 4 but for &lt;em&gt;all&lt;/em&gt; nuclear interactions, not just elastic scattering.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;While the elastic scattering line is still very prominent, some new interesting features emerge from inelastic and other nonelastic reactions&lt;sup id=&quot;fnref:1&quot; role=&quot;doc-noteref&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; with carbon. One approach would be to simply filter out these new events by rejecting $\theta$ values which exceed some absolute or fractional difference from the estimated $\theta_\mathrm{est}$. This is not the most future-proof approach for the aforementioned reasons, but it is simple and quite effective, as illustrated for a 5% fractional difference threshold in Figure 6.&lt;/p&gt;

&lt;div id=&quot;theta_nuclear_guess_5pcthresh&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/theta-calculated-vs-mc-truth_nuclear_guess_target_5pctcut.svg&quot; style=&quot;width:95.0%&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 6.&lt;/strong&gt; Same as Figure 5 but eliminating all events whose calculated &lt;span class=&quot;math inline&quot;&gt;&lt;em&gt;θ&lt;/em&gt;&lt;/span&gt; closest to &lt;span class=&quot;math inline&quot;&gt;&lt;em&gt;θ&lt;/em&gt;&lt;sub&gt;est&lt;/sub&gt;&lt;/span&gt; deviates by more than 5% from it. Of these 10322 event cones, 7501 were attributed to proton recoils and 2821 to carbon recoils.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This approach / last step is more for illustration and should &lt;strong&gt;NOT&lt;/strong&gt; used experimentally as it also “forces the right answer”, i.e., we only image the cones that yield the image we’re expecting. In experimental work, the fairer angular constraint to place on the cones is that they be pointing in the general $2\pi$ direction toward the source. Thus, the net effect of including all of these nonelastic scattering events is added noise/blurring in the neutron images.&lt;/p&gt;

&lt;p&gt;It may be possible that nonelastic events can be considered in some more clever way, rather than just being a source of noise, but this seems unlikely to be feasible experimentally, as the experimental data is not remotely as clean as the MC truth data presented here, making it seemingly impossible to distinguish elastic and nonelastic interactions in any practical and fair way. Still, with good pulse shape discrimination, it may be possible to reliably distinguish proton from carbon recoils as well as the alphas from carbon breakup reactions.&lt;/p&gt;

&lt;h2 id=&quot;gamma-ray-cone-generation&quot;&gt;Gamma-ray cone generation&lt;/h2&gt;

&lt;p&gt;The primary challenge in generating event cones for gamma rays is event sequencing. Owing to the tight bar spacing in NOVO, time resolution of the measurement apparatus, and light-speed velocity of gamma rays, the experimentally produced timestamps for gamma rays are effectively meaningless for determining the actual order of the three coincident interactions. Thus, some other approach must be developed for determining the true, or at least most probable, sequencing of the three interactions.&lt;/p&gt;

&lt;p&gt;Fortunately, there are only six possible arrangements for three interactions (123, 132, 213, 231, 312, and 321), and all combinations can be tested. When solving Equation &lt;a href=&quot;#eq:theta_gamma&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;eq:theta_gamma&quot;&gt;(15)&lt;/a&gt; for $\theta_1$, the kinematically nonsensical arrangements will make themselves evident by yielding a value outside of $[-1,1]$ for the argument of $\arccos$ or returning a $\theta_1$ of 0. Eliminating these leaves only the potentially valid sequences of the three interactions. Plotting all these calculated $\theta$ values against the Monte Carlo true $\theta$ for the simulation of a 2 MeV gamma-ray source described earlier yields Figure 7.&lt;/p&gt;

&lt;div id=&quot;theta_gamma_all_valid&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/gamma_theta-calculated-vs-mc-truth_all-valid-cones.svg&quot; style=&quot;width:95.0%&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 7.&lt;/strong&gt; Comparison of calculated and MC truth gamma-ray scattering angles &lt;span class=&quot;math inline&quot;&gt;&lt;em&gt;θ&lt;/em&gt;&lt;/span&gt; for all valid gamma-ray event cones in each event&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Here, a clear line is present consisting of the correctly calculated $\theta$ values, but they are in a sea of faulty cones. While one could image all of these cones, it would be more computationally expensive and produce a noisier image. The next logical step would be, rather than selecting all potentially valid cones for each event, to select the “best” cone among the valid ones for each event. At present, in a similar fashion to the neutron cone generation, all valid cone angles are compared to the angle $\theta_\mathrm{est}$ between $\hat{D}$ and the vector from the cone vertex $O$ to the estimated source location, and whichever is closest is picked as the “best” cone. This is shown in Figure 8.&lt;/p&gt;

&lt;div id=&quot;theta_gamma_best&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/gamma_theta-calculated-vs-mc-truth_only-best-cones.svg&quot; style=&quot;width:95.0%&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 8.&lt;/strong&gt; Same as Figure 7 but only selecting for each event the “best” cone whose &lt;span class=&quot;math inline&quot;&gt;&lt;em&gt;θ&lt;/em&gt;&lt;/span&gt; is closest to the estimated angle &lt;span class=&quot;math inline&quot;&gt;&lt;em&gt;θ&lt;/em&gt;&lt;sub&gt;est&lt;/sub&gt;&lt;/span&gt; between the direction vector and the vector from the vertex to source location.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This eliminates almost all errant $\theta$ values. Given that the remaining calculated $\theta$ deviating from the Monte Carlo true $\theta$ are already the “best” of the possible valid $\theta$, it makes sense to conclude that these events correspond to situations where the earlier assumptions (that all interactions are Compton scattering and that all recoil electrons stop in the bars) prove to be false. This, however, is only a very tiny minority of events. Similar to the neutron cone generation method, an arbitrary maximum deviation from an estimated $\theta_\mathrm{est}$ can be enforced and is illustrated for a 5% fractional difference threshold in Figure 9. (Again, this final cleaning step should &lt;strong&gt;NOT&lt;/strong&gt; done for experimental data.)&lt;/p&gt;

&lt;div id=&quot;theta_gamma_guess_5pcthresh&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/gamma_theta-calculated-vs-mc-truth_only-best-cones_guess-target-5pctcut.svg&quot; style=&quot;width:95.0%&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 9.&lt;/strong&gt; Same as Figure 8 but eliminating all events whose calculated &lt;span class=&quot;math inline&quot;&gt;&lt;em&gt;θ&lt;/em&gt;&lt;/span&gt; closest to &lt;span class=&quot;math inline&quot;&gt;&lt;em&gt;θ&lt;/em&gt;&lt;sub&gt;est&lt;/sub&gt;&lt;/span&gt; deviates by more than 5% from it.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;With this approach, accurate event cones can be constructed for nearly all gamma-ray events. However, other approaches for selecting the “best” cones may need to be considered in the future depending on the magnitude of measurement uncertainties.&lt;/p&gt;

&lt;h1 id=&quot;imaging&quot;&gt;Imaging&lt;/h1&gt;

&lt;p&gt;What I am calling “imaging” is the process of projecting a constructed event cone—with a vertex coordinate $O=(x_O,y_O,z_O)$, axis/direction unit (column) vector $\hat{D}=\langle u,v,w \rangle^\intercal$, and interior half-angle $\theta$—onto a two-dimensional plane to create an image. Mathematically, this cone is described as the set of all points $X = [x_X,y_X,z_X]^\intercal$ which satisfy [5]:&lt;/p&gt;

&lt;p&gt;\begin{equation}
(X-O)^\intercal M (X-O) = 0
\label{eq:cone}
\end{equation}&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;p&gt;\begin{equation}
M =
\hat{D}\hat{D}^\intercal
-
\cos^2(\theta)I_{3\times3}
\label{eq:cone_M}
\end{equation}&lt;/p&gt;

&lt;p&gt;For simplicity, let:&lt;/p&gt;

&lt;p&gt;
\begin{gather}
U = X - O =
\begin{bmatrix} x_X - x_O \\ y_X - y_O \\ z_X - z_O \end{bmatrix}
=
\begin{bmatrix} x_U \\ y_U \\ z_U \end{bmatrix}
\label{eq:simplify}
\end{gather}
&lt;/p&gt;

&lt;p&gt;And substitute \ref{eq:simplify} into \ref{eq:cone} and expand:&lt;/p&gt;

&lt;p&gt;
\begin{gather}
\begin{bmatrix} x_U &amp;amp; y_U &amp;amp; z_U \end{bmatrix}
\begin{bmatrix} M_{00} &amp;amp; M_{01} &amp;amp; M_{02} \\ M_{10} &amp;amp; M_{11} &amp;amp; M_{12} \\ M_{20} &amp;amp; M_{21} &amp;amp; M_{22} \end{bmatrix}
\begin{bmatrix} x_U \\ y_U \\ z_U \end{bmatrix}
= 0
\label{eq:cone_matrix}
\end{gather}
&lt;/p&gt;

&lt;p&gt;Multiplying the matrices gives us a second-order polynomial describing the cone:&lt;/p&gt;

&lt;p&gt;
\begin{align}
x_U^2M_{00} + y_U^2M_{11} + z_U^2M_{22} + x_Uy_U(M_{01}+M_{10}) \notag\\
{} + y_Uz_U(M_{12}+M_{21}) + x_Uz_U(M_{02}+M_{20}) &amp;amp;= 0
\label{eq:cone_poly}
\end{align}
&lt;/p&gt;

&lt;p&gt;The mathematics is simpler if forcing the imaging plane to be perpendicular to a coordinate axis. While the &lt;a href=&quot;https://github.com/Lindt8/ng-imager&quot;&gt;ng-imager&lt;/a&gt; implementation allows arbitrary plane orientations, the same underlying mathematics applies after expressing coordinates in the plane’s basis, which is assumed for the sake of simplifying this discussion. The image generation process is as follows:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Define an imaging plane/window with two points, $P_{BL}$ and $P_{TR}$, denoting the bottom-left and top-right corners of the imaging window, which have one matching coordinate variable whose axis the plane lies perpendicular to. This will set the corresponding element of $X$ to a constant.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Define the horizontal and vertical spatial pixel resolutions (e.g., 1 mm). Extract from $P_{BL}$ and $P_{TR}$ the horizontal and vertical bounds of the imaging window. Create 1D arrays for the bin centers and bin edges for both the columns and rows of the image, and create a 2D array $G$ of zeros corresponding to the image rows and columns (referred to as the image array).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;For each cone:&lt;/p&gt;

    &lt;ol&gt;
      &lt;li&gt;
        &lt;p&gt;Initialize two empty lists for storing horizontal and vertical coordinates, $L_H$ and $L_V$, respectively.&lt;/p&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;Loop through the horizontal bins and in each:&lt;/p&gt;

        &lt;ol&gt;
          &lt;li&gt;
            &lt;p&gt;Set the corresponding element of $X$ to the bin center.&lt;/p&gt;
          &lt;/li&gt;
          &lt;li&gt;
            &lt;p&gt;Calculate the two known elements of $U$ provided $X$.&lt;/p&gt;
          &lt;/li&gt;
          &lt;li&gt;
            &lt;p&gt;Solve Equation &lt;a href=&quot;#eq:cone_poly&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;eq:cone_poly&quot;&gt;(20)&lt;/a&gt; for the remaining unknown element of $U$.&lt;/p&gt;
          &lt;/li&gt;
          &lt;li&gt;
            &lt;p&gt;Substitute the real $\mathbb{R}$ root(s) found into Equation &lt;a href=&quot;#eq:simplify&quot; data-reference-type=&quot;ref&quot; data-reference=&quot;eq:simplify&quot;&gt;(18)&lt;/a&gt; to obtain the value(s) of last unknown element of $X$.&lt;/p&gt;
          &lt;/li&gt;
          &lt;li&gt;
            &lt;p&gt;Append the found value(s) for the $X$ elements corresponding to the image’s horizontal and vertical axes to their respective lists $L_H$ and $L_V$.&lt;/p&gt;
          &lt;/li&gt;
        &lt;/ol&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;Repeat the same loop as above but for the vertical bins.&lt;/p&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;Score/tally the coordinate pairs in $L_H$ and $L_V$ in a 2D histogram of the same dimensions as the 2D image array $G$ defined earlier and using the bin edges defined earlier. Set all nonzero entries in the histogram to 1.&lt;/p&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;Add the histogram to the 2D image array $G$.&lt;/p&gt;
      &lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Plot the 2D image array $G$ to show the obtained image.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are a few notes to be made about this approach. First, the image window is “scanned” in both the horizontal and vertical directions to ensure that each ellipse (technically, conic section, but usually an ellipse) in the imaging window is “continuous” from pixel to pixel. Additionally, the current method is only counting pixels as hit or not hit. Rather than this binary approach, a fancier approach could incorporate the length of the ellipse segment within each pixel and weight them accordingly but is not explored here.&lt;/p&gt;

&lt;p&gt;Using this method, Figure 10a is generated for a 1 mm$^2$ pixel resolution and 100 mm wide and tall image window centered on the source location (0,0,0), using the neutron event cones generated earlier, employing results from all nuclear reactions but filtering out rejected events (see Figures 5 vs. 6 earlier). The same image but for the gamma-ray event cones generated earlier (see Figure 9) is shown in Figure 10b.&lt;/p&gt;

&lt;div style=&quot;text-align:center;&quot;&gt;
  &lt;img src=&quot;/files/cones/cone-intersections-at-x0-mmnsource-at-000-novo-face-at-100000.png&quot; style=&quot;width:100%;&quot; /&gt;
  &lt;p&gt;&lt;em&gt;(a) 14.1 MeV neutron image&lt;/em&gt;&lt;/p&gt;
  &lt;img src=&quot;/files/cones/gamma_cone-intersections-at-x0-mmnsource-at-000-novo-face-at-100000.png&quot; style=&quot;width:100%;&quot; /&gt;
  &lt;p&gt;&lt;em&gt;(b) 2 MeV gamma-ray image&lt;/em&gt;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;Figure 10.&lt;/strong&gt; The images generated from neutron and gamma-ray events simulated earlier.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;As a bit of a fun simulation and to test a non-trivial source, next was simulated and imaged a ring-shaped source centered at (0,30,40) with inner radius 48 mm and outer radius 52 mm, isotropically emitting 14.1 MeV neutrons. Its image is shown in Figure 11 and verifies the imaging approach is working as intended.&lt;/p&gt;

&lt;div id=&quot;neutron_image_ring-source&quot; style=&quot;text-align:center;&quot;&gt;
&lt;img src=&quot;/files/cones/ring-source_cone-intersections-at-x0-mmnsource-at-03040-novo-face-at-100000.png&quot; style=&quot;width:95%; display:block; margin:auto;&quot; /&gt;
&lt;p&gt;&lt;strong&gt;Figure 11.&lt;/strong&gt; Image of a neutron ring-shaped source.&lt;/p&gt;
&lt;/div&gt;

&lt;h2 id=&quot;effect-of-using-realistic-interaction-coordinates&quot;&gt;Effect of using “realistic” interaction coordinates&lt;/h2&gt;

&lt;p&gt;In the images thus far, all interaction coordinates have used the exact MC true values. In the real experiment, we must assume that every interaction happens at the center of the bar in two dimensions and only has granularity along the depth of interaction (DOI) axis. Figure 12 shows the exact same image as earlier with Figure 10 but only using the exact MC coordinate for the DOI axis and using the location of the bar’s centerline for the other two dimensions, keeping everything else unchanged.&lt;/p&gt;

&lt;div style=&quot;text-align:center;&quot;&gt;
  &lt;img src=&quot;/files/cones/cone-intersections-at-x0-mmnsource-at-000-novo-face-at-100000_exp.png&quot; style=&quot;width:100%;&quot; /&gt;
  &lt;p&gt;&lt;em&gt;(a) 14.1 MeV neutron image&lt;/em&gt;&lt;/p&gt;
  &lt;img src=&quot;/files/cones/gamma_cone-intersections-at-x0-mmnsource-at-000-novo-face-at-100000_exp.png&quot; style=&quot;width:100%;&quot; /&gt;
  &lt;p&gt;&lt;em&gt;(b) 2 MeV gamma-ray image&lt;/em&gt;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;Figure 12.&lt;/strong&gt; Same as Figure 10 but using the bar centerline coordinates for each interaction position for two of the coordinate axis values and the MC true value along the DOI axis.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;To make matters worse, experimental position resolution / uncertainties in the DOI axis actually tend to be worse / greater than those in the bar’s two cross sectional directions. Fortunately though, with enough statistics these issues can, at least to some degree, be overcome.&lt;/p&gt;

&lt;h1 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h1&gt;

&lt;p&gt;A consistent methodology for constructing, selecting, and filtering event cones for both neutrons and gamma rays has been outlined here. Provided “pure” Monte Carlo data, the methods perform very well. The analytic, raster scanning-esque implementation of simple back projection employed here performed very well, producing consistently high-quality images.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;[1] T. Sato, Y. Iwamoto, S. Hashimoto, T. Ogawa, T. Furuta, S. Abe, T. Kai, P.-E. Tsai, N. Matsuda, H. Iwase, N. Shigyo, L. Sihver, and K. Niita, “Features of Particle and Heavy Ion Transport code System (PHITS) version 3.02,” &lt;em&gt;Journal of Nuclear Science and Technology&lt;/em&gt;, vol. 55, no. 6, pp. 684-690, 2018. &lt;a href=&quot;https://doi.org/10.1080/00223131.2017.1419890&quot;&gt;doi: 10.1080/00223131.2017.1419890&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[2] R. Fitzpatrick, &lt;em&gt;Newtonian Dynamics&lt;/em&gt;. The University of Texas at Austin (&lt;a href=&quot;https://farside.ph.utexas.edu/teaching/336k/Newton.pdf&quot;&gt;online&lt;/a&gt;), 2008.&lt;/p&gt;

&lt;p&gt;[3] J. A. Bernard, &lt;em&gt;22.05 Neutron Science and Reactor Physics, Fall 2006&lt;/em&gt; (&lt;a href=&quot;http://hdl.handle.net/1721.1/74136&quot;&gt;online&lt;/a&gt;), 2006.&lt;/p&gt;

&lt;p&gt;[4] I. Meric, E. Alagoz, L. B. Hysing, T. Kögler, D. Lathouwers, W. R. B. Lionheart, J. Mattingly, J. Obhodas, G. Pausch, H. E. S. Pettersen, H. N. Ratliff, M. Rovituso, S. M. Schellhammer, L. M. Setterdahl, K. Skjerdal, E. Sterpin, D. Sudac, J. A. Turko, and K. S. Ytre-Hauge, “A hybrid multi-particle approach to range assessment-based treatment verification in particle therapy,” &lt;em&gt;Scientific Reports&lt;/em&gt;, vol. 13, no. 1, p. 6709, 2023. &lt;a href=&quot;https://doi.org/10.1038/s41598-023-33777-w&quot;&gt;doi: 10.1038/s41598-023-33777-w&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[5] D. Armstrong, “Where is the cone?” arXiv:1708.07093, 2018. &lt;a href=&quot;https://arxiv.org/abs/1708.07093&quot;&gt;arXiv:1708.07093&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot; role=&quot;doc-endnote&quot;&gt;
      &lt;p&gt;For clarification, I mean inelastic scattering as reactions just leaving the target nucleus in an excited state and nonelastic reactions as inelastic scattering plus all other reactions resulting in everything else aside from just a neutron and the original target nucleus. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="neutron imaging" /><category term="gamma-ray imaging" /><category term="event cones" /><category term="simple back projection" /><category term="PHITS" /><summary type="html"></summary></entry><entry><title type="html">Choosing macOS after a lifetime on Windows</title><link href="https://lindt8.github.io/posts/choosing-macOS/" rel="alternate" type="text/html" title="Choosing macOS after a lifetime on Windows" /><published>2026-03-01T00:00:00-08:00</published><updated>2026-03-01T00:00:00-08:00</updated><id>https://lindt8.github.io/posts/blog-post-11</id><content type="html" xml:base="https://lindt8.github.io/posts/choosing-macOS/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;p&gt;A younger version of myself would have scoffed and even laughed at the thought of ever buying a Mac. Little did he know that over the years all of his arguments against Apple for his computing needs would either be flipped upside down or fade into irrelevancy—and that &lt;em&gt;Microsoft&lt;/em&gt; would be the one to provide him that final push to buy a MacBook.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;I grew up in a Windows household, introduced to computers from a young age. I played the original &lt;a href=&quot;https://en.wikipedia.org/wiki/Putt-Putt_Joins_the_Parade&quot;&gt;Putt-Putt game&lt;/a&gt; on a Compaq PC running Windows 95, spent countless hours in Microsoft Paint recoloring Pokemon sprites on our Windows XP machine, fell in love with PC gaming on a hand-me-down laptop running Windows Vista (later upgrading to a more capable Windows 7 laptop), and eventually built my own desktop PC with Windows 10. I became more and more a power-user over the years, especially once my grasp on coding and scripting was developed in my university years. I’d burned into muscle memory a bunch of keyboard shortcuts and even a moderate list of &lt;a href=&quot;https://en.wikipedia.org/wiki/Alt_code&quot;&gt;Alt codes&lt;/a&gt;—after all, I do love my em and en dashes (Alt + 0151/0150). I became comfortable in the CME.exe terminal, edited Windows registry keys to fix bugs/annoyances, used &lt;a href=&quot;https://en.wikipedia.org/wiki/Robocopy&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;robocopy&lt;/code&gt;&lt;/a&gt; for offline file syncing, and generally became acquainted with lots of power-user features, utilities, and programs through my professional work. I felt good about my mastery over Windows, after all, the professional world runs on Windows, right?&lt;/p&gt;

&lt;p&gt;Simultaneously, I had been nursing a relationship with the Apple ecosystem most of this time. For Christmas in eigth grade I received an &lt;a href=&quot;https://en.wikipedia.org/wiki/IPod_Classic&quot;&gt;iPod classic&lt;/a&gt;, and this set in motion how I would manage my music to this very day, curating my own collection of MP3 files and playlists, a holdout in today’s era of music streaming. When I went off to university, I got my first smartphone, an &lt;a href=&quot;https://en.wikipedia.org/wiki/IPhone_4&quot;&gt;iPhone 4&lt;/a&gt;, the natural choice given my music lived in iTunes. To some degree, I had initialy felt a bit like a hostage, that I &lt;em&gt;had&lt;/em&gt; to use an iPhone owing to insisting on having my music in iTunes and not wanting to mess with other music management solutions. That feeling, fortunately, mostly faded before too long owing to actually liking the mobile experience on iOS, though I did harbor some envy for the extra customization, control, and freedoms of Android.&lt;/p&gt;

&lt;p&gt;Even then though, I would &lt;em&gt;never&lt;/em&gt; consider buying a Mac. It was simply unfathomable. Why would I pay a lot more money for a machine with worse performance, second-class to absent support for many programs and games especially, and on a relatively niche operating system with hardly any footprint in the professional world of engineering? Furthermore, I saw value in being able to customize everything, in both hardware and software. Maybe this made me a hypocrite for using iOS over Android, but at least on mobile there were plenty more arguments, in my view, for choosing Apple (device build quality, OS polish and consistency, software support longevity, etc.). In the personal computing realm, the only choices were Microsoft Windows, Apple macOS, and Linux, and Windows was &lt;em&gt;obviously&lt;/em&gt; the right choice. And this is how I continued thinking throughout my twenties.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;In the summer before my final year of undergrad, I had an internship where I was provided an &lt;a href=&quot;https://en.wikipedia.org/wiki/IMac&quot;&gt;iMac&lt;/a&gt; to use, and I remember just being so frustrated with it. I resented Apple for so many Mac-isms and for, what seemed to me, unintuitive differences in design whose only purpose was to be different. In all fairness, I still hold many of those opinions (looking at you, reversed “natural” scrolling), and being cursed with a &lt;a href=&quot;https://en.wikipedia.org/wiki/Magic_Mouse&quot;&gt;Magic Mouse&lt;/a&gt;—a design and erginomic nightmare so bad as to be comical—probably contibuted a fair amount toward my tainted view of Macs and Apple’s design philosophy. At the time, I failed to critically reflect on &lt;em&gt;why&lt;/em&gt; I had been given a Mac in the first place, for a job involving writing, compiling, and executing Fortran and C++ code.&lt;/p&gt;

&lt;p&gt;I did get a taste of Linux in my university years too, though mostly through &lt;a href=&quot;https://en.wikipedia.org/wiki/PuTTY&quot;&gt;PuTTY&lt;/a&gt; SSH windows, primarily for utilizing the department’s high-performance computing resources for extensive radiation transport simulations. I started understanding Linux’s place in the professional world and its utility. In my first summer of grad school I had the joy of trying to compile Fortran code on a Windows machine, realizing just how much more of a pain it was than in my undergraduate Fortran course where I just SSH’d into one of the department’s Linux machines. For the particle experiments in my PhD work, we used CERN’s &lt;a href=&quot;https://en.wikipedia.org/wiki/ROOT&quot;&gt;ROOT&lt;/a&gt; software for data analysis, where I was stuck using an old version owing to deprecation of Windows support. These were my first professional tastes of Windows being the second-class citizen and challenging my preconceptions of the professional ubiquity of Windows.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;From grad school onward, I was content with my Windows + iOS setup. Neither were perfect, but I felt like I had made the right choice for my personal needs and priorities. My desktop PC served me well, satisfying my gaming needs and providing a smooth and functional experience. Time continued to pass. As a young adult, I became more aware of the news and formed opinions about how the world should be. I aligned with &lt;a href=&quot;https://en.wikipedia.org/wiki/Open_source&quot;&gt;open source&lt;/a&gt; ideals and &lt;a href=&quot;https://en.wikipedia.org/wiki/Right_to_repair&quot;&gt;right-to-repair&lt;/a&gt; initiatives (a place where Apple holds a notorious reputation), and more broadly I developed values of personal privacy and data security. Amid incremental steps toward a more Orwellian future, Apple has &lt;a href=&quot;https://en.wikipedia.org/wiki/Apple%E2%80%93FBI_encryption_dispute&quot;&gt;repeatedly&lt;/a&gt; &lt;a href=&quot;https://arstechnica.com/tech-policy/2025/02/uk-demands-apple-break-encryption-to-allow-govt-spying-worldwide-reports-say/&quot;&gt;demonstrated&lt;/a&gt; its resolve on maintaining its principled stance of &lt;a href=&quot;https://www.apple.com/privacy/&quot;&gt;protecting user privacy&lt;/a&gt; even against the strongest adversaries possible (large Western governments), and this has left a strong impression on me.&lt;/p&gt;

&lt;p&gt;Even still, I was content with my Windows 10 PC I’d built, and I continued to game on it, even lugging it with me to Japan when I moved there (and later Norway too). However, in those years in Japan, working my first “real job” after my PhD, I started doing a bit more introspection about how I was spending my free time, and specifically on the value of my continuing years-long relationship with &lt;a href=&quot;https://en.wikipedia.org/wiki/World_of_Warcraft&quot;&gt;World of Warcraft&lt;/a&gt;, especially as the game itself had started feeling more like a chore than a fun way to spend time. More broadly, I had begun questioning the value of time spent gaming versus productive or other consumptive hobbies, like reading and watching shows/movies. Regardless, owing to the pandemic keeping me at home for most of my time not at work, gaming remained a part of my life, though in a waning capacity and with a shift back toward console gaming (hello &lt;a href=&quot;https://arstechnica.com/gaming/2020/03/animal-crossing-new-horizons-review-a-quarantined-life-has-never-been-cuter/&quot;&gt;Animal Crossing&lt;/a&gt;). It was also in these years that the &lt;a href=&quot;https://www.relay.fm/cortex/&quot;&gt;Cortex&lt;/a&gt; podcast, which I’d been a loyal listener of, began chronicling the rise of Apple Silicon computer processors, sparking within me the tiniest bit of intrigue for Mac computers.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;By the time life had taken me to Norway and I had fully settled in, my priorities and how I spent my free time were quite different from years past. I no longer found myself gaming at all, aside from occasionally booting up my old Minecraft world for some nostalgia and cathartic building or casually playing various single-player or local-multi-player games on my Nintendo Switch. Instead I was spending my free time reading, watching shows and YouTube edutainment, hiking, and socializing. Thus, my aging gaming PC was largely relegated to web browsing and basic computer tasks.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;As time marched on, I started noticing a decline in the quality of my experience on Windows. &lt;em&gt;“Did that pop-up always appear after each update? Wait, where on earth did that Word document just save? What do you mean on OneDrive? You mean I &lt;strong&gt;can’t&lt;/strong&gt; make the default behavior to just save the file locally, that I’m forever plagued with extra clicks to get the more sensible behavior? Why do I need to sign-in to a Microsoft account for this? No, really, I am not interested in the Edge browser. Nor Bing search. Please, I meant &lt;strong&gt;no&lt;/strong&gt; the first three dozen times I told you, and no my mind has not and will not change.”&lt;/em&gt; The modern decline of software user experiences to maximally squeeze profits from the user base is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Enshittification&quot;&gt;well-documented phenomenon&lt;/a&gt;, and Microsoft most certainly has not been immune to it.&lt;/p&gt;

&lt;p&gt;And this was all &lt;strong&gt;before&lt;/strong&gt; Copilot and Microsoft’s decision to force-feed its users with its solution-looking-for-a-problem AI agent, intruding &lt;em&gt;everywhere&lt;/em&gt; in Windows and Microsoft’s other software products.  I was flabbergasted when I first heard of Microsoft’s &lt;a href=&quot;https://arstechnica.com/gadgets/2024/05/microsofts-new-recall-feature-will-record-everything-you-do-on-your-pc/&quot;&gt;new Windows 11 &lt;s&gt;spyware&lt;/s&gt; “feature” called Recall&lt;/a&gt; that &lt;a href=&quot;https://en.wikipedia.org/wiki/Windows_Recall&quot;&gt;constantly records your screen for an AI to study&lt;/a&gt;, a blatant and reckless disregard for privacy and security. Even after immense backlash, &lt;a href=&quot;https://arstechnica.com/gadgets/2025/04/in-depth-with-windows-11-recall-and-what-microsoft-has-and-hasnt-fixed/&quot;&gt;they have still deployed it&lt;/a&gt;—though at least it’s only opt-in by default, &lt;em&gt;for now&lt;/em&gt;. The fact that this idea was even humored seriously, let alone developed then deployed, was the writing on the wall for me: my ideals are fundamentally incompatible with where Microsoft wants to take Windows. Furthermore, with Microsoft &lt;a href=&quot;https://www.pcworld.com/article/2768784/microsoft-ceo-claims-30-of-new-code-is-written-by-ai.html&quot;&gt;already claiming&lt;/a&gt; up to 30% of their code is being written by AI (and hearing through the grapevine that the target is an even higher percentage to be written by Copilot), I personally lost all faith in the long-term future of Windows, at least for me. This sounds like a recipe for the operating system, which already feels like a patchwork mess (with some functions &lt;em&gt;still&lt;/em&gt; only accessible via a &lt;a href=&quot;https://arstechnica.com/gadgets/2024/08/microsoft-formally-deprecates-the-39-year-old-windows-control-panel/&quot;&gt;decades-old control panel&lt;/a&gt;), to just completely implode.&lt;/p&gt;

&lt;p&gt;Still, inertia is a very strong force. I may not like Windows 11 on my work laptop, but at least I still have good ol’ Windows 10 on my home PC that I can just keep using, right? Nope! I can’t fault Microsoft for phasing out old operating systems; expecting them to support Windows 10 forever would be unreasonable. But, I can and absolutely will fault them for &lt;a href=&quot;https://arstechnica.com/gadgets/2024/08/microsoft-formally-deprecates-the-39-year-old-windows-control-panel/&quot;&gt;deprecating tons of perfectly serviceable hardware, including my desktop, by ceasing security updates for Windows 10 and requiring newer hardware for Windows 11&lt;/a&gt;, seeking to generate tons of e-waste in an effort just to get people on older hardware to shell out for the shiny new “&lt;a href=&quot;https://blogs.microsoft.com/blog/2024/05/20/introducing-copilot-pcs/&quot;&gt;Copilot+ PCs&lt;/a&gt;” on offer, equipped with the latest and greatest user-data-harvesting technology. So, an expiry date of 14 October 2025 had been slapped on my trusty (though aging) home-built PC.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Thus, Microsoft forced my hand. If I wanted to keep using my old desktop PC securely, my only option would be to switch to using Linux on it. While Steam’s recent hardware survey &lt;a href=&quot;https://www.tomshardware.com/tech-industry/linux-usage-hits-an-all-time-high-in-steam-hardware-survey-and-amd-processors-continue-their-march-against-intel&quot;&gt;shows a good chunk of gamers have made exactly this choice&lt;/a&gt;, I ultimately decided that this was not tenable for me, at least not for my primary machine. While Linux does best align with some of my ideals around software, practically speaking I do not have the time to undertake all of the learning, tinkering, debugging, maintaining, and fixing that would come with a move to Linux, not to mention the fact that there are still widespread software compatibility issues I would have to overcome on Linux. This could still be a fun hobby project for the old desktop one day though. But, the more pressing issue was deciding what kind of new primary computer to buy to replace my nearly-ten-year-old desktop for daily use.&lt;/p&gt;

&lt;p&gt;This is the context that ultimately made me question: &lt;em&gt;“Do I even want a Windows 11 machine?”&lt;/em&gt; Up until this point, it was a given that I would &lt;em&gt;always&lt;/em&gt; use Windows as my primary operating system. But, owing to the ill-will Microsoft had garnered from me in forcing my hand on upgrade timing, together with its consistent display of extreme misalignment with my own values, I was starting to ponder: &lt;em&gt;“Is macOS just my only choice here?”&lt;/em&gt; At first, I was really not happy with the pickle I found myself in, trying to just pick the least bad option in front of me. After all, what about all of the reasons I had dismissed Macs previously?&lt;/p&gt;

&lt;p&gt;Well, it turns out most of those were moot or wrong now. Computer gaming is the least important it’s ever been to me in my adult life, so there goes that argument for Windows (even with Mac/Linux gaming continuing to make remarkable progress in recent years). While it’s true Apple still charges a premium for its products, I’m also a working adult now and not a poor university student.  Furthermore, what that money gets me today is genuinely best-in-class build quality (something Apple has long done well) on top of genuinely excellent performance &lt;em&gt;and&lt;/em&gt; efficiency thanks to the progress with &lt;a href=&quot;https://www.macrumors.com/guide/apple-silicon/&quot;&gt;Apple Silicon&lt;/a&gt;. As far as program support is concerned, macOS actually comes out ahead of Windows on this front owing to a number of the software I use professionally actually being developed for Unix systems (macOS and Linux) and Windows either being an afterthought or wholly unsupported, with most other major software today being distributed across both Windows and macOS.&lt;/p&gt;

&lt;p&gt;On top of this, I started researching the benefits of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Apple_ecosystem&quot;&gt;Apple ecosystem&lt;/a&gt;—the synergies I’d benefit from by using both macOS and iOS together—and suddenly started feeling a bit of excitement surrounding the prospect of moving to macOS. As it turns out, there’s a bunch of &lt;a href=&quot;https://www.apple.com/macos/continuity/&quot;&gt;cool functionality and conveniences&lt;/a&gt; to be gained. It also felt like a nice opportunity for a clean start of sorts, a chance to rethink my file management system from the ground up along with other facets of my digital life in terms of the tools I use and how I organize things. While a part of me is reluctant to be willingly handing over some degree of my &lt;a href=&quot;https://www.weforum.org/stories/2025/01/europe-digital-sovereignty/&quot;&gt;digital sovereignty&lt;/a&gt; to Apple by investing in their ecosystem, I would argue that it is nigh impossible for most of us to fully escape the clutches of the big tech companies today, so I might as well choose the company whose values best align with my own and that maximizes convenience. (No big tech company is perfect; see Wikipedia’s nicely compiled list of criticisms of both &lt;a href=&quot;https://en.wikipedia.org/wiki/Criticism_of_Apple_Inc.&quot;&gt;Apple&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/Criticism_of_Microsoft&quot;&gt;Microsoft&lt;/a&gt;, along with the others with pages in the “&lt;a href=&quot;https://en.wikipedia.org/wiki/Category:Criticisms_of_companies&quot;&gt;Criticism of companies&lt;/a&gt;” category.)&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;So, I pulled the trigger and bought an &lt;a href=&quot;https://en.wikipedia.org/wiki/MacBook_Pro_(Apple_silicon)&quot;&gt;M4 MacBook Pro&lt;/a&gt;, and I am happy to report I have absolutely no regrets two and a half months in! I have genuinely had a great time on macOS, despite the current version 26 Tahoe being &lt;a href=&quot;https://daringfireball.net/2026/01/resize_columns_to_fit_filenames&quot;&gt;controversial&lt;/a&gt; among the established Mac community. It has not been without its growing pains. There have been plenty of moments where I have to search for how to do something or have been a bit frustrated at something I’m used to doing quickly in Windows being a bit more arduous in macOS. Often though, I will find there is a sensible “Mac-native” approach to accomplishing my goal that is simply just different from how Windows had trained me to think, and this time I am embracing these differences with an open mind instead of faulting macOS for not being Windows. I will also add that the &lt;a href=&quot;https://sindresorhus.com/supercharge&quot;&gt;Supercharge&lt;/a&gt; app has been worth every cent I paid for it and addresses many of my remaining grievances with macOS so far.&lt;/p&gt;

&lt;p&gt;Furthermore, (re)learning all of the new keyboard shortcuts has been an ongoing challenge, though with time I am internalizing patterns for how things work and realizing there is some degree of logic to when the various modifier keys are used. On that note, I am very happy that my beloved em and en dashes are typed far more sensibly on macOS (Shift + Option + hyphen / Option + hyphen) than the Alt codes on Windows, and it gives me a little bit of pleasure each time I discover yet another symbol on macOS that is sensibly typed rather than requiring memorization of a four-digit code. Developing and enacting my revisions to my file management and overall digital strategies has been a substantial amount of work, though I am very happy with how organized my previous mess of files now is. (This is more the fruits of my hard labor, not really a macOS benefit though.)&lt;/p&gt;

&lt;p&gt;I have also enjoyed the passionate online media and community around Apple and macOS, with podcasts helping me fulfill my &lt;a href=&quot;https://www.relay.fm/mpu&quot;&gt;power-user nature on macOS&lt;/a&gt; and casually following &lt;a href=&quot;https://www.relay.fm/upgrade/604&quot;&gt;news&lt;/a&gt; and &lt;a href=&quot;https://www.relay.fm/connected/592&quot;&gt;rumors&lt;/a&gt; around Apple, &lt;a href=&quot;https://www.macrumors.com/&quot;&gt;news sites&lt;/a&gt; where I can keep up-to-date with new features being added to macOS and iOS and future releases, and &lt;a href=&quot;https://daringfireball.net/&quot;&gt;blogs&lt;/a&gt; that have chronicled Apple for many years. I feel like I have learned quite quickly and have already achieved some macOS power-user-level feats in making &lt;a href=&quot;https://github.com/Lindt8/phits-router-macos&quot;&gt;tools&lt;/a&gt; for my personal workflows and &lt;a href=&quot;https://hratliff.com/posts/time-machine-hdd-spinup-minimization/&quot;&gt;resolving minor annoyances&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;I must admit, I have felt a degree of schadenfreude in seeing the news covering the mess that has been &lt;a href=&quot;https://www.theverge.com/news/867647/microsoft-windows-11-january-2026-update-bugs-issues&quot;&gt;buggy Windows 11 updates in early 2026&lt;/a&gt; wreaking havoc, like I’m witnessing the results of AI slop code being pushed out to the masses recklessly without adequate testing. I understand that Apple is widely regarded as having fallen way behind on AI, but this is a feature, not a bug, for me. I don’t know why Apple has failed to achieve their initial AI goals, but I like to think it isn’t because they lacked the talent but at least partially because they are holding themselves to a higher standard of quality and reliability than the rest of the tech industry clambering to “move fast and break things” in the pursuit of AI rollout. Perhaps they underestimated the difficulty of developing an AI that can actually be trusted to have permissions on your phone or computer, but I am grateful Apple has chosen to withhold AI features and take a bit of egg to the face instead of following this trend of shoving unhelpful (and sometimes destructive) AI everywhere. I just hope this remains to be the case moving forward.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;I do not see myself becoming one of the Apple fan-boys my past self would have scoffed at—I have no intention of blindly defending any poor decision-making at Apple—but man am I thrilled about actually enjoying my personal computing experience once again.&lt;/p&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="macOS" /><category term="Mac" /><category term="iOS" /><category term="Windows" /><category term="switch" /><summary type="html"></summary></entry><entry><title type="html">Choosing city transit tickets in Bergen, Norway</title><link href="https://lindt8.github.io/posts/Bergen-transit-tickets/" rel="alternate" type="text/html" title="Choosing city transit tickets in Bergen, Norway" /><published>2025-01-12T00:00:00-08:00</published><updated>2025-01-12T00:00:00-08:00</updated><id>https://lindt8.github.io/posts/blog-post-10</id><content type="html" xml:base="https://lindt8.github.io/posts/Bergen-transit-tickets/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;p&gt;Bergen, Norway is an absolutely lovely city I have &lt;a href=&quot;https://northboundarchives.com/five-reasons-to-fall-in-love-with-bergen-norway/&quot;&gt;fallen in love with&lt;/a&gt; and am lucky to call home, and getting around it is made quite easy with its excellent public transportation network of busses, light rail, and boats, all operated by &lt;a href=&quot;https://www.skyss.no/en/&quot;&gt;Skyss&lt;/a&gt;.  A valid Skyss ticket is required for and will work on all of these modes of transport (no need for different tickets for each).&lt;/p&gt;

&lt;p&gt;Full details on the boarding process are outlined &lt;a href=&quot;https://www.skyss.no/en/travel/useful-travel-information/&quot;&gt;here&lt;/a&gt;, but, in short, within the Bergen area you do not need to present/validate/scan your ticket to board transit but should be prepared to present your valid ticket to an inspector in the event of a &lt;a href=&quot;https://www.skyss.no/en/help-and-contact/ticket-inspections/&quot;&gt;ticket inspection&lt;/a&gt;, where you will face a fine if without a valid ticket.  (Outside of the Bergen area, you must present your ticket when boarding.)  Skyss tickets can be purchased in &lt;a href=&quot;https://www.skyss.no/en/tickets-and-prices/buying-tickets/buses-and-some-boat-connections/&quot;&gt;a number of ways&lt;/a&gt;, though using the &lt;a href=&quot;https://www.skyss.no/en/tickets-and-prices/buying-tickets/skyss-ticket-app/&quot;&gt;Skyss Billet app&lt;/a&gt; is the most common and convenient; note that tickets are activated at the moment of purchase, starting the timer on their validity period.  Skyss also provides a separate travel/route planning app &lt;a href=&quot;https://www.skyss.no/en/travel/Skyss-travel-app/&quot;&gt;Skyss Reise&lt;/a&gt;, which displays information about any expected delays or route changes owing to tunnel closures, extreme weather, traffic, or anything else.&lt;/p&gt;

&lt;p&gt;Skyss operates throughout Vestland, the Norwegian county containing Bergen, and has divided it into a number of &lt;a href=&quot;https://www.skyss.no/en/tickets-and-prices/prize-zones/&quot;&gt;price zones&lt;/a&gt;, where Zone A contains all of Bergen and a large area around it—including the airport and its major nearby islands Askøy and Sotra (among others). Skyss sells tickets encompassing 1, 2, 3, or 4+ zones; however, most people visiting Bergen and using public transit are likely to remain within just Zone A.  Thus, this article only covers single-zone ticket prices.  Furthermore, the contents of this article reflect the Skyss policies and prices as of the time of writing (12 January 2025), which may have changed in time.&lt;/p&gt;

&lt;p&gt;Skyss sells the following types of tickets (&lt;a href=&quot;https://www.skyss.no/en/tickets-and-prices/prices/bus-light-rail-and--some-boat-connections/&quot;&gt;current ticket prices&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Single ticket (90-minute validity): †47 kr&lt;/li&gt;
  &lt;li&gt;24-hour ticket: 115 kr&lt;/li&gt;
  &lt;li&gt;7-day season ticket: 255 kr&lt;/li&gt;
  &lt;li&gt;30-day season ticket: 795 kr&lt;/li&gt;
  &lt;li&gt;180-day season ticket: 3975 kr&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that the season tickets last exactly their stated duration (e.g., the 7-day ticket is valid for 168 hours from its moment of purchase).  Here are some Google search links for converting various currencies to Norwegian kroner (kr / NOK): &lt;a href=&quot;https://www.google.com/search?hl=en&amp;amp;q=1+usd+in+nok&quot;&gt;USD&lt;/a&gt;, &lt;a href=&quot;https://www.google.com/search?hl=en&amp;amp;q=1+gbp+in+nok&quot;&gt;GBP&lt;/a&gt;, &lt;a href=&quot;https://www.google.com/search?hl=en&amp;amp;q=1+eur+in+nok&quot;&gt;EUR&lt;/a&gt;, &lt;a href=&quot;https://www.google.com/search?hl=en&amp;amp;q=1+cad+in+nok&quot;&gt;CAD&lt;/a&gt;, &lt;a href=&quot;https://www.google.com/search?hl=en&amp;amp;q=1+aud+in+nok&quot;&gt;AUD&lt;/a&gt;, &lt;a href=&quot;https://www.google.com/search?hl=en&amp;amp;q=1+nzd+in+nok&quot;&gt;NZD&lt;/a&gt;, &lt;a href=&quot;https://www.google.com/search?hl=en&amp;amp;q=100+jpy+in+nok&quot;&gt;JPY&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;†The single ticket pricing is complicated by the &lt;a href=&quot;https://www.skyss.no/en/tickets-and-prices/tickets/travel-discount/&quot;&gt;travel discount system&lt;/a&gt;, which decreases the cost of each additional single ticket purchased within the last 30 days, to a maximum discount of 40%.&lt;/p&gt;

&lt;p&gt;While circumstances usually make it easy to predict whether a 24-hour ticket is more sensible than single tickets, over longer periods it can be less obvious whether it makes more sense to buy single tickets, one or more 7-day season tickets (or “passes”, for brevity), or a 30-day pass.  Thus, I have done the math here.&lt;/p&gt;

&lt;p&gt;The below plot shows horizontal lines indicating the prices of the 30-day pass and one to four 7-day passes, and its curves show the cumulative cost of purchasing some number of single tickets “N”.  The darker black curve assumes all single ticket purchases are made within a 30-day window and that the first single ticket is bought at full price (which should be the case for most visiting tourists); meanwhile, the lighter gray curve assumes all single ticket purchases are made with the maximum 40% discount (representing the theoretical minimum cost of that number of single tickets).&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/files/Skyss ticket costs plot vertical web.png&quot; style=&quot;width:80%;&quot; /&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;From this, we see that the pricing breakpoints making passes cheaper than single tickets—for most visitors without any pre-existing travel discount accumulated—as a function of the number of “rides” (uses of public transit more than 90 minutes apart, thus requiring separate single ticket purchases) are as follows:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;For 6 or more rides, the 7-day pass (255 kr) is cheaper.&lt;/li&gt;
  &lt;li&gt;For 13 or more rides, two 7-day passes (510 kr) are cheaper.&lt;/li&gt;
  &lt;li&gt;For 21 or more rides, a 30-day pass (795 kr) is cheaper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For visitors intending to make frequent use of the public transportation, buying passes (season tickets) covering the duration of their stay is very likely to be the most economical (and convenient) option.&lt;/p&gt;

&lt;p&gt;For locals and frequent visitors to Bergen buying the single tickets regularly who do have some travel discount already accumulated, these breakpoints will be a bit different.  However, this plot is hopefully still a helpful tool in determining under what circumstances each combination of tickets/passes makes the most sense.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;While this article does not seek to be a comprehensive guide to transportation in Bergen, the dedicated airport express bus service &lt;a href=&quot;https://flybussen.no/en&quot;&gt;Flybussen&lt;/a&gt; should be mentioned.  It is not operated by Skyss (though appears in the Skyss Reise app travel route suggestions) and requires purchase of its own tickets though Flybussen, either in advance &lt;a href=&quot;https://flybussen.no/en&quot;&gt;online&lt;/a&gt; or at the bus (slightly more expensive). If needing to get to the Bergen airport very early in the morning (e.g., for a 6 am flight) or if arriving at the airport late at night (particularly on weekdays) and needing to get into the city, Flybussen may be your only option aside from taxis/Uber, which are considerably more expensive.  More information on the travel times and stops serviced by Flybussen can be found &lt;a href=&quot;https://flybussen.no/en/airports/bergen-airport-flybussen-bergen/where-does-flybussen-bergen-stop/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;!--
comment block
--&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="Skyss" /><category term="Bergen" /><category term="transit" /><category term="tickets" /><summary type="html"></summary></entry><entry><title type="html">What happened to the red Froot Loop?</title><link href="https://lindt8.github.io/posts/red-Froot-Loops-fate/" rel="alternate" type="text/html" title="What happened to the red Froot Loop?" /><published>2024-07-24T00:00:00-07:00</published><updated>2024-07-24T00:00:00-07:00</updated><id>https://lindt8.github.io/posts/blog-post-9</id><content type="html" xml:base="https://lindt8.github.io/posts/red-Froot-Loops-fate/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;p&gt;I recently went to the grocery store and picked up a couple of boxes of Kellogg’s Froot Loops cereal.  Upon getting home I realized the two boxes were not the same: Only one contained red loops!&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/files/Froot-Loops_1-cover.jpg&quot; style=&quot;width:80%;&quot; /&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;The two boxes are pictured above; the left one with red loops had a manufacture date of 4 March 2024 while the right box without the red loops was produced on 9 May 2024.  After inspecting their ingredients lists, the only difference was that the older box contained radish within its fruit and vegetable concentrates while the newer one did not.&lt;/p&gt;

&lt;p&gt;I was a bit curious as to why Froot Loops had become 33% less colorful, especially given that I grew up in the US where the Froot Loops have six vibrant colors using artifical food colorings, contrasted against the European version’s (until now) three natural colors—red, yellow, and purple.  Unable to find any information online about this change (and noticing that even on the official Kellogg’s European websites that the images of the Froot Loops boxes had not yet reflected this change), I reached out to the Kellogg’s UK contact page to lightheartedly ask about the reason behind this change and whether it was a permanent one.  Their excellent consumer affairs department got back to me promptly with the following explanation:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Thank you for getting in touch with us regarding the new Kellogg’s Froot Loops recipe.&lt;/p&gt;

  &lt;p&gt;From time to time we evolve our recipes so that we can continue to meet our consumers’ needs and expectations. We identified that the red vegetable colouring we use for the red loop in Kellogg’s Froot Loops has increased significantly in price as a raw material so in order to minimise any pricing impact for our loyal Froot Loops fans, we made the decision to remove this colour. Froot Loops will now be a feast of our yellow and purple coloured loops and most importantly, keep the great taste our consumers know and love.&lt;/p&gt;

  &lt;p&gt;Furthermore, to answer your question about whether this is a permanent change: yes, this is indeed a permanent change for Froot Loops from Kellogg’s.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;!--
I'll make sure your comments are shared with the relevant team.   

Thanks again for your comments, I appreciate that you took the time to let us know your thoughts. If you have any further questions, do not hesitate to contact us.

Best wishes

Francesca
Kellogg Consumer Affairs
--&gt;

&lt;p&gt;So, there we have it; the mystery is solved. The red loops are permanently gone owing to a raw materials price increase.  While this is slightly dissapointing news, I am happy to report the new two-toned Froot Loops are indeed just as delicious as their tri-toned predecessor. :)&lt;/p&gt;

&lt;!--
Well, if you found this, here's the password to unprotect the abvove sheet:
W9dQms3%_j5@i1uykJvEM1Kr]?uP;:gF%Y;1vRr!JmCpW)9hzY
--&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="Froot Loops" /><category term="cereal" /><category term="red loop" /><summary type="html"></summary></entry><entry><title type="html">Schengen schedule calculator</title><link href="https://lindt8.github.io/posts/schengen-schedule-calculator/" rel="alternate" type="text/html" title="Schengen schedule calculator" /><published>2024-02-07T00:00:00-08:00</published><updated>2024-02-07T00:00:00-08:00</updated><id>https://lindt8.github.io/posts/blog-post-8</id><content type="html" xml:base="https://lindt8.github.io/posts/schengen-schedule-calculator/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;center&gt;&lt;a title=&quot;Rob984, CC BY-SA 4.0 &amp;lt;https://creativecommons.org/licenses/by-sa/4.0&amp;gt;, via Wikimedia Commons&quot; href=&quot;https://commons.wikimedia.org/wiki/File:Map_of_the_Schengen_Area.svg&quot;&gt;&lt;img width=&quot;80%&quot; alt=&quot;Map of the Schengen Area&quot; src=&quot;https://upload.wikimedia.org/wikipedia/commons/5/57/Map_of_the_Schengen_Area.svg&quot; /&gt;&lt;/a&gt;&lt;/center&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;If you have traveled to (nearly) any European country, you have probably entered the &lt;a href=&quot;https://en.wikipedia.org/wiki/Schengen_Area&quot;&gt;Schengen Area&lt;/a&gt; (pictured above), a group of countries that have abolished all border controls at their shared borders and, as far as freedom of movement is concerned, work kind of like one large country.  However, unless you’re a citizen of a Schengen country or in a Schengen country in which you hold a residence permit, your stay in Schengen is limited in a somewhat unique way: You can stay in the Schengen Area for &lt;strong&gt;a maximum of 90 days in any 180-day period&lt;/strong&gt;. More comprehensive information can be found &lt;a href=&quot;https://home-affairs.ec.europa.eu/policies/schengen-borders-and-visa/visa-policy_en&quot;&gt;here&lt;/a&gt; regarding the specific visa agreements Schengen holds with all non-Schengen countries and whether you can enter Schengen visa-free.&lt;/p&gt;

&lt;p&gt;In short, this 180-day period is a rolling window, looking 180 days backwards from any given date and counting the days spent inside the Schengen Area in that window.  The days in which you enter and exit Schengen (travel days) are also counted as days spent in Schengen.  If you are a frequent Schengen visitor (backpacker, digital nomad, lover to a Schengen resident, travel addicted retiree, international man of mystery, etc.), this can pose a rather complicated scheduling problem to ensure you don’t overstay your legal welcome.  Our calendar of uneven months does not make this any easier, and complications abound the moment you’re trying to schedule numerous short trips instead of monolithic nearly 90-day ones.&lt;/p&gt;

&lt;p&gt;There are already &lt;a href=&quot;https://www.schengenvisainfo.com/visa-calculator/&quot;&gt;nice Schengen calculators&lt;/a&gt;  online, but I felt they all suffered some shortcomings that made them difficult and/or clunky to use for actual planning (but perfectly fine for checking the legality of an existing plan with fixed dates).  I wanted a tool that could help me &lt;em&gt;brainstorm potential plans&lt;/em&gt;, giving me feedback on the implications of each stay on any future, yet unplanned, trips into Schengen.&lt;/p&gt;

&lt;p&gt;While this feels like the perfect task for a program, I wanted to make something more accessible.  After all, a majority of the people wanting to perform these calculations probably don’t have Python installed on their computers nor are comfortable with running scripts in a terminal.  And these are not very user-friendly ways of inputting data anyways.  You know what is widely familiar, visual, interactive, and can store data? &lt;em&gt;Spreadsheets!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So that’s exactly what I’ve made.  Below I have linked to my Schengen Schedule Calculator spreadsheet and included a screenshot (hover to enlarge) of its input and results area, with disucssion of its usage following.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://hratliff.com/files/Schengen_schedule_calculator.xlsx&quot;&gt;&lt;font color=&quot;#709E4A&quot;&gt;[Download link for Schengen_schedule_calculator.xlsx]&lt;/font&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;sup&gt;&lt;em&gt;As an obligitory disclaimer, you are solely responsible for ensuring you obey all laws pertaining to visiting and staying in the Schengen Area. While a lot of attention and troubleshooting went into this spreadsheet, its results are not guaranteed. This site’s owner disclaims all responsibly for anything that may or may not happen to you. Before going through with any travel plan, make sure to double-check the travel dates!&lt;/em&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;style&gt;
.image-container img {
    transition: transform 0.3s ease-in-out;
}
.image-container img:hover {
    transform: scale(2.2103896);
}
&lt;/style&gt;

&lt;div class=&quot;image-container&quot; style=&quot;text-align: center;&quot;&gt;
  &lt;img src=&quot;/files/Schengen-calculator-schreenshot.png&quot; style=&quot;width:100%;&quot; /&gt;
&lt;/div&gt;

&lt;!--
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/files/Schengen-calculator-schreenshot.png&quot; style=&quot;width:100%;&quot;&gt;&lt;/div&gt;
--&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: The spreadsheet utilizes the &lt;a href=&quot;https://support.microsoft.com/en-au/office/xlookup-function-b7fd680e-6d10-43e6-84f9-88eae8bf5929&quot;&gt;XLOOKUP() function&lt;/a&gt;, which is only available in Excel 2021 and newer. So, opening the spreadsheet in older versions of Excel will result in&lt;/em&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#NAME?&lt;/code&gt; &lt;em&gt;errors.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The spreadsheet is quite simple to operate.  The cells in which information should be inputted are filled in a light blue color and have a bold black border around them.  Principally, these are just the dates of tentative stays in Schengen.  On the right side of the results area is a section specifying when the next possible entry date is for a stay of some specific desired length, and the first of these columns has a value that can be changed to whatever length of stay you want.&lt;/p&gt;

&lt;p&gt;The results columns to the right of the input dates are self explanitory, specifying how long the stay is (including the travel dates), how many “spare Schengen days” one has as of the start and end dates, a “Yes/No” on whether the planned stay is illegal, a “Yes/No” on whether you’re “locked out” of Schengen after the stay (meaning you must wait some minimum number of days before being able to re-enter), and the next date on which you can legally re-enter Schengen and for how long at most.&lt;/p&gt;

&lt;p&gt;In particular, column F reflects spare days at the start of the stay, and column G reflects how many days the stay could still be extended at its end (or, if negative, how many days must be removed from the end of the stay). A stay is marked illegal if either the start date is too early or the end date is too late.&lt;/p&gt;

&lt;p&gt;Everything below this main input/results box (everything below row 16) is solely for the calculations being performed and should not be tampered with (and is quite complicated). If giving this spreadsheet to a friend or loved one that may be prone to inputting values outside of the designated cells and breaking the spreadsheet, &lt;strong&gt;I would recommend using &lt;a href=&quot;https://support.microsoft.com/en-us/office/lock-or-unlock-specific-areas-of-a-protected-worksheet-75481b72-db8a-4267-8c43-042a5f2cd93a&quot;&gt;Excel’s protection features&lt;/a&gt;&lt;/strong&gt; to lock down all but the blue input cells of the spreadsheet before distributing it to others.  In fact, here is a separate version of the spreadsheet where I have gone ahead and locked down all but the intended input cells:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://hratliff.com/files/Schengen_schedule_calculator_protected.xlsx&quot;&gt;&lt;font color=&quot;#709E4A&quot;&gt;[Download link for the &quot;protected&quot; version: Schengen_schedule_calculator_protected.xlsx]&lt;/font&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this spreadsheet can be a useful tool for anyone trying to plan out and schedule days in the Schengen Area, for themselves, a loved one, friends, or family!&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;To address a potential edge case, the sheet as currently designed—optimized for performance—is only capable of handling 10 “stays” and a maximum of 907 days between the first day of the month &lt;em&gt;(minus one day)&lt;/em&gt; of the start date of the first stay (cell B22) and the end of the tenth stay (cell B928).  Provided the substantial number of calculations being conducted within the sheet, expanding this much further can cause performance issues.  However, if you wish to make plans spanning longer than 907 days and/or more than 10 stays, greater capicity is needed.  Solving this issue, an “extended” version of the spreadsheet is linked below.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://hratliff.com/files/Schengen_schedule_calculator_extended.xlsx&quot;&gt;&lt;font color=&quot;#709E4A&quot;&gt;[Download link for the &quot;extended&quot; version: Schengen_schedule_calculator_extended.xlsx]&lt;/font&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this extended spreadsheet, “Sheet1” remains the same; however, the additional “Sheet2” and “Sheet3” are essentially extensions of the first sheet.  Sheet2’s first “stay” will be a copy of the final provided stay on Sheet1, and the calculations further down in the sheet (rows 18 onward) will begin 180 days prior to the end date of the final stay provided on Sheet1, just copying over the relevant state of calculations for those first 180 days from Sheet1 (and thus preserving the dates relevant to the rolling Schengen stay limit).  Sheet3 is the same but starting from the last stay provided on Sheet2.  Note that this means &lt;strong&gt;the ordering of the sheets matters&lt;/strong&gt; given that they reference each other!  The sheets should be ordered chronologically; you can essentially think of each additional sheet as an extension of the previous one.  If you wish to extend the number of sheets further than the three included in this spreadsheet, follow the below steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Right click the rightmost sheet tab at the bottom and select “Move or Copy…”&lt;/li&gt;
  &lt;li&gt;In the popup menu, tick the “Create a copy” box and select “(move to end)” then click “OK”
    &lt;ul&gt;
      &lt;li&gt;Note that the copying process will take some time; you can monitor its progress on the right side of the bottom bar reading “Calculating (X Threads): ##%”.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;In the newly created sheet, delete the contents of the cells in range &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C7:D15&lt;/code&gt; (all light-blue shaded start/end dates in the table, excluding the first pair of dates with a dark cell color)&lt;/li&gt;
  &lt;li&gt;Rename the newly created sheet to be in sequence with the others (e.g., “Sheet4” instead of “Sheet3 (2)”)&lt;/li&gt;
  &lt;li&gt;Use the new sheet as normal!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;The only caveat to mention is that this 907-day limit per sheet still exists, and if this is to be exceeded before the 10/9-stay limit per sheet, the next sheet will work fine but only if the final stay of the preceding sheet still fits within the 907-day limit (i.e., there should not be any errors in the main input/results table area for the last stay in any given sheet).&lt;/em&gt;&lt;/p&gt;

&lt;!--
Well, if you found this, here's the password to unprotect the abvove sheet:
W9dQms3%_j5@i1uykJvEM1Kr]?uP;:gF%Y;1vRr!JmCpW)9hzY
--&gt;

&lt;!--
-------------------------
*As a technical side note, the spreadsheet as currently written is only capable of handling dates for all stays within a 907-day window starting on the date specified in cell B22.  By default, this is set to the first day of the month of the date inputted into cell C6 (the start date of Stay 1), or 1 June 2023 if left blank.*
--&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="Schengen" /><category term="calculator" /><category term="Excel" /><category term="spreadsheet" /><category term="Schengen calculator" /><summary type="html"></summary></entry><entry><title type="html">Controlling iCloud/OneDrive file sync status in CMD</title><link href="https://lindt8.github.io/posts/icloud-onedrive-syncing-in-cmd/" rel="alternate" type="text/html" title="Controlling iCloud/OneDrive file sync status in CMD" /><published>2024-01-27T00:00:00-08:00</published><updated>2024-01-27T00:00:00-08:00</updated><id>https://lindt8.github.io/posts/blog-post-7</id><content type="html" xml:base="https://lindt8.github.io/posts/icloud-onedrive-syncing-in-cmd/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;p&gt;If you ever find yourself needing to control the sync status of a OneDrive or iCloud for Windows file from a batch file or CMD, it is fortunately quite straightforward with ATTRIB.exe built into Windows (&lt;a href=&quot;https://ss64.com/nt/attrib.html&quot;&gt;comprehensive documentation here&lt;/a&gt;), which is used to view and change file attributes.  Here, we are only concerned with three particular attributes (with their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib&lt;/code&gt; flags in parenthesis): Offline (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;), Pinned (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt;), and Unpinned (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;U&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;In CMD, using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib&lt;/code&gt; command with just a filename (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib filename.txt&lt;/code&gt;) without flags prints the file’s current attributes and path.  By adding flags with a plus/minus (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt;) between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib&lt;/code&gt; and the filename, the file’s individual attributes are changed and toggled on/off (True/False). Putting a plus before the flag tells it to set that attribute to True (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+P&lt;/code&gt;)  while a minus sign sets it to false (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-P&lt;/code&gt;). Each of the various icons indicating sync status of a file corresponds to some combination of these flags being enabled or disabled:&lt;/p&gt;

&lt;style&gt;
table th:first-of-type {
    width: 5%;
}
&lt;!--
table th:nth-of-type(2) {
    width: 11%;
}
table th:nth-of-type(3) {
    width: 17%;
}
table th:nth-of-type(4) {
    width: 67%;
}
--&gt;
&lt;/style&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Icon&lt;/th&gt;
      &lt;th&gt;Attributes&lt;/th&gt;
      &lt;th&gt;Set with&lt;/th&gt;
      &lt;th&gt;Explanation&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;img src=&quot;/files/icon_Blue-cloud.png&quot; style=&quot;width:150%;&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O  U&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O &lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+U&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;“Only available online” files, in the cloud only and not locally available&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;img src=&quot;/files/icon_Solid-green-circle.png&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+P&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;“Always keep on this device” files, locally available and will be kept downloaded&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;img src=&quot;/files/icon_Green-tick.png&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;` `&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-P&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+P&lt;/code&gt; then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-P&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;“Available on this device” files, locally available (will be made online-only if space is needed)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;img src=&quot;/files/icon_Sync-in-progress.png&quot; /&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O P&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O &lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;n/a&lt;/td&gt;
      &lt;td&gt;Sync in progress (status when a file is downloading or queued to be downloaded)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p class=&quot;tablelines&quot;&gt;Note that the Offline (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;) flag is something that just updates automatically and is not something we set. Setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;U&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt; to True automatically sets the other to False, resulting in Unpinned (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;U&lt;/code&gt;) files being online only and Pinned (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt;) files being always locally available.  Both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;U&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt; can be set to False simultaneously, resulting in the file being locally available but not necessarily &lt;em&gt;always&lt;/em&gt; available.&lt;/p&gt;

&lt;p&gt;In the picture below is an example with a file called IMG_6736.MOV in my iCloud Photos library.  The file goes through the following steps:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;It begins as online only (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;It’s then “pinned” (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+P&lt;/code&gt;) to be always kept on this device&lt;/li&gt;
  &lt;li&gt;We check to see the attributes while queued/downloading (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O P&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;We check to see the attributes after the download finishes (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;Its “pinned” status is revoked (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-P&lt;/code&gt;) to remain available but not necessarily always downloaded&lt;/li&gt;
  &lt;li&gt;We check its attributes and see both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;U&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt; are False&lt;/li&gt;
  &lt;li&gt;It’s finally “unpinned” (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+U&lt;/code&gt;) to remove the download and make it online only again&lt;/li&gt;
  &lt;li&gt;And we check its attributes to verify its online only status (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O U&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/files/Sync_attrib_example.png&quot; style=&quot;width:100%;&quot; /&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-icloud-photos-use-case&quot;&gt;The iCloud Photos use case&lt;/h2&gt;

&lt;p&gt;My motivation for looking into all of this in the first place revolved around iCloud Photos and its &lt;em&gt;abhorrently terrible&lt;/em&gt; performance when trying to download many files at once.  This seems like a normal use case, no?  I have just downloaded iCloud for Windows, and I want local copies of all of my iPhone’s photos that are synced to iCloud.  I have nearly 28,000 photos, so I right click the “Photos” folder and select “Always keep on this device” then go to bed.  I wake up to see in the window from the iCould taskbar tray icon that there was essentially no progress in eight hours.  After some troubleshooting, I eventually right click the “Photos” folder and select “Free up space” to stop this stalled process.&lt;/p&gt;

&lt;p&gt;If iCloud can’t handle trying to download 28,000 files, how many can it?  After some amount of testing, I discover 50 is about the sweet spot.  If I select 50 files and set them to “Always keep on this device”, it takes on the order of five minutes (to a bit less) before they are all downloaded.  If I do the same but with a selection of 75 files, this time increass to a bit over 10 minutes.  With 200 files, it varied but was on the order of an hour.  When I had 500 selected, it took well over six hours.  It seems iCloud gets increasingly bogged down the longer its queue for files to be downloaded is.&lt;/p&gt;

&lt;p&gt;So, what am I to do?  Am I really going to babysit my iCloud Photos File Explorer window and spoonfeed it 50 files at a time to download every five minutes or so… &lt;strong&gt;for over 550 cycles&lt;/strong&gt;?!? Assuming perfect efficiency, that’s 45 hours of tending to it.  Arguably better (but perhaps wrose even), I could feed it ~500 files to download overnight and do this nightly for nearly two months…  These are obviously not sensible solutions.&lt;/p&gt;

&lt;p&gt;What I needed was a way to automate this.  A scripted solution.  Thus, the batch script at the bottom of this article was created.  The script scans through all files in the “Photos” directory and sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib +P&lt;/code&gt; to any files it sees are currently set as Offline (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;).  Once it has “pinned” 50 files, a sleep/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timeout&lt;/code&gt; timer of 300 seconds is activated to give iCloud time to download the files.  The counter is then reset, and the scan continues.&lt;/p&gt;

&lt;!--It is not particularly elegant or efficient, but, frankly, it doesn't need to be as iCloud's terrible performance and, to a lesser extent, my network speed are the bigger bottlenecks.--&gt;

&lt;p&gt;One small hiccup I did encounter was that the substantially larger (and sporadically located) .MOV video files were taking longer to download and would sometimes cause a backup where the 5 minute timer was insufficient to download the 50 flagged files if they contained too many/large .MOV files.  This then spiralled the queue out of control as iCloud choked on the increasing number of files being fed to it, slowing down even more the further behind it got.  To circumvent this, I would suggest running the script first with the directory scan only looking for .MOV files (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for %%f in (Photos\*.MOV)&lt;/code&gt;) with a lower counter value before the sleep timer is activated, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if !count!==10&lt;/code&gt;, and perhaps with a longer sleep timer too depending on your video file sizes and network speed.  After all the larger video files are downlaoded, just rerun the script but searching for all file extensions and allowing the count to get up to 50.  The counter limit and sleep timer duration are values you can play around with and optimize for what prevents backlogs in your own environment.&lt;/p&gt;

&lt;p&gt;As a warning, it may be tempting to reduce the sleep timer as much as possible to maximize efficiency, the percentage of time spent actually downloading files rather than sleeping.  However, especially if you would plan to leave the script running unattended for hours, I would caution against getting too close to “maximum” efficiency.  Remember, when iCloud gets overburdened with too many files queued, &lt;em&gt;it slows down&lt;/em&gt;, meaning if you keep feeding it more files to download at a constant rate, it will just continuously slow down more and more and fail catastrophically, getting hung up and (nearly) ceasing all downloads.  Thus, I think it is best to give it ample time to download the “slowest” batch of files fed to it at a time.&lt;/p&gt;

&lt;p&gt;The below Windows batch script was stored in and executed from the “iCloud Photos” directory, which contains the “Photos” subdirectory holding all of the photo and video files synced to iCloud Photos.  Beneath it is some more details on the script.&lt;/p&gt;

&lt;div class=&quot;language-bat highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@echo &lt;span class=&quot;na&quot;&gt;off&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;setlocal&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;enableextensions&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;enabledelayedexpansion&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/a &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;vm&quot;&gt;%%f&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;Photos&lt;/span&gt;\&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;.&lt;span class=&quot;o&quot;&gt;*)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;attrib&lt;/span&gt; &lt;span class=&quot;vm&quot;&gt;%%f&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;attrib&lt;/span&gt; &lt;span class=&quot;vm&quot;&gt;%%f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;findstr&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;^.&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;......O&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;nul&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;attrib&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;+P &lt;/span&gt;&lt;span class=&quot;vm&quot;&gt;%%f&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/a &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;!count!&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;Current&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;!time!&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;Waiting&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;min&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;previous&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;files&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;download&lt;/span&gt;...
      &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/a &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;timeout&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;-t &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;300&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;nul&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The script’s first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for&lt;/code&gt; loop scans over all files in the Photos directory.  The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib&lt;/code&gt; command is called on its own just to print to the terminal all the files and their attributes as the code progresses.  The next &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib&lt;/code&gt; call has its output piped to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findstr&lt;/code&gt; call that looks for an “&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;” in the eighth position; for a file with the Offline attribute, the eigth character in the string printed by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib&lt;/code&gt; call is an “&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;”, hence the seven periods followed by an “&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;” in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findstr&lt;/code&gt; call.  If an “&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;” is found, the code executes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib +P&lt;/code&gt; to the file and increments the counter variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; for the queued files.  The loop proceeds like this until the counter reaches 50.  Then, the code prints the current time and waits for 300 seconds, set by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timeout&lt;/code&gt; call, to give iCloud time to download the 50 files that had just been pinned and slated for download.  The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; variable is reset to 0, and then the code proceeds with this loop until all files have been cycled through.&lt;/p&gt;

&lt;p&gt;My initial version of the script I came up with is shown below, with its own explanation following.  It is similar but is quite a bit slower at scanning through the directory.&lt;/p&gt;

&lt;div class=&quot;language-bat highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@echo &lt;span class=&quot;na&quot;&gt;off&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;setlocal&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;enableextensions&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;enabledelayedexpansion&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/a &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;vm&quot;&gt;%%f&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;Photos&lt;/span&gt;\&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;.&lt;span class=&quot;o&quot;&gt;*)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/f &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;delims=&quot;&lt;/span&gt; &lt;span class=&quot;vm&quot;&gt;%%a&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'attrib &lt;/span&gt;&lt;span class=&quot;vm&quot;&gt;%%f&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;readvalue=&lt;/span&gt;&lt;span class=&quot;vm&quot;&gt;%%a&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;!readvalue!&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;IsOffline&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;call&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;:CheckAttributes&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;!IsOffline!&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;attrib&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;+P &lt;/span&gt;&lt;span class=&quot;vm&quot;&gt;%%f&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/a &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;!count!&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;files&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;download&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;queue&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;timeout&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;-t &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;nul&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;!count!&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;Current&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;!time!&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;Waiting&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;min&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;previous&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;files&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;download&lt;/span&gt;...
    &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/a &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;timeout&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;-t &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;300&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;nul&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;:CheckAttributes&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/a &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;ichar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/F &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;delims=&quot;&lt;/span&gt; &lt;span class=&quot;vm&quot;&gt;%%G&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'cmd /D /U /C  echo &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;!readvalue!&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;^|&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt; find /V &quot;&quot;'&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;/a &lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;ichar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;vm&quot;&gt;%%G&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;O&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;IsOffline&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;goto&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;:break&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;!ichar!&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;17&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;goto&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;:break&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;:break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The script’s first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for&lt;/code&gt; loop scans over all files in the Photos directory.  The line immediately beneath that is essentially just executing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib filename.txt&lt;/code&gt; but storing the string output of that command into the variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;readvalue&lt;/code&gt;.  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IsOffline&lt;/code&gt; is initialized as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt;, and then the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CheckAttributes&lt;/code&gt; function (defined at the bottom, outside the main loop) is called, which scans &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;readvalue&lt;/code&gt; character by character, checking if any of the characters are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;O&quot;&lt;/code&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if &quot;%%G&quot;==&quot;O&quot;&lt;/code&gt;) indicating the presence of the Offline flag and setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IsOffline=1&lt;/code&gt; if an “O” is present.  To avoid reading characters in the file path portion of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib&lt;/code&gt; output, the function terminates and returns to the code’s main loop after checking the first 17 characters (or once an “O” is found).&lt;/p&gt;

&lt;p&gt;Then, if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IsOffline&lt;/code&gt; is 1, the code executes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attrib +P&lt;/code&gt; to the file and increments the counter variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; for the queued files.  The loop proceeds like this until the counter reaches 50.  Then, the code prints the current time and waits for 300 seconds, set by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timeout&lt;/code&gt; call, to give iCloud time to download the 50 files that had just been pinned and slated for download.  The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; variable is reset to 0, and then the code proceeds with this loop until all files have been cycled through.&lt;/p&gt;

&lt;p&gt;The slowest part of this code is the character-by-character check for the attributes in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CheckAttributes&lt;/code&gt; function call.  Perhaps there is a faster way, but in my troubleshooting I had difficulty getting other approaches to work.  And, as I stated earlier, efficiency wasn’t my greatest concern given how slow iCloud is in the first place.&lt;/p&gt;

&lt;p&gt;Also, while my script searches for the presence of the Offline flag, a slightly more robust way would be to search for the &lt;em&gt;absence&lt;/em&gt; of the Pinned flag.  In the script, renaming &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IsOffline&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IsPinned&lt;/code&gt; and changing the logic in the first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; statement to be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if !IsPinned!==0&lt;/code&gt; would accomplish this.  I did not implent it this way myself because this essentially made the default action of the code to “do something” if my attribute checking were to fail in some way, though I now believe it would be perfectly fine.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;As a disclaimer, I only tested all of this on iCloud for Windows files; however, iCloud for Windows works in the same way as OneDrive for file syncing attributes.  In fact, as I was troubleshooting all of this myself, all of the resources I used referenced OneDrive.&lt;/em&gt;&lt;/p&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="iCloud" /><category term="OneDrive" /><category term="cmd" /><category term="batch" /><summary type="html"></summary></entry><entry><title type="html">AI-generated artwork</title><link href="https://lindt8.github.io/posts/ai-art/" rel="alternate" type="text/html" title="AI-generated artwork" /><published>2022-09-23T00:00:00-07:00</published><updated>2022-09-23T00:00:00-07:00</updated><id>https://lindt8.github.io/posts/blog-post-6</id><content type="html" xml:base="https://lindt8.github.io/posts/ai-art/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;p&gt;Inspired by a &lt;a href=&quot;https://arstechnica.com/information-technology/2022/08/ai-wins-state-fair-art-contest-annoys-humans/&quot;&gt;recent news story&lt;/a&gt; and an episode of the &lt;a href=&quot;https://www.relay.fm/cortex/133&quot;&gt;Cortex podcast&lt;/a&gt;, I made the below presentation to present to my friends and colleagues informally at a social meeting of the PhD students and postdocs in my department/institute at HVL.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://hratliff.com/files/AI-art.pdf&quot;&gt;&lt;font color=&quot;#709E4A&quot;&gt;[Direct link to PDF of the presentation]&lt;/font&gt;&lt;/a&gt;&lt;/p&gt;

&lt;iframe src=&quot;/files/AI-art.pdf&quot; style=&quot;width: 100%;height: 800px;border: none;&quot;&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;em&gt;This presentation was originally given at an HVL PhD social meeting on 23 September 2022.&lt;/em&gt;&lt;/p&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="AI" /><category term="artwork" /><summary type="html"></summary></entry><entry><title type="html">Is nuclear power our savior?</title><link href="https://lindt8.github.io/posts/is-nuclear-power-our-savior/" rel="alternate" type="text/html" title="Is nuclear power our savior?" /><published>2022-02-25T00:00:00-08:00</published><updated>2022-02-25T00:00:00-08:00</updated><id>https://lindt8.github.io/posts/blog-post-5</id><content type="html" xml:base="https://lindt8.github.io/posts/is-nuclear-power-our-savior/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;p&gt;Nuclear energy is one of our best tools on the path to a greener future, triumphing where many renewable technologies struggle in reliability and output, while being similarly—or even more—green and safe.  How is this so?&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;/files/figures/EPR_OLK3_TVO_fotomont_2_Vogelperspektive.jpg&quot; style=&quot;width:100%;&quot; /&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;Finland’s Olkiluoto nuclear power plant’s newly opened unit 3 (left) and older units 1 and 2 (right).  Source: &lt;a href=&quot;https://en.wikipedia.org/wiki/File:EPR_OLK3_TVO_fotomont_2_Vogelperspektive.jpg&quot;&gt;Wikipedia&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Energy is mankind’s most valuable resource, behind only water. Harnessing it propelled humanity into the industrial revolution, first converting thermal energy simply to mechanical energy and later, perhaps the greatest revolution, to electrical energy. Most global electrical power is still produced through these means: heating water to make steam to turn a turbine to generate electricity. Traditionally, this heat came from the combustion of fossil fuels; however, the past century has seen a few innovations on that front. The most substantial of these is nuclear energy, currently responsible for &lt;a href=&quot;https://ourworldindata.org/electricity-mix&quot;&gt;about 10%&lt;/a&gt; of all electricity produced globally.&lt;/p&gt;

&lt;p&gt;Nuclear power exploits the nature’s strong nuclear fundamental force, letting us convert mass to energy per Einstein’s famous E=mc². The density of this energy is staggering.  A 10 g uranium fuel pellet in a reactor releases approximately &lt;a href=&quot;https://whatisnuclear.com/energy-density.html&quot;&gt;the same energy as&lt;/a&gt; 1.3 tons of coal, 1300 L of oil, or 1100 m³ of natural gas, about 40 gigajoules (~11,000 kWh&lt;sub&gt;th&lt;/sub&gt;). The “breeder” reactors of the future increase these numbers at least tenfold with greater utilization of the nuclear fuel while simultaneously dramatically reducing the radioactive longevity of the spent fuel left behind.&lt;/p&gt;

&lt;p&gt;Nuclear plants also boast the &lt;a href=&quot;https://www.energy.gov/ne/articles/what-generation-capacity&quot;&gt;highest uptime&lt;/a&gt; at maximum output of all energy sources, making them an extremely reliable source of baseload electricity (the constant demand, regardless of time of day), a role none of the other, often weather-dependent, low-carbon energy sources are well-suited for, aside from hydro in abundance, which Norway happens to be naturally blessed with. Nature did not deal such a generous hand to the rest of the world.&lt;/p&gt;

&lt;p&gt;In the pursuit of a safer and carbon-free energy future, what role does nuclear play?  &lt;a href=&quot;https://ourworldindata.org/nuclear-energy&quot;&gt;Per GWh electricity produced&lt;/a&gt;, nuclear is actually a bit greener than wind or solar in greenhouse gas emissions and results in comparably few deaths from pollution and accidents as renewables. For reference, these emissions and fatality figures from oil and coal are hundreds of times larger.  It seems evident renewable energy should join hands with nuclear to overcome some of its largest shortcomings in reliability and throughput.&lt;/p&gt;

&lt;p&gt;Some challenges persist, more political than technical. Radiation’s invisibility and mystique easily trigger our evolutionary instincts to fear the unknown, and this is further amplified by nuclear energy’s unforgettable origins and few high-profile accidents, leading to a large “not in my backyard” sentiment. Risk assessment is not one of our natural strengths; many similarly fear flight but feel at ease in cars, despite the safety statistics painting a starkly different picture. Another challenge is economic. Though nuclear fuel is cheap, startup costs are high, and investors are too impatient to wait for easy profits decades away, especially if seen as a risk due to political volatility. Fortunately, as public knowledge on nuclear is rising, so is its image and its odds of helping us achieve a greener future.&lt;/p&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h3 id=&quot;quick-facts&quot;&gt;Quick facts&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Electricity produced in 1 year: 1 nuclear plant = 980 wind turbines, 25500 m² solar panels, or 100 average Norwegian hydroelectric plants.&lt;/li&gt;
  &lt;li&gt;Nuclear accounts for the least CO₂ per GWh produced of all major energy sources.&lt;/li&gt;
  &lt;li&gt;Deaths from accidents and pollution per GWh for nuclear are 341 times lower than for coal, 263 for oil, and 40 for natural gas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was originally written to be translated to Norwegian and submitted to Bergens Tidene for publication; the text featured here is the original article pre-translation. A dialog-formatted version of this text in Norwegian (nynorsk) was &lt;a href=&quot;https://www.bt.no/btmeninger/debatt/i/L5Rv79/atomdialog&quot;&gt;published in the 19 May 2022 issue of Bergens Tidene&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="nuclear energy" /><category term="green energy" /><summary type="html"></summary></entry><entry><title type="html">Interactive world data map generation tool</title><link href="https://lindt8.github.io/posts/interactive-world-data-map-tool/" rel="alternate" type="text/html" title="Interactive world data map generation tool" /><published>2021-03-14T00:00:00-08:00</published><updated>2021-03-14T00:00:00-08:00</updated><id>https://lindt8.github.io/posts/blog-post-4</id><content type="html" xml:base="https://lindt8.github.io/posts/interactive-world-data-map-tool/">&lt;p&gt;&lt;!--  o --&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;This page serves to discuss the usage and behaviors of the &lt;a href=&quot;https://github.com/Lindt8/Interactive_world_data_map&quot;&gt;Interactive world data map generator&lt;/a&gt; tool that I wrote.  The GitHub page’s README covers required packages and very basic usage, so this page seeks to take a deeper look at the intended input formats and settings, the logic behind some of the code’s options, and how the maps generated by this code differ from those generated by the base Pygal library which this code uses.&lt;/p&gt;

&lt;hr /&gt;

&lt;!--
Add to &lt;svg&gt; tag:
viewBox=&quot;0 100 800 400&quot; preserveAspectRatio=&quot;xMidYMid meet&quot; width=&quot;100%&quot; height=&quot;100%&quot;
--&gt;

&lt;p&gt;This code serves to easily create interactive world maps such as the one below showing the number of &lt;a href=&quot;https://en.wikipedia.org/wiki/All-time_Olympic_Games_medal_table&quot;&gt;Olympic medals won&lt;/a&gt; by each country.  The code’s input data can be in an Excel spreadsheet, a .csv (comma separated values) file, or a .tsv (tab separated values) file.  (Any other file formats provided are assumed to be CSV files.)  It outputs a Scalable Vector Graphics (SVG) file containing the map and an HTML file with the map embedded along with a table of all of the data on the map.&lt;/p&gt;

&lt;p&gt;Hovering over a country reveals the value associated with it (if nonzero).  Hovering over each bin in the legend will highlight all countries in that bin, and clicking the bins in the legend will toggle whether those countries belonging to each bin are shown on the map or not.&lt;/p&gt;

&lt;div class=&quot;fluid-width-video-wrapper&quot; style=&quot;padding-top: 77%; text-align: center;&quot;&gt;&lt;embed src=&quot;/files/Olympic_medals_per_country.svg&quot; type=&quot;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://hratliff.com/files/world-map-tool/standalone/Olympic_medals_per_country.svg&quot;&gt;[view standalone SVG in fullscreen]&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;basic-operation&quot;&gt;Basic operation&lt;/h2&gt;

&lt;p&gt;When the code is ran, the GUI shown below will pop up (but with default entries).  This article will discuss these various settings.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;&lt;img src=&quot;/files/world-map-tool/map_gui_medals.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The default value of “Input file” will be either a user-provided file path taken from the first line of a file titled “default_path_to_data_file.txt” if the code detects this .txt file in the working directory, or it will default to “default.xlsx” in the current working directory.  A different file can be selected using the Browse button or by manually entering a new path.  When the code is ran for the first time for a given input file (or if the input file has been updated since the last time the code was ran), the code will carefully parse the file and print diagnostic information to the screen, allowing you to check if the listed countries are being properly identified.  If “Run” is selected, the window will close and this information is printed to the terminal as usual; if “Run (keep window open)” is selected, a popup window (shown below) to which the terminal output is redirected will appear.&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;&lt;img src=&quot;/files/world-map-tool/terminal_output.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Keeping the window open can be useful for trying different colors and settings quickly.  After the file has been carefully parsed, its data and the GUI’s settings are saved to a file of the same name as the input data file but ending with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pickle&lt;/code&gt; extension instead.  Then, in future cases when the code is ran, if it discovers that this pickle file is present in the same folder as the source data file and that the source data file has not been modified since the pickle was created or last updated, the code will use the data stored in the pickle file instead of rereading the source data file, speeding the code up notably.  Additionally, anytime the code is ran and the corresponding pickle file is discovered, the GUI will automatically change its settings to match those used the last time the pickle file was updated, and any new or modified settings will be saved to the pickle file for future iterations.  For Excel spreadsheet files, you can select data from any of its sheets, and the pickle file will independently save the data from and GUI settings used for each sheet.  Updating the Input file or the sheet name causes the code to search for the pickle file and to update the GUI if found.&lt;/p&gt;

&lt;h2 id=&quot;tallied-vs-untallied-data&quot;&gt;Tallied vs Untallied data&lt;/h2&gt;

&lt;p&gt;The most important option provided by the code is the type of data: tallied or untallied.  Tallied data is presented as a list of countries with values already assigned (population per country, &lt;a href=&quot;https://en.wikipedia.org/wiki/All-time_Olympic_Games_medal_table&quot;&gt;Olympic medals won per country&lt;/a&gt; as shown at the top of this page, etc.) and would be in a spreadsheet/data file tabulated as shown below.  In this case, the code needs to know the column numbers for both where the country names are listed and where the data values are listed.  If a country is identified multiple times, the values from each are summed; for instance, in this example the number of medals for Germany includes those from “Germany”, “West Germany”, “East Germany”, and the “Unified Team of Germany”.&lt;/p&gt;

&lt;table class=&quot;tablelines&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Country&lt;/th&gt;
      &lt;th&gt;Total medals won (Summer + Winter Games)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;United States&lt;/td&gt;
      &lt;td&gt;2828&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Russia (and Soviet Union)&lt;/td&gt;
      &lt;td&gt;1776&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Germany (modern, East, West, and United Team of)&lt;/td&gt;
      &lt;td&gt;1754&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;United Kingdom (Great Britain)&lt;/td&gt;
      &lt;td&gt;883&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;France&lt;/td&gt;
      &lt;td&gt;840&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Untallied data consists of just a list of countries, and the number of appearances of each country is counted.  This would be useful if you hosted an international event, had a list of all of the attendees and their countries, and wanted to make a map illustrating how many attendees you had from each country.  The code takes care of summing up the number of attendees per country for you.  The example of this type of data presented below is generating a map of the number of Olympic Games (Summer and Winter) hosted by each country provided a list of the &lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_Olympic_Games_host_cities&quot;&gt;locations of all Olympic Games&lt;/a&gt;.  For the sake of this example, canceled events are excluded while tentative future events are included.  Some of the data is presented in the table below.&lt;/p&gt;

&lt;table class=&quot;tablelines&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;City&lt;/th&gt;
      &lt;th&gt;Country&lt;/th&gt;
      &lt;th&gt;Year&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Atlanta&lt;/td&gt;
      &lt;td&gt;United States&lt;/td&gt;
      &lt;td&gt;1996&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Nagano&lt;/td&gt;
      &lt;td&gt;Japan&lt;/td&gt;
      &lt;td&gt;1998&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Sydney&lt;/td&gt;
      &lt;td&gt;Australia&lt;/td&gt;
      &lt;td&gt;2000&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Salt Lake City&lt;/td&gt;
      &lt;td&gt;United States&lt;/td&gt;
      &lt;td&gt;2002&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Athens&lt;/td&gt;
      &lt;td&gt;Greece&lt;/td&gt;
      &lt;td&gt;2004&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Turin&lt;/td&gt;
      &lt;td&gt;Italy&lt;/td&gt;
      &lt;td&gt;2006&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Beijing&lt;/td&gt;
      &lt;td&gt;China&lt;/td&gt;
      &lt;td&gt;2008&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Vancouver&lt;/td&gt;
      &lt;td&gt;Canada&lt;/td&gt;
      &lt;td&gt;2010&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;London&lt;/td&gt;
      &lt;td&gt;United Kingdom&lt;/td&gt;
      &lt;td&gt;2012&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Sochi&lt;/td&gt;
      &lt;td&gt;Russia&lt;/td&gt;
      &lt;td&gt;2014&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Rio de Janeiro&lt;/td&gt;
      &lt;td&gt;Brazil&lt;/td&gt;
      &lt;td&gt;2016&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Pyeongchang&lt;/td&gt;
      &lt;td&gt;South Korea&lt;/td&gt;
      &lt;td&gt;2018&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;To generate the map, the number of instances each country name appears in the data is counted.  For example, the United States appears 2 times in the above table (but a total of 9 times in the Wikipedia table including past and future events).  Once the data is tallied, the map below can be generated.&lt;/p&gt;

&lt;div class=&quot;fluid-width-video-wrapper&quot; style=&quot;padding-top: 75%; text-align: center;&quot;&gt;&lt;embed src=&quot;/files/world-map-tool/Olympic_events_per_country.svg&quot; type=&quot;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://hratliff.com/files/world-map-tool/standalone/Olympic_events_per_country.svg&quot;&gt;[view standalone SVG in fullscreen]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;For both tallied and untallied data, the code also always needs to know how many header rows there are (rows at the top which will be skipped).  And, as noted in the GUI, letters (as used by Excel) may be used for the column numbers.  If Excel-style letters are provided for column numbers for non-Excel files, they will be converted to integer numbers.&lt;/p&gt;

&lt;p&gt;The map of Olympic events per country above was generated with the following settings in the GUI:&lt;/p&gt;

&lt;p align=&quot;center&quot;&gt;&lt;img src=&quot;/files/world-map-tool/map_gui_events.png&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;output-options&quot;&gt;Output options&lt;/h2&gt;

&lt;p&gt;The output folder will default to the same directory as the input file, though this can be customized.  This is where the output .svg and .html files will be written, and their filenames can be customized.&lt;/p&gt;

&lt;p&gt;The maps have a single base color.  The countries in the highest bin will adopt this color while lower bins will have this color mixed with increasing amounts of white.  Clicking the “Pick color” button opens a color picking window, and the color of the button afterward will be updated to reflect your selection.&lt;/p&gt;

&lt;p&gt;The HTML embedding options for the SVG file are discussed in detail later on this page due to being a slightly more complicated topic.&lt;/p&gt;

&lt;p&gt;Most of the text fields have self-evident names, controlling the map title, the text appearing in the boxes when hovering over a country on the map, the text in the legend in the bottom left of the map, and the column titles of the HTML table.  UTF-8 characters are supported in these fields, not just ASCII.&lt;/p&gt;

&lt;h2 id=&quot;binning&quot;&gt;Binning&lt;/h2&gt;

&lt;p&gt;The custom binning of data is one of the most valuable features of this tool, and a variety of options are available.  In principle, there are four main binning styles: (1) providing manual bin edges, having the code automatically calculate evenly spaced bin edges on either a (2) linear or (3) logarithmic scale, or (4) using the default binning structure of Pygal, ignoring all of the other binning features (not recommended and only available when “Show legend” is disabled).&lt;/p&gt;

&lt;p&gt;When manually entering bins, they must be provided as a list of increasing numbers separated by commas.  Note that you are specifying bin &lt;em&gt;edges&lt;/em&gt; here, meaning the nominal number of bins is one less than the number of values provided.&lt;/p&gt;

&lt;p&gt;When automatic binning is selected, the code automatically constructs evenly spaced bins in either linear or logarithmic space, and you can specify the desired number of bins.&lt;/p&gt;

&lt;p&gt;There are two special options which impact the meaning of each bin: (1) the overflow bin and (2) forcing integer bin edges.  When the overflow bin is enabled, an extra bin containing everything above the original final bin maximum is added to the plot.  In the case of the automatically generated bins, this effectively just increases the selected number of bins by 1.&lt;/p&gt;

&lt;p&gt;Forcing integer bin edges does a few things.  First, the automatically generated bins edges, which often are decimal values, are forced to be integers.  (Manually entered bin edges are not affected.)  Second, it causes the data to be interpreted as if it were “counted” integer data (regardless of whether it actually is).  When this is the case, the left edge of each bin after the first one is increased by 1.  When forced integer bin edges are disabled, the data is assumed to be decimal, and the bin edges are set to contain all numbers between the minimum and maximum listed value.&lt;/p&gt;

&lt;p&gt;By default, both of these options, the additional overflow bin and forced integer bin edges, are enabled.  The behavior of these two settings is illustrated in the table below where “V_lower” and “V_upper” are consecutive bin edges (either specified manually or automatically calculated) and where square brackets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[]&lt;/code&gt; include the values next to them ([v1,v2] means ≥v1 and ≤v2) while parentheses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;()&lt;/code&gt; do not include the values next to them ((v1,v2) means &amp;gt;v1 and &amp;lt;v2).&lt;/p&gt;

&lt;table class=&quot;tablelines&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Binning Logic&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;add overflow bin = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;&lt;br /&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;add overflow bin = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;&lt;br /&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;force integer bin edges = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;&lt;br /&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;First bin: [V_lower,V_upper]&lt;br /&gt;Mid bins: [V_lower+1,V_upper]&lt;br /&gt;Last bin: [V_lower+1,V_upper]&lt;br /&gt;Overflow bin: [V_upper+1,∞)&lt;/td&gt;
      &lt;td&gt;First bin: [V_lower,V_upper]&lt;br /&gt;Mid bins: [V_lower+1,V_upper]&lt;br /&gt;Last bin: [V_lower+1,V_upper]&lt;br /&gt;Overflow bin: n/a&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;force integer bin edges = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;&lt;br /&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;First bin: [V_lower,V_upper]&lt;br /&gt;Mid bins: (V_lower,V_upper]&lt;br /&gt;Last bin: (V_lower,V_upper]&lt;br /&gt;Overflow bin: (V_upper,∞)&lt;/td&gt;
      &lt;td&gt;First bin: [V_lower,V_upper]&lt;br /&gt;Mid bins: (V_lower,V_upper]&lt;br /&gt;Last bin: (V_lower,V_upper]&lt;br /&gt;Overflow bin: n/a&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;As an example, the resulting bins from the four permutations of these special options for manually provided bins &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1, 5, 10, 20, 50&lt;/code&gt; are shown in the table below.&lt;/p&gt;

&lt;table class=&quot;tablelines&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Special options settings&lt;/th&gt;
      &lt;th&gt;Produced bins&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;force integer bin edges = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;, &lt;strong&gt;Yes&lt;/strong&gt; &lt;br /&gt; add overflow bin = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;, &lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;[1,5], [6,10], [11,20], [21,50], [51,∞)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;force integer bin edges = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;, &lt;strong&gt;Yes&lt;/strong&gt; &lt;br /&gt; add overflow bin = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;, &lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;[1,5], [6,10], [11,20], [21,50]&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;force integer bin edges = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;, &lt;strong&gt;No&lt;/strong&gt; &lt;br /&gt; add overflow bin = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;, &lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;[1,5], (5,10], (10,20], (20,50], (50,∞)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;force integer bin edges = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;, &lt;strong&gt;No&lt;/strong&gt; &lt;br /&gt; add overflow bin = &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;, &lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;[1,5], (5,10], (10,20], (20,50]&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;And as you may have noticed on the Olympic Games hosted map, when forced integer bin edges is enabled, any bin with width 1 and containing only a single value is printed to the legend as that single value rather than as a range.&lt;/p&gt;

&lt;h2 id=&quot;html-embedding-options&quot;&gt;HTML embedding options&lt;/h2&gt;

&lt;p&gt;Several different options are provided for embedding the SVG file into HTML since different web backends may behave differently with each method and only allow some to work.  While &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;object&amp;gt;&lt;/code&gt; is generally recommended by online resources, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;embed&amp;gt;&lt;/code&gt; is used for the maps shown on this page.  It is generally not suggested to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;, but it is included for completeness.  All three of these nominally let the SVG file behave like normal, retaining all of its interactivity.  When using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img&amp;gt;&lt;/code&gt; this interactivity is completely destroyed, and the SVG will just be rendered as a static image.  (Note that none of these options affect the SVG file, just how it appears in the HTML page.)&lt;/p&gt;

&lt;p&gt;By default, the SVG files look fine in the unstyled HTML file produced or on their own.  However, depending on your web backend, you may also need to make some minor modifications to the HTML code and the SVG file itself for it to render optimally (in terms of the gray border’s size and positioning) on your own webpage.  For the SVG files shown on this page, I opened the SVG file in a text editor (like Notepad++), and on line 2 within the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; tag I replaced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;viewBox=&quot;0 0 800 600&quot;&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;viewBox=&quot;0 100 800 400&quot; preserveAspectRatio=&quot;xMidYMid meet&quot; width=&quot;100%&quot; height=&quot;100%&quot;&lt;/code&gt; to get the positioning and framing just how I wanted it.  When inserting the Olympic events map from earlier into this page, I used the below HTML code:&lt;/p&gt;

&lt;div class=&quot;language-html 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;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;fluid-width-video-wrapper&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;padding-top: 75%; text-align: center;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;embed&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/files/world-map-tool/Olympic_events_per_country.svg&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Again, depending on your own backend and styling (CSS and/or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags), you may need different adjustments to make the maps appear &lt;em&gt;just right&lt;/em&gt; on your own webpages.&lt;/p&gt;

&lt;h2 id=&quot;how-do-these-maps-differ-from-those-made-with-stock-pygal&quot;&gt;How do these maps differ from those made with stock Pygal?&lt;/h2&gt;

&lt;p&gt;The &lt;a href=&quot;http://www.pygal.org/en/stable/&quot;&gt;Pygal&lt;/a&gt; library is used by this tool for generating the basic core of these maps.  The map generation capability is just one of a variety of very handy functions of the Pygal library.  However, there is quite a bit of value added by using this tool versus Pygal on its own.&lt;/p&gt;

&lt;p&gt;Below is the same map as presented at the top of this page but without any of the additional processing performed by this tool aside from the parsing of the data spreadsheet to automatically populate the dictionary object (with ISO2 country names as the keys) that Pygal accepts as its input, something you would need to construct manually if using Pygal alone.&lt;/p&gt;

&lt;div class=&quot;fluid-width-video-wrapper&quot; style=&quot;padding-top: 77%; text-align: center;&quot;&gt;&lt;embed src=&quot;/files/world-map-tool/Pygal_only_medals-map.svg&quot; type=&quot;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://hratliff.com/files/world-map-tool/standalone/Pygal_only_medals-map.svg&quot;&gt;[view standalone SVG in fullscreen]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;So, in addition to handling the rather time-consuming task of manually constructing the input dictionary object, using Pygal on its own for constructing world maps can produce rather lacking results.  There is no way to control the binning of the data, meaning for some datasets the maps are not particularly informative to look at.  Additionally, there is no way to display the binning used by Pygal, meaning the maps &lt;em&gt;feel&lt;/em&gt; subjective in nature as well, and the text appearing in the legend is identical to the next in the hover boxes.  You may also notice that some of the country names used by Pygal are either uncommon, unnatural, or slightly controversial.&lt;/p&gt;

&lt;p&gt;This tool provides a GUI for easily generating these maps, an automated way to parse and tally data (a nontrivial task in regards to translating country names in a large variety of formats to ISO2 codes, thanks to &lt;a href=&quot;https://github.com/konstantinstadler/country_converter&quot;&gt;coco&lt;/a&gt;), the ability to fully customize the data binning structure, the ability to customize every visible descriptive text field independently, and presenting the country names in more common formats (again, thanks to &lt;a href=&quot;https://github.com/konstantinstadler/country_converter&quot;&gt;coco&lt;/a&gt;).&lt;/p&gt;</content><author><name>Hunter Ratliff</name><email>contact@hratliff.com</email></author><category term="world map" /><category term="Python" /><category term="interactive" /><category term="data" /><category term="binned" /><category term="SVG" /><category term="Pygal" /><summary type="html"></summary></entry></feed>