<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
<title type="text">Evan Kiefl</title>
<generator uri="https://github.com/jekyll/jekyll">Jekyll</generator>
<link rel="self" type="application/atom+xml" href="https://ekiefl.github.io/feed.xml" />
<link rel="alternate" type="text/html" href="https://ekiefl.github.io" />
<updated>2026-03-21T21:36:21+00:00</updated>
<id>https://ekiefl.github.io/</id>
<author>
  <name>Evan Kiefl</name>
  <uri>https://ekiefl.github.io/</uri>
  <email>kiefl.evan@gmail.com</email>
</author>


<entry>
  <title type="html"><![CDATA[Northern Mongolia: a journey into the Taiga]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2025/03/09/north-mongolia/" />
  <id>https://ekiefl.github.io/2025/03/09/north-mongolia</id>
  <published>2025-03-09T00:00:00+00:00</published>
  <updated>2025-03-09T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    &lt;h2 id=&quot;day-1-the-black-market&quot;&gt;Day 1: The black market&lt;/h2&gt;

&lt;p&gt;The text of a nearby ad caught my eye as we waited at the baggage carousel. It was written in two scripts, and to my surprise, neither of them used the Latin alphabet. I could have at least learned the Mongolian phrase for “thank you”, I thought to myself.&lt;/p&gt;

&lt;p&gt;We grabbed our bags and found the exit. Amongst the people waiting in arrivals, I saw someone holding a sign that said “Evan and Kourtney”. I could tell it was Anuka, our tour guide, based on the pictures from her website. Thank God she was real.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_22-52-18-055000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_22-52-18-055000_cam.webp&quot; alt=&quot;Anuka fastening a reindeer during the Eastern reindeer herder Autumnal migration (Day 9).&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Anuka fastening a reindeer during the Eastern reindeer herder Autumnal migration (Day 9).
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After brief introductions she led us outside to the parking lot. She introduced us to a tall skinny man named Baagii, her husband. We would get to know him very well over the next two weeks. It seemed he didn’t speak any English, but with a shy smile, he motioned for us to hand him our bags.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/13/__2023-09-09_04-54-47-121000_kourtney.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/13/__2023-09-09_04-54-47-121000_kourtney.webp&quot; alt=&quot;Kourtney (left), Baagii (center), and me (right) on Baagii&apos;s friend&apos;s speedboat on Khuvsgul Lake (Day 13).&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Kourtney (left), Baagii (center), and me (right) on Baagii&apos;s friend&apos;s speedboat on Khuvsgul Lake (Day 13).
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;On our way to Ulaanbaatar, Mongolia’s capital, Kourtney and I calibrated to Anuka’s accent as she shared bits of information about Mongolia and Ulaanbaatar. She talked quietly with a friendly demeanor. I could tell she loved her country and was eager to share it with us. We were in good hands.&lt;/p&gt;

&lt;p&gt;“We are a quickly developing nation,” Anuka started. 1.8 million people lived in Ulaanbaatar. Including undocumented people, it might be over 2 million, which is well over half of Mongolia’s total population. Mongolians were contending with a blazingly fast speed of urbanization. Smartphones, 4G networks, TV, 9-5 jobs, fast food, and high-rise apartment buildings were pervading the urban lifestyle, and Ulaanbaatar was the hearthstone of this drastic cultural shift.&lt;/p&gt;

&lt;p&gt;As we drove through the rolling hill landscape, littered with thousands of free-roaming cows, goats, and sheep, we crested over a hill and in the distance, we saw Ulaanbaatar sprawled out beneath the backdrop of the Khentii mountains. Along the outskirts of the city, white circular tents called &lt;em&gt;gers&lt;/em&gt; (similar to yurts) sat interspersed among modern buildings and city streets.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/general/ubi.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/general/ubi.webp&quot; alt=&quot;As we drive toward the city center, we watch the view outside transform from scattered gers to increasingly dense clusters of shacks, eventually giving way to high-rise buildings. Our journey along the road&apos;s transect through the city, a symbolic timeline of Ulaanbaatar&apos;s rapid urbanization. &amp;lt;i&amp;gt;Photo credit: Hector Retamal&amp;lt;/i&amp;gt;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    As we drive toward the city center, we watch the view outside transform from scattered gers to increasingly dense clusters of shacks, eventually giving way to high-rise buildings. Our journey along the road&apos;s transect through the city, a symbolic timeline of Ulaanbaatar&apos;s rapid urbanization. &lt;i&gt;Photo credit: Hector Retamal&lt;/i&gt;
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;In the not too distant past, nearly 100% of Mongolians lived in gers, and traveled nomadically with their livestock, perpetually moving their gers with the change of seasons, conditions, and resource availability. Today, many gers are equipped with satellite dishes and solar panels, showcasing the conveniences of modernity knocking on the doorstep of ancient tradition.&lt;/p&gt;

&lt;p&gt;I was taken aback by the sheer chaos of the traffic. Faded road paint feebly demarcated lanes, traffic lights were sparse, and where they did exist, they seemed to serve more as suggestions than mandates.&lt;/p&gt;

&lt;p&gt;As a “fix” to Ulaanbaatar’s traffic problem, the government had instated police-enforced access restrictions at critical bottlenecks in the city traffic flow. This is done by categorizing cars into two groups based on whether the first digit of your license plate number is odd or even. If you’ve got the wrong license plate for a given time, you aren’t allowed to enter the region of the city.&lt;/p&gt;

&lt;p&gt;Eventually, we made it to our hotel, which would serve as a launch point for the trip, which started the next day. With the remainder of the day, our plan was: have a nap, buy supplies for the trip, go to a museum, and get a good night’s rest.&lt;/p&gt;

&lt;p&gt;After our nap, it was 1 pm and we headed downstairs to meet Anuka and Baagii. To buy supplies for the trip, we headed to the black market. “You can buy anything and everything at the black market”, Anuka explained. Clothes, shoes, electronics, containers, tools, building materials, cabinetry, camping gear, school supplies, plastic, art, horse saddles, livestock, meat, fruit, dry goods—the list went on and on, and included more nefarious things like bullets and presumably drugs. Few needs couldn’t be met at the black market.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/1/market.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/1/market.webp&quot; alt=&quot;(left) A horse accessory market stall featuring colorful saddles with intricate embroidery, braided leather reins and ropes hanging prominently, and horse tack and accessories. (right) An eclectic market stall of Buddha statues, hanging necklaces and rosaries, ornate ceremonial knives and sheaths, miniature statues of animals and creatures, and a variety of leather and metal accessories.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/1/market_2.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/1/market_2.webp&quot; alt=&quot;(left) A horse accessory market stall featuring colorful saddles with intricate embroidery, braided leather reins and ropes hanging prominently, and horse tack and accessories. (right) An eclectic market stall of Buddha statues, hanging necklaces and rosaries, ornate ceremonial knives and sheaths, miniature statues of animals and creatures, and a variety of leather and metal accessories.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) A horse accessory market stall featuring colorful saddles with intricate embroidery, braided leather reins and ropes hanging prominently, and horse tack and accessories. (right) An eclectic market stall of Buddha statues, hanging necklaces and rosaries, ornate ceremonial knives and sheaths, miniature statues of animals and creatures, and a variety of leather and metal accessories.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Before long, we were in the heart of the bustling bazaar. Mongolians all around us were buying and selling, trying to score the best deals they could find. Sellers showcased how well their products worked, while buyers, hiding their interest, pointed out identifiable flaws in the product. After successful deals, everyone walked away happy. After failed deals, vendors returned to their smartphones, shaking their heads, perhaps getting in one last verbal jab, while the unsuccessful customers tried their luck with the neighboring vendor, who sold nearly identical products.&lt;/p&gt;

&lt;p&gt;Anuka expertly dipped and dodged through the densely packed stalls. To give a sense of scale, it took two minutes of walking just to get out of the shoe section. Exchanging words with the nearest vendors for directions, we finally arrived at the camping section. Kourtney and I bought two sleeping bags, and Kourtney bought a warm fleece. Encouraged by Anuka to haggle, we weakly negotiated, but in truth, since the prices were so cheap compared to our Western standards, we decided the few dollars we could have shaved off the price were better off in the vendors’ hands anyway.&lt;/p&gt;

&lt;p&gt;Next, we wanted to bring gifts for the reindeer herders and perhaps any nomadic families we stayed with. Relying on Anuka’s knowledge of the locals, we settled on pencil bags that we stuffed with ornate pencils, erasers, and pencil sharpeners for the kids. For the adults, we bought light bulbs powered by AC battery cables, flashlights powerable via a multitude of methods, battery-powered headlamps, miniature binoculars, and medium- and large-sized thick plastic containers for milking and berry picking. Against Anuka’s advice, we also purchased fifty .22 caliber bullets, which are particularly useful, if not illegal, in the Taiga for hunting.&lt;/p&gt;

&lt;p&gt;Afterward, we headed back to the car. On the way, an old drunkard tossed an empty vodka bottle in the direction of a woman sitting on a concrete block. As he shouted, the bottle slid and tumbled on the dirt, coming to a stop near her feet. “Do you want to drink with me? No one can drink more than me,” Anuka translated. The woman didn’t bat an eye, indicating her battle-hardened experience in dealing with drunks. Alcoholism is a major issue in Mongolia, particularly in the city. Anuka offered to us her opinion that the solution is a more stringent allocation of welfare. It made me want to get out of the city.&lt;/p&gt;

&lt;p&gt;We spent the rest of the day driving throughout the city, visiting the Buddhist monastery, getting some classic Mongolian fast food, and visiting the Chinggis Khaan Museum.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 70%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/1/baby_in_car.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/1/baby_in_car.webp&quot; alt=&quot;A Mongolian family beside us in traffic singing along to a traditional Mongolian ballad that blasted from their car speakers. Their baby, standing on the lap of someone in the back seat, hangs his head out of the rolled-down window, staring at Kourtney and me smiling back at him.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    A Mongolian family beside us in traffic singing along to a traditional Mongolian ballad that blasted from their car speakers. Their baby, standing on the lap of someone in the back seat, hangs his head out of the rolled-down window, staring at Kourtney and me smiling back at him.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Well-fed and sleepy, we were dropped off at the hotel by Anuka and Baagii, and we agreed to reconvene the next day at 10:00 am for the start of our big journey.&lt;/p&gt;

&lt;h2 id=&quot;day-2-journey-to-the-monastery&quot;&gt;Day 2: Journey to the monastery&lt;/h2&gt;

&lt;p&gt;Now might be a good time to give an overview of our planned &lt;strong&gt;itinerary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Roughly speaking, the plan was to drive deep into Northern Mongolia to the small village of Tsagaannuur. This is where Anuka lives. We would then treat the village as a basecamp, setting off on two distinct horseback journeys. The first to visit the reindeer herders in the Western Taiga, and the second to visit the reindeer herders in the Eastern Taiga. Then we would make our way back to Ulaanbaatar, stopping at some touristic sites along the way.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Details about selecting an itinerary&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;When planning the trip with Anuka she gave us the reins to create whatever sort of itinerary we wanted. But since she was the expert of her land, it felt more appropriate to let Anuka suggest some sample itineraries, and have us select the one that interested us most. Here is the one we selected:&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;Day 1: Welcoming day&lt;/li&gt;
    &lt;li&gt;Day 2: Travel to Amarbaysgalant Monastery&lt;/li&gt;
    &lt;li&gt;Day 3: Drive to Murun town&lt;/li&gt;
    &lt;li&gt;Day 4: Off-road drive to Tsagaannuur village&lt;/li&gt;
    &lt;li&gt;Day 5: Horse riding to West taiga&lt;/li&gt;
    &lt;li&gt;Day 6: Exploring day with the Western Reindeer Herders&lt;/li&gt;
    &lt;li&gt;Day 7: Back to the village&lt;/li&gt;
    &lt;li&gt;Day 8: Horse riding to East taiga&lt;/li&gt;
    &lt;li&gt;Day 9: Exploring day with the Eastern Reindeer Herders&lt;/li&gt;
    &lt;li&gt;Day 10: Back to the village&lt;/li&gt;
    &lt;li&gt;Day 11: Off-road drive to Khuvsgul lake&lt;/li&gt;
    &lt;li&gt;Day 12: Rest day - boat trip through the lake or hiking to the High mountain (horse riding) which you can see whole pure water lake&lt;/li&gt;
    &lt;li&gt;Day 13: Drive to Uran togoo&lt;/li&gt;
    &lt;li&gt;Day 14: Khustai national Park&lt;/li&gt;
  &lt;/ul&gt;

  &lt;p&gt;Add in a spattering of cursory Google searches, and you now know as much as we did going into this.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Our plan for the day was to log some distance in our multi-day goal of reaching Tsagaannuur, and then spending the night by a historical monastery.&lt;/p&gt;

&lt;p&gt;We drove through the morning traffic slowly but surely.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/ub_buildings.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/ub_buildings.webp&quot; alt=&quot;The rapid growth of the city was glaringly obvious when looking at the construction of high-rise apartments. For every occupied high rise, there was another under construction. And building contracts were clearly carried out in bulk, whereby as many as 12 identical high rises were erected concurrently, side by side.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The rapid growth of the city was glaringly obvious when looking at the construction of high-rise apartments. For every occupied high rise, there was another under construction. And building contracts were clearly carried out in bulk, whereby as many as 12 identical high rises were erected concurrently, side by side.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/scenery.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/scenery.webp&quot; alt=&quot;Just 15 minutes outside Ulaanbaatar and already Mongolia&apos;s natural balance seems restored.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Just 15 minutes outside Ulaanbaatar and already Mongolia&apos;s natural balance seems restored.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;We stopped at a roadside restaurant. The kitchen was within the ger, and outside was customer seating, underneath an awning protruding from the ger’s entrance. The furniture was patchwork, best explained by Baagii’s seat being an extricated car seat.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/ger_restaurant.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/ger_restaurant.webp&quot; alt=&quot;Anuka explains the menu to Kourtney at a roadside restaurant ger. The food is prepared behind the orange door in the background.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Anuka explains the menu to Kourtney at a roadside restaurant ger. The food is prepared behind the orange door in the background.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Kourtney got Хуушуур (Khuushuur), a stack of flat, deep-fried meat pastries, and I got Цуйван (Tsuivan), a very popular meat-heavy noodle stir fry. They were both delicious, if not lacking in vegetables.&lt;/p&gt;

&lt;p&gt;Alongside our meals, the waitress, a 14-year-old girl on summer break, brought us milk tea, a staple beverage in Mongolia. It’s black tea leaves and salt, brewed in a mix of water and most commonly cow’s milk. I didn’t realize it at the time, but milk tea is more than just a beverage to Mongolians. It’s embedded in their lifestyle, served to guests as a way of welcoming someone into their home, a way to keep warm and hydrated. It’s like their version of “breaking bread”.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/general/__2023-09-05_03-36-11-727000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/general/__2023-09-05_03-36-11-727000_cam.webp&quot; alt=&quot;Anuka brewing some milk tea in an ort. Whether at a fastfood restaurant in Ulaanbaatar, a roadside restaurant, a family&apos;s ger, or an ort, milk tea is always on the menu.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Anuka brewing some milk tea in an ort. Whether at a fastfood restaurant in Ulaanbaatar, a roadside restaurant, a family&apos;s ger, or an ort, milk tea is always on the menu.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;In the car, Anuka gave us our second Mongolian lesson. Yesterday, we learned sembano (hi) and bayarlala (thank you). Today, we learned tawny nerr himbe? (What’s your name?) and mini nerr (my name is). We practiced on Baagii, who somehow struck a balance between laughing at us while also providing genuine encouragement.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/dirt_road.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/dirt_road.webp&quot; alt=&quot;At a nondescript location somewhere along the highway, we slowed down, turned onto a dirt road, and left the asphalt behind us. Off-roading would become the new norm for our trip.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    At a nondescript location somewhere along the highway, we slowed down, turned onto a dirt road, and left the asphalt behind us. Off-roading would become the new norm for our trip.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After getting turned around once or twice in the labyrinth of dirt roads, the car got stuck during a creek crossing. After an all-hands-on-deck scenario in which Kourtney, Baagii, and I pushed in unison while Anuka manned the wheel, we finally got the car on the right side of the creek. Kourtney remarked that it was the most exciting part of the trip so far. Anuka assured her there was a lot more where that came from.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/stuck_car.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/stuck_car.webp&quot; alt=&quot;The lexus gets stuck in a creek just a few kilometers from the monastery.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/stuck_car_2.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/stuck_car_2.webp&quot; alt=&quot;The lexus gets stuck in a creek just a few kilometers from the monastery.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    The lexus gets stuck in a creek just a few kilometers from the monastery.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;We finally arrived at the monastery, but it was closed. While Anuka cooked a flavorful vegetable soup on a portable stovetop, Kourtney and I climbed up the hill that had a big golden Buddha statue that overlooked the Monastery.&lt;/p&gt;

&lt;p&gt;In the twilight, the moon shone brightly, lighting up the monastery and the surrounding pastures, accentuating the calmness of the valley.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/monastary.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/monastary.webp&quot; alt=&quot;Our evening hike to the viewpoint of the monastery.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/monastary_3.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/monastary_3.webp&quot; alt=&quot;Our evening hike to the viewpoint of the monastery.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Our evening hike to the viewpoint of the monastery.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After eating dinner, we retired to the tourist camp, where a ger was waiting for the four of us. In bed, Anuka explained a little bit about the simple, yet effective ger architecture.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/2/ger.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/2/ger.webp&quot; alt=&quot;The design of a ger. The ceiling is supported by a central wooden support with a circular window on top. The woodstove sits underneath this, and the chimney feeds through a segment of the window. From the centerpiece emanates 80 spokes. Layers of plastic and fabric lay on top of these spokes, forming the roof.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The design of a ger. The ceiling is supported by a central wooden support with a circular window on top. The woodstove sits underneath this, and the chimney feeds through a segment of the window. From the centerpiece emanates 80 spokes. Layers of plastic and fabric lay on top of these spokes, forming the roof.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Then we all quieted down and let the silence envelope us. I was enjoying this so much more than the busyness of Ulaanbaatar.&lt;/p&gt;

&lt;h2 id=&quot;day-3-journey-to-muurun&quot;&gt;Day 3: Journey to Muurun&lt;/h2&gt;

&lt;p&gt;In the morning we woke up and headed to the car, where Baagii and Anuka were already busy at work. Baagii was doing car maintenance and Anuka was preparing a scrambled egg breakfast with watermelon.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_20-24-14-191000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_20-24-14-191000_cam.webp&quot; alt=&quot;Allured by the smell of our cooking breakfast, a herd of curious cows invades our space around the car.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Allured by the smell of our cooking breakfast, a herd of curious cows invades our space around the car.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_20-25-35-433000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_20-25-35-433000_cam.webp&quot; alt=&quot;Three cows eye the breakfast on our fold out table.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Three cows eye the breakfast on our fold out table.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;When we finished eating, we headed to the monastery, seeing it for the first time in the morning light.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_20-39-05-884000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_20-39-05-884000_cam.webp&quot; alt=&quot;Exploring the Amarbayasgalant Monastery grounds. The monastery was built in 1727-1736 in honor of Undur Gegeen Zanabazar, the first Bogd, Buddhist leader of Mongolia.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_20-40-04-198000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_20-40-04-198000_cam.webp&quot; alt=&quot;Exploring the Amarbayasgalant Monastery grounds. The monastery was built in 1727-1736 in honor of Undur Gegeen Zanabazar, the first Bogd, Buddhist leader of Mongolia.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_20-41-04-711000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_20-41-04-711000_cam.webp&quot; alt=&quot;Exploring the Amarbayasgalant Monastery grounds. The monastery was built in 1727-1736 in honor of Undur Gegeen Zanabazar, the first Bogd, Buddhist leader of Mongolia.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_20-49-06-892000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_20-49-06-892000_cam.webp&quot; alt=&quot;Exploring the Amarbayasgalant Monastery grounds. The monastery was built in 1727-1736 in honor of Undur Gegeen Zanabazar, the first Bogd, Buddhist leader of Mongolia.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Exploring the Amarbayasgalant Monastery grounds. The monastery was built in 1727-1736 in honor of Undur Gegeen Zanabazar, the first Bogd, Buddhist leader of Mongolia.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Three monks, the oldest no more than 14, emerged from a ger residing within the monastery grounds. At the entrance of the central building they formed a small huddle, their heads almost touching. At first, I thought they were engaged in a prayer or ritual of some sort. But soon I realized they were huddled around a smartphone, watching a video that was making them laugh. The fun cut short by their responsibilities, at the turn of the hour, they opened up the monastery doors, and motioned for us to follow them in.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_21-18-15-904000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_21-18-15-904000_cam.webp&quot; alt=&quot;Three monk children huddle around a smartphone moments before their monasterial duties of opening the monastery.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Three monk children huddle around a smartphone moments before their monasterial duties of opening the monastery.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After turning on the lights and lighting candles and incense, the three apprentices sat in meditative poses around the center of the room and the eldest began speaking his morning recitations into a microphone. As he spoke I explored the beautifully ornate interior full of gongs, statues of gods, gorgeous drapery, and colorful symbology. His monotonous mantra echoed endlessly and unwaveringly throughout the chamber. This is not a performance because we are here, I kept reminding myself, this is their life.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_21-27-32-785000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_21-27-32-785000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_21-32-14-057000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_21-32-14-057000_evan.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-29_21-38-08-573000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-29_21-38-08-573000_evan.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Outside, Anuka explained that many of these children are either orphans or were sent here by their parents because the monastery serves as a refuge from poverty and provides educational opportunities they couldn’t afford otherwise.&lt;/p&gt;

&lt;p&gt;It was time to get back onto the road and continue our journey to Tsagaannuur. The plan was to spend the night in Muurun, the administrative capital of Northern Mongolia, north of which one can only find nomads and small villages. So onwards and upwards, into Northern Mongolia.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;I woke up to Baagii braking. A weather-seasoned nomadic man was herding his horses across the road with a dirt bike. Anuka and Baagii exchanged some dialogue and then Anuka explained we would go say hi to the nomad and his family.&lt;/p&gt;

&lt;p&gt;We parked the car on the side of the road, climbed up the dirt bank, and onto the prairie. The man and his wife were corralling a herd of at least 30 horses, some young, some old. The young ones were tied to a line pegged to the ground. This was to make sure the mothers never strayed too far.&lt;/p&gt;

&lt;p&gt;Baagii took the lead and started talking with them. Kourtney and I kept a tentative distance. Weren’t we intruding? But the conversation was going well, and it became obvious they knew each other, so we got closer and started snapping pics.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_04-59-53-509000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_04-59-53-509000_evan.webp&quot; alt=&quot;(left) Baagii makes himself at home while talking to the couple, playing absent-mindedly with one of their horse leads while they milk two horses within earshot. (right) Baagii and the lady by the stable.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_04-39-23-825000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_04-39-23-825000_cam.webp&quot; alt=&quot;(left) Baagii makes himself at home while talking to the couple, playing absent-mindedly with one of their horse leads while they milk two horses within earshot. (right) Baagii and the lady by the stable.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) Baagii makes himself at home while talking to the couple, playing absent-mindedly with one of their horse leads while they milk two horses within earshot. (right) Baagii and the lady by the stable.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Baagii and Anuka spoke with the nomads while they milked all the horses. The procedure was as follows: they released the foal from the rope line, it started to suck on the mare’s teat, at which point the foal was removed, the woman came in with her bucket, and milked. Meanwhile, the man steadied the mother and, if it was a feisty one, he held its leg bent, so it didn’t kick the bucket or his wife.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_04-42-12-155000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_04-42-12-155000_cam.webp&quot; alt=&quot;Holding the foal by its mane, the man allows it to feed in order to promote the mother&apos;s milk production. A solitary basketball hoop lies in the distance.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Holding the foal by its mane, the man allows it to feed in order to promote the mother&apos;s milk production. A solitary basketball hoop lies in the distance.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_04-56-36-669000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_04-56-36-669000_cam.webp&quot; alt=&quot;The woman milks the mare, filling the bucket while the man steadies it while keeping the foal nearby.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The woman milks the mare, filling the bucket while the man steadies it while keeping the foal nearby.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_04-57-05-249000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_04-57-05-249000_cam.webp&quot; alt=&quot;(left) Kourtney milking the mare. (right) Hitching a ride from Handa&apos;s husband.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/IMG_1868.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/IMG_1868.webp&quot; alt=&quot;(left) Kourtney milking the mare. (right) Hitching a ride from Handa&apos;s husband.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) Kourtney milking the mare. (right) Hitching a ride from Handa&apos;s husband.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Every two hours they milked the horses, Anuka told us. It was unendingly exhausting work, and you could tell that to them it was clockwork. They processed the dairy in multiple ways, ate what they could, and sold the rest at market, Anuka continued. In the traditional nomadic lifestyle, summertime was a season of preparation. To survive the upcoming winter, that meant no meat was to be eaten. With little going on agriculturally due to the intense dry climate of Mongolia, that meant a primarily dairy-based diet.&lt;/p&gt;

&lt;p&gt;“&lt;em&gt;Таны нэр хэн бэ?&lt;/em&gt;” I asked casually, with my barely noticeable accent. Just kidding. With my heart racing, after practicing three times in my head, I managed to squeek out the phrase: “&lt;em&gt;TaWny neRr HimbE?&lt;/em&gt;”. She stared at me blankly, probably deciphering what the fuck I just said, then, after figuring it out, snapped back to reality and responded (in Mongolian), “[My name is] Handa”. Handa was short, stout, and had deep red cheeks. Anuka explained to me that her dark rosy cheeks were a product of being outside so many hours a day. When winter came, her skin would turn even darker due to a dramatic change. Not only would the intensely cold and dry climate suck the blood out of her face, but apparently her diet would shift to primarily meat. Anuka looked me straight in the eyes and told me with a very serious tone, “You cannot imagine how much meat we eat in winter.” Apparently, this dramatically affected their body.&lt;/p&gt;

&lt;p&gt;After some language-transcending humor, including Kourtney being asked to milk the horses and me catching a ride on the back of the man’s dirt bike, we were invited to their home, a white ger around 100m down the hill. As we sauntered down, I inquired about how Anuka knew these nomads.&lt;/p&gt;

&lt;p&gt;“I’ve never met them, and neither has Baagii.”&lt;/p&gt;

&lt;p&gt;“Anyone is welcome to stay with anyone 24 hours today and they are treated like family”, Anuka explained after seeing our surprise. In our culture, you don’t just walk onto someone’s land and expect the red carpet to be rolled out. But in Mongolia, no one owns the land. Perhaps this can in part explain a fundamental cultural difference. Since there is a shared conception of the land, this may have a profound influence on the interrelationships between Mongolian people and how they congregate. Kourtney also pointed out that because life is so difficult here, people need to band together in a sort of “we’re all in this together” mentality. In other words, people rely on their literal and proverbial neighbors. Fostered over millennia, this generosity and hospitality has penetrated deep into nomadic culture and persists today as an overwhelming welcoming attitude to strangers. This is just a theory.&lt;/p&gt;

&lt;p&gt;Before entering the ger, I noticed some fermenting cow milk being pressed down by two large rocks. Anuka explained that they were making a sort of hard, thin cheese. In fact, some was already fully processed and baking in the sun. Anuka and I grabbed a chunk to share. It was very dry, sour, and salty. It was an intriguing taste, far outside my realm of familiarity. I didn’t ask for seconds.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_05-05-11-876000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_05-05-11-876000_evan.webp&quot; alt=&quot;(left) Horse milk in the process of being turned into cheese. The milk is being pressed in sacs of cheese cloth while baking in the sun. (right) The finished product which tastes very chalky, sour, and salty.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_05-06-51-327000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_05-06-51-327000_evan.webp&quot; alt=&quot;(left) Horse milk in the process of being turned into cheese. The milk is being pressed in sacs of cheese cloth while baking in the sun. (right) The finished product which tastes very chalky, sour, and salty.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) Horse milk in the process of being turned into cheese. The milk is being pressed in sacs of cheese cloth while baking in the sun. (right) The finished product which tastes very chalky, sour, and salty.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Inside, we sat on some stools. To the right of the entrance was a bed that Handa was sitting on. At the far end of the entrance, the man, whose name I asked but could not pronounce, was sitting near a small table. They laid out some bread, surcream (a thick, creamy, and slightly tangy butter), and candies.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_05-12-06-667000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_05-12-06-667000_evan.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Near the man was a highly decorated cabinet with a telephone, some seemingly Buddhist items, and a mirror. My eyes were drawn to the left side of the ger, where a very large sack hung from a strong beam of wood. It was made of an old, large animal hide that had been stitched together in two pieces. It looked like it had a capacity of at least 100L. Handa walked over and poured her newest batch of mare milk into the sack. From the sound of the pour, I could tell the sack was nearly full, presumably with more mare’s milk.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_05-08-05-705000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_05-08-05-705000_evan.webp&quot; alt=&quot;Inside Handa&apos;s ger, a large cow-skin hide has been sewn into a vessel for fermenting horse milk into kumis. It hangs from a sturdy wooden frame that leans against the ger&apos;s interior wall. Hundreds of liters of milk stretch the leather to its limit, exerting tremendous force on the two small wooden toggles that solely hold back a catastrophic milk tsunami.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Inside Handa&apos;s ger, a large cow-skin hide has been sewn into a vessel for fermenting horse milk into kumis. It hangs from a sturdy wooden frame that leans against the ger&apos;s interior wall. Hundreds of liters of milk stretch the leather to its limit, exerting tremendous force on the two small wooden toggles that solely hold back a catastrophic milk tsunami.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After giving the contents a quick stir, Handa filled a plastic jug by dunking it into the sack, and then placed the jug on the table. Wasting no time, the man procured a decently sized wooden bowl that he filled to the brim, and then took a long pull. After wiping the milk from his mustache, he replaced what he had drunk by pouring more into the bowl, and then passed it to me. &lt;strong&gt;To me&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;Remembering what Anuka had told me previously about receiving with my right hand, I tentatively took the bowl and looked inside. The sides of the bowl had small chunks of rancid dairy, and on top of the liquid was a thin film that I had watched the man gently blow to one side of the bowl before sipping (gulping).&lt;/p&gt;

&lt;p&gt;Everyone hushed to watch what would happen next, with curiosity and anticipation. How would the foreigner react to the taste? Emulating the man as closely as possible, I blew the film off to the side and touched the bowl to my lips and started drinking. Sour and bubbly, it tasted nothing like I expected. Sort of like kefir. I think I liked it! I had several gulps and passed back the bowl, and the ritual continued. The man filled the bowl, looked at someone, and that special someone would receive the bowl and sip from it, then pass it back.&lt;/p&gt;

&lt;p&gt;I asked Anuka what I was drinking. “This is Mongolia’s national drink, kumis,” she explained. When offered kumis, it is a sign of respect that you must not refuse, and the more you can drink in one sip, the better. But never drink the whole bowl, she continued. Perhaps it’s just a superstition, but supposedly this is a guarantee for a bad milking yield the next day.&lt;/p&gt;

&lt;p&gt;Handa and her husband were very interested in our situation. Anuka translated everything as the questions rolled in: What is your itinerary? Why did you come to Mongolia? Do you think it’s weird we drink from one bowl? I explained that we were in Mongolia to learn about cultures that are different from ours, so we can be more mindful about which cultural practices we engage in within our own culture. It was a complex concept to translate, but I could tell that whatever Anuka said resonated with Handa, as she paused her normally talkative and smile-filled demeanor to listen and nod.&lt;/p&gt;

&lt;p&gt;I told them they were very friendly and explained that from our countries, this level of hospitality would require years of knowing each other. How bizarre must that seem to them?&lt;/p&gt;

&lt;p&gt;Handa’s youngest son came in and shyly sat down beside her. He loved Michael Jordan and had a Michael Jordan jersey. After learning Kourtney was from Chicago, Handa said he wanted to write a letter that we would give to Michael Jordan. All the adults laughed at the impossibility.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/3/__2023-08-30_05-36-03-557000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/3/__2023-08-30_05-36-03-557000_evan.webp&quot; alt=&quot;Depending on the strength and stage of fermentation, kumis can be as alcoholic as a light beer.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Depending on the strength and stage of fermentation, kumis can be as alcoholic as a light beer.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;We needed to get back onto the road, so we said our goodbyes. Handa gifted us a big bottle of kumis, not expecting or asking for anything in return. But before leaving, we gifted them 20000 MNT as a thank you for the kumis and their hospitality. At first, she declined, so I handed it to her son. They stood outside their ger and waved to us as we drove back onto the road.&lt;/p&gt;

&lt;p&gt;We drove for another couple of hours until we made it to just outside Murun, where Anuka had arranged for us to spend the night in a tourist camp. As we approached our ger, a very loud and drunk man stumbled out of the neighboring ger and Anuka immediately turned around. “You will have a very bad night. You will not get any sleep,” she said. Back at reception, she threw some assertive words towards the employee, and before we knew what was happening, Kourtney and I were spending the night in a small, one-room cabin in a more secluded section of the camp. One of many instances where Anuka had our back.&lt;/p&gt;

&lt;p&gt;After a game of killing all the flies, we settled into the calm of night, far away from the ruckus of our neighbors (and flies).&lt;/p&gt;

&lt;h2 id=&quot;day-4-to-tsagaannuur&quot;&gt;Day 4: To Tsagaannuur&lt;/h2&gt;

&lt;p&gt;Today would be our biggest driving day, with our plan being to make it all the way to Tsagaannuur. This would not be as simple as following a road–the main issue being the lack thereof.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 50%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-30_20-36-33-680000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-30_20-36-33-680000_evan.webp&quot; alt=&quot;Before the beginning of the end of the road, we made a quick pitstop into Muurun so Baagii could get the car checked by a mechanic.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Before the beginning of the end of the road, we made a quick pitstop into Muurun so Baagii could get the car checked by a mechanic.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;We drove on the only road out of Muurun for about 20 minutes. At a nondescript portion of the road, Baagii veered left onto a dirt road. And that would be the last asphalt we’d see for hundreds of miles.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_02-17-00-337000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_02-17-00-337000_evan.webp&quot; alt=&quot;&apos;Dirt road&apos; would be a generous term to describe what we were driving on. It was essentially a conglomerate of tire tracks through terrain suitable for a car ad. There were river crossings, tire-puncturing rocks littered like landmines, worn out ditches and trenches filled with mud. The tire tracks split and merged over the uneven terrain like capillaries, their patterns serving as remnants of drivers&apos; decisions past.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    &apos;Dirt road&apos; would be a generous term to describe what we were driving on. It was essentially a conglomerate of tire tracks through terrain suitable for a car ad. There were river crossings, tire-puncturing rocks littered like landmines, worn out ditches and trenches filled with mud. The tire tracks split and merged over the uneven terrain like capillaries, their patterns serving as remnants of drivers&apos; decisions past.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Baagii navigated the obstacles expertly. In the rare instance that an oncoming car was sighted, the windows would invariably roll down to discuss conditions: Which ways are muddy? How high is the water level of the upcoming river? Is the bridge passable?&lt;/p&gt;

&lt;p&gt;We started up a mountain hill and after some discussion in Mongolian between Anuka and Baagii, we turned around, deciding that the path would be too muddy given the recent rainfall. As a Tsagaannuur local, Anuka has traveled to and from Murun many times in her life. “There are many paths to Tsagaannuur”, she said before adding with a smile, “And you can of course always make your own path”.&lt;/p&gt;

&lt;p&gt;Cut to 20 minutes later and I was clutching the overhead handlebar with unexplainable strength to avoid bouncing out of my seat as the car barged forward. The GPS sat unused while Anuka directed Baagii, pointing to distant landmarks and presumably saying things like “turn left at the three-peaked mountain,” or “follow this dry riverbed until the standing stone.” The more remotely we traveled, the more ancient knowledge proved to be infinitely more valuable than the GPS.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_01-15-06-762000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_01-15-06-762000_evan.webp&quot; alt=&quot;The relentless pursuit of perfection. Lexus.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The relentless pursuit of perfection. Lexus.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After around an hour on the road, the landscape was beginning to change. Away from Murun and its sought after asphalt, the true expansiveness of Mongolia was beginning to reveal itself. Ancient mountains covered in nothing but short grass and rocks. And interestingly, no matter how far away we drove from “civilization”, we saw gers scattered everywhere.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/general/__2023-08-30_02-22-00-460000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/general/__2023-08-30_02-22-00-460000_cam.webp&quot; alt=&quot;A typical landscape in northern Mongolia. Scattered and free-roaming livestock graze the green rolling hills. The white canopies of gers are sprinkled all throughout each valley. Evergreen forests blanket the distant hillsides.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    A typical landscape in northern Mongolia. Scattered and free-roaming livestock graze the green rolling hills. The white canopies of gers are sprinkled all throughout each valley. Evergreen forests blanket the distant hillsides.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;The “road” was taking a toll on the Lexus, leading Baagii to make frequent stops to cool the engine. (It goes without saying that cars don’t last long here). During one of these stops, we were approached by a man walking alongside his horse. Baagii started talking with him, and we did our ritual of asking what his name was (pretty much the only thing we know how to say), and then unsuccessfully repeating their name back to them. Before long, the gentleman’s friend pulls up on his motorcyle, in masterful style. They wanted to drink with us.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-30_23-00-48-503000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-30_23-00-48-503000_evan.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-30_23-01-35-128000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-30_23-01-35-128000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-30_23-03-58-622000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-30_23-03-58-622000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-30_23-04-33-111000_cam_cropped.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-30_23-04-33-111000_cam_cropped.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Twenty minutes later we stopped to eat lunch while Baagii fixed the car suspension yet again. Anuka prepared a pasta with fresh veggies and rehydrated beef. Rehydrated meat is amazing. It’s light, doesn’t expire, compact, easy to rehydrate, and delicious. When Mongolians kill an animal, usually in November, they often dedicate two legs just for dried meat. By cutting it into thin strips and hanging it for winter, it’s ready to eat the following April.&lt;/p&gt;

&lt;p&gt;During lunch, Kourtney asked if Anuka and Baagii ever disagree about which way to go. Anuka said they frequently bicker about which way is best, and when the car ends up stuck in the mud there is always hell to pay for the person who was wrong. “Fighting is in every culture”, I said, which Baagii found especially funny. The friendship was growing.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-30_21-39-54-024000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-30_21-39-54-024000_cam.webp&quot; alt=&quot;Rolling hills cloaked in evergreens cradle a few grazing horses.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Rolling hills cloaked in evergreens cradle a few grazing horses.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-30_22-05-26-727000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-30_22-05-26-727000_cam.webp&quot; alt=&quot;Unmonitored livestock grazing as free animals. Four different nomadic families in the background.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Unmonitored livestock grazing as free animals. Four different nomadic families in the background.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_00-58-56-554000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_00-58-56-554000_evan.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_02-23-01-723000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_02-23-01-723000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_02-45-19-769000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_02-45-19-769000_cam.webp&quot; alt=&quot;Hundreds of sheep and goat litter a hillside, free to roam and graze.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Hundreds of sheep and goat litter a hillside, free to roam and graze.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_03-32-05-362000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_03-32-05-362000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Later in the day we found ourselves enjoying the scenery as we drove alongside a gorgeous mountain. We crossed a muddy river, and then ascended up a rolling mountain. We arrived at a fork in the proverbial road. The car stopped. There were two potential ridgeline crossings. To be honest, both looked exposed and steep. I looked at the left pass and imagined the Lexus barrel-rolling down the side of the slope. After some hemming and hawing, Baagii opted for the rightmost mountain pass, which my imagination was thankful for. We let the engine cool and drank some kumis. It was a hold your breath moment with slopes to our left and right. When we successfully mounted and traversed the ridgeline, everyone was relieved and smiling.&lt;/p&gt;

&lt;p&gt;After the mountain pass, we descended into a valley, where a small village lay. It’s a common place to stop for food for passersby traveling to and from the remote north. Day or night, the restaurant will open for a bus full of travelers. An old man sat in a security booth manning a rudimentary car gate that used a defunct chainsaw as a counterweight. A few words were spoken and the conversation ended with a &lt;em&gt;za&lt;/em&gt; from both parties. He released the drawstring and the chainsaw pulled the gatekeeping log upwards, allowing our passage.&lt;/p&gt;

&lt;p&gt;We were in the middle of Autumn and the more north we drove, the more the grass and trees were showing a spectrum of Fall colors. “It will be even better when we return”, Anuka said.&lt;/p&gt;

&lt;p&gt;We were slowed by a herd of about 40 horses who were blocking the road, their owners nowhere in sight. I can imagine no better life for captive animals than in Mongolia, where they live free and nearly wild. Seeing all the horses inspired us to stop for a quick kumis break. As we sipped, Anuka, as she often did, spoiled us with a little tidbit of information. Apparently, Chinggis Khaan proclaimed that a proper Mongolian should drink more horse milk than water.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_01-34-37-443000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_01-34-37-443000_cam.webp&quot; alt=&quot;A curious foal explores, but not too far from its mother&apos;s side.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    A curious foal explores, but not too far from its mother&apos;s side.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_01-35-05-975000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_01-35-05-975000_cam.webp&quot; alt=&quot;Horses are branded to establish ownership. More on that later...&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Horses are branded to establish ownership. More on that later...
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;At the top of a mountain was a collection of 13 &lt;em&gt;ovoos&lt;/em&gt;. An ovoo is a shrine composed of branches, rocks, and multicolored silk fabrics, each color representing a natural element (sun, sky, water, etc.). They are typically found at the top of mountains and mark border crossings, and serve as checkpoints either in the beginning or the end of a journey. I couldn’t make heads or tails as to whether this was the beginning or the end of our journey.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_03-06-04-381000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_03-06-04-381000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Following Anuka’s example, we grabbed three rocks and made our way around the central ovoo, walking three laps in a clockwise motion. While you loop around the sacred space, it’s customary to throw the rocks onto it, adding to its stature. After that, you are to send a wish out into the universe. Since this ovoo site has so many ovoos, we move to the ones linked with our respective Chinese zodiac sign and repeated the process.&lt;/p&gt;

&lt;p&gt;After making a wish, I watched Baagii pour out 3 cups of kumis, flinging each skyward. Finishing his ritual, he looked my way and noticed I had been watching him. He smiled. Back in the car, he spoke with Anuka and she translated to us that he was sharing his kumis with nature. It gave me pause. Baagii’s spiritual connection to the land is palpable. He believes in these practices. This is how he lives.&lt;/p&gt;

&lt;p&gt;The full journey from Murun to Tsagaannuur is around 9 hours. Understandably, improved road conditions drive much of the election cycle. Candidates visit all the villages by helicopter, and then sing about promises of road infrastructure. Famous people are ushered in and perform concerts, and free vodka is handed out to restore voter faith from the last election. But post election, the promises of roads fade like the distant hum of their departing helicopters.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_03-41-41-651000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_03-41-41-651000_cam.webp&quot; alt=&quot;(left) An average road. (right) A rickety bridge crossing just outside Red Mountain Village, a small village that neighbors Tsagaannuur.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_04-22-23-197000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_04-22-23-197000_cam.webp&quot; alt=&quot;(left) An average road. (right) A rickety bridge crossing just outside Red Mountain Village, a small village that neighbors Tsagaannuur.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) An average road. (right) A rickety bridge crossing just outside Red Mountain Village, a small village that neighbors Tsagaannuur.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Perhaps it is for this reason that motorbikes reign supreme in Mongolia. They are cheap on gas, cheap to purchase, easy to repair, and can traverse roads and bridges that cars struggle with. With the introduction of the Chinese motorcycle in the 1990s, and its widespread proliferation throughout the country in the following decades, horseback riding has greatly diminished. Increasingly so, Mongolians are traveling, transporting goods, carrying out errands, and herding by motorcycle rather than by horseback. It’s an example like many others found in Mongolia: modern technologies threatening to overwrite a lifestyle maintained for thousands of years, and the Mongolian people and government trying to strike a balance between modernity and tradition.&lt;/p&gt;

&lt;p&gt;Past Red Mountain Village we saw some activity outside a cluster of three gers. The men and women were saddling up horses, and someone was drinking. “They are going to race horses”, Anuka determined. “But we can’t go. If we do, we can’t leave. Evan not be able to walk, and Evan no translator”. After a moment, I understood her riddle: if we visit them, we’d be culturally obligated to drink the night away and we wouldn’t make it to Tsagaannuur. We drove on.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_07-05-35-010000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_07-05-35-010000_cam.webp&quot; alt=&quot;A tranquil mood envelopes us as Baagii drives towards the darkening horizon. The car squeaks with each passing bump as we&apos;re thrown from side to side. In the twilight, an adolescent herds her family&apos;s sheep on motorcycle towards an out of sight ger.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    A tranquil mood envelopes us as Baagii drives towards the darkening horizon. The car squeaks with each passing bump as we&apos;re thrown from side to side. In the twilight, an adolescent herds her family&apos;s sheep on motorcycle towards an out of sight ger.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Thirty minutes later, after a journey that seemingly never ended, we made it to &lt;em&gt;White Lake&lt;/em&gt;. This is the lake upon which Tsagaannuur village was formed. Anuka explained that no one swims, bathes, or washes their clothes in the lake out of respect for its purity. This respectful relationship with the lake allows Tsagaannuurians to drink its water and maintain a healthy ecosystem that teems with a famous “white fish”, known throughout Mongolia for being tasty. Though Anuka noted that high winds are often blowing roadside trash into the lake.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/4/__2023-08-31_07-38-21-330000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/4/__2023-08-31_07-38-21-330000_cam.webp&quot; alt=&quot;The entrance to Tsagaannuur. Anuka&apos;s father is the artist who created the metal reindeer statues.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The entrance to Tsagaannuur. Anuka&apos;s father is the artist who created the metal reindeer statues.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;In the moonlight, we arrived at Anuka’s childhood home. We were greeted by her parents and Anuka’s precious little girl. Just 18 months old, she squealed with energy at the reunion. After some brief introductions, Kourtney and I retired into an offshoot of their home where Anuka’s mother used to operate a foreign guesthouse. It was during Anuka’s childhood where she interacted with foreign guests that her ideas of becoming a Mongolian tour guide started forming. Without taking our surroundings in with any great detail, Kourtney and I clambered into our beds and fell asleep nearly immediately. What a day.&lt;/p&gt;

&lt;h2 id=&quot;day-5-horseback-to-the-western-reindeer-herders&quot;&gt;Day 5: Horseback to the Western Reindeer Herders&lt;/h2&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-08-31_22-28-06-030000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-08-31_22-28-06-030000_evan.webp&quot; alt=&quot;The shower of Anuka&apos;s childhood home. Since there is no running water in the village, showering is quite the event. An open water basin sits on the roof that can be filled with water from the lake (hence the ladder). This basin feeds into an electric water heater that&apos;s bolted to the ceiling of the shower room, that can be powered by running an extension cord from the house. A simple spigot controls the water flow. This complexity, coupled with bone-chilling temperatures in winter, means that showering is a weekly, not a daily occurrence.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The shower of Anuka&apos;s childhood home. Since there is no running water in the village, showering is quite the event. An open water basin sits on the roof that can be filled with water from the lake (hence the ladder). This basin feeds into an electric water heater that&apos;s bolted to the ceiling of the shower room, that can be powered by running an extension cord from the house. A simple spigot controls the water flow. This complexity, coupled with bone-chilling temperatures in winter, means that showering is a weekly, not a daily occurrence.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;It was September 1st, the first day of school in the village. After breakfast, we headed to the school to check out the special occasion. As we approached, we heard speeches and singing performances emanating from the center of the village through shoddy speakers. Within the school grounds, parents and children had gathered around the makeshift stage: a square of concrete embedded in the otherwise grassy school grounds. Here, songs and ceremonies were taking place. A teacher accepted a Teacher of the Year Award, a 6-year-old child sang a song about September 1st, the village leader asked for donations. Children and parents swayed to the rhythm of the music during this joyous day.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 60%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-08-31_23-04-22-417000_kourtney.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-08-31_23-04-22-417000_kourtney.webp&quot; alt=&quot;A schoolgirl sings a welcome back to school tune. A banner celebrating the school&apos;s Tsaatan pupils hangs in the background.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    A schoolgirl sings a welcome back to school tune. A banner celebrating the school&apos;s Tsaatan pupils hangs in the background.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;The pupils ranged from 1st to 12th grade. Independent of age, the boys wore ties and white shirts tucked into dress pants, and the girls wore pleated skirts and cardigans. About half of the students lived within the village, whereas the other half lived in nomadic families. Throughout Mongolia, villages with schools host students coming from the nomadic families who live in the surrounding mountains, plains, and valleys. However, unlike other villages, Tsagaannuur also hosts children of reindeer herding families in the Taiga.&lt;/p&gt;

&lt;p&gt;Whether their families are classic nomads or reindeer herding nomads, the nomadic children are boarded in a partially government funded dormitory that resides on the school grounds. It is the only brick house I saw in the village. Beside it is the old dormitory, a single story wooden building. This is the dormitory many of Anuka’s classmates lived in when she was a child. The living conditions have improved greatly between old and new. There are dormitory teachers who take care of the children, a cook, who feeds them 3 times a day, and a custodian who keeps the fire stove burning from October through May.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-08-31_23-13-51-590000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-08-31_23-13-51-590000_evan.webp&quot; alt=&quot;(left) The new government-funded dormitory on the school grounds. It&apos;s the only two-story brick house in Tsagaannuur that I saw. (right) The old and new school toilets. When Anuka was in school, the toilets were just a pit with planks stretching across to stand on. Now they have seats to sit on.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-08-31_22-55-17-462000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-08-31_22-55-17-462000_evan.webp&quot; alt=&quot;(left) The new government-funded dormitory on the school grounds. It&apos;s the only two-story brick house in Tsagaannuur that I saw. (right) The old and new school toilets. When Anuka was in school, the toilets were just a pit with planks stretching across to stand on. Now they have seats to sit on.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) The new government-funded dormitory on the school grounds. It&apos;s the only two-story brick house in Tsagaannuur that I saw. (right) The old and new school toilets. When Anuka was in school, the toilets were just a pit with planks stretching across to stand on. Now they have seats to sit on.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;It was obvious that education is extremely important to the villagers, the nomads, and the reindeer herders.&lt;/p&gt;

&lt;p&gt;“Are the nomads or herders sacrificing their culture by sending their children to school?” I asked Anuka.&lt;/p&gt;

&lt;p&gt;“No. They stay with their family for the first 6 years of life. After this, they already know the lifestyle”. Her response illustrated to me how capable children can become when thrust into such a hard-pressing lifestyle. They are capably riding horses by 4-5 years old, and are already taking over familial responsibilities such as goat and sheep herding. Meanwhile in North America tire swings have been removed from playgrounds.&lt;/p&gt;

&lt;p&gt;After around 40 minutes, during which time Anuka caught up with all the village folk, we stripped ourselves away from the festivities of September 1st. A big day lay ahead of us: we were heading into the Taiga to meet some of the western reindeer herders.&lt;/p&gt;

&lt;p&gt;20km outside the town we met our horse guide, Lhagwa. He’s a nomad with hundreds of livestock, including many horses–some of which he uses for horse trekking services. After waiting a bit at the meetup spot, Lhagwa showed up on one of his horses and ushered us to meet him at an alternative location. At the alternate spot, his wife and a handful of horses were hanging out.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-10-23-900000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-10-23-900000_evan.webp&quot; alt=&quot;(left) Lhagwa smiling through the antlers of a massive bull reindeer (Day 9). (right) Lhagwa&apos;s wife sitting down with the soon-to-be baggage horse, sucking on a candy.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_01-16-41-181000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_01-16-41-181000_evan.webp&quot; alt=&quot;(left) Lhagwa smiling through the antlers of a massive bull reindeer (Day 9). (right) Lhagwa&apos;s wife sitting down with the soon-to-be baggage horse, sucking on a candy.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) Lhagwa smiling through the antlers of a massive bull reindeer (Day 9). (right) Lhagwa&apos;s wife sitting down with the soon-to-be baggage horse, sucking on a candy.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;As we geared up for our journey into the Taiga, there was a natural rhythm to the preparations. Anuka boiled drinking water, Lhagwa and his wife saddled the horses, and Baagii packed the saddlebags. Kourtney and I helped where we could but felt relatively useless. Soon, some of us sat down in the grass and enjoyed a snack.&lt;/p&gt;

&lt;p&gt;Baagii helped me into his deel, showing me how the buttons and silk sash work.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 80%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_01-07-05-725000_kourtney.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_01-07-05-725000_kourtney.webp&quot; alt=&quot;Kourtney and I in Anuka and Baagi&apos;s deels that they lent us for the trip. Deels are a time-proven garb that&apos;s ideal for nomadic life. They shield against the sun, they&apos;re insulative when it&apos;s cold, and they protect your body from branches when on horseback through forested regions. Moreover, a natural pocket is created between the two flaps and above the sash, which can store a surprising amount of stuff.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Kourtney and I in Anuka and Baagi&apos;s deels that they lent us for the trip. Deels are a time-proven garb that&apos;s ideal for nomadic life. They shield against the sun, they&apos;re insulative when it&apos;s cold, and they protect your body from branches when on horseback through forested regions. Moreover, a natural pocket is created between the two flaps and above the sash, which can store a surprising amount of stuff.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_01-30-54-726000_kourtney.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_01-30-54-726000_kourtney.webp&quot; alt=&quot;Baagii (left), Anuka (center), and Lhagwa (right) load up the luggage horse, who is responsible for carrying all of our luggage and equipment. Strong and intelligent horses are chosen for this job because the packed gear rests on either side, and the horse must have the physical awareness to account for this when choosing which path to take through the forest. And if a path is too skinny the horse must be smart enough to back up and find another path instead of freaking out.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Baagii (left), Anuka (center), and Lhagwa (right) load up the luggage horse, who is responsible for carrying all of our luggage and equipment. Strong and intelligent horses are chosen for this job because the packed gear rests on either side, and the horse must have the physical awareness to account for this when choosing which path to take through the forest. And if a path is too skinny the horse must be smart enough to back up and find another path instead of freaking out.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;When everything was ready, Anuka, Kourtney, Lhagwa, and I set off on horseback heading due North. The herders live northwest of Tsagaannuur and to get there, we needed to ascend up a mountain by horseback. After cresting the mountain, it descends into a flat, marshy bog. From there we would cross the bog, and then descend further into the valley where the reindeer herders were currently settled at their Autumn camp location.&lt;/p&gt;

&lt;p&gt;We ascended up the gentle mountain slope and stopped for lunch while overlooking a great view of an expansive bog that lay ahead of us. From far away, the bog looked like a cake walk, but as we would soon learn, it was anything but gentle.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_02-58-29-903000_kourtney.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_02-58-29-903000_kourtney.webp&quot; alt=&quot;Lhagwa ties up our horses as we break for lunch. The foreboding bog beckons in the distance.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Lhagwa ties up our horses as we break for lunch. The foreboding bog beckons in the distance.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After lunch we saddled up. Being exceptionally tall and of generally low fortitude for Mongolian standards, I was experiencing some knee pain from the stirrups being too short, so Lhagwa lengthened them to the last notch in the leather. Still too short, but it was better. We descended down to the bog and it became clear how treacherous it really was. Extending for kilometers, the bog was a patchwork of soft moss, mud pools, and oily bog water.&lt;/p&gt;

&lt;p&gt;As my horse heroically navigated waist deep bog water, I came to realize that we aren’t just riding horses for the hell of it. This ain’t no trail ride. This was in fact, the only way to get to the Taiga. Traversing by foot would be impossible, though technically possible in Winter when land freezes over. Cars are an obvious no, and motorcycles are no better. That leaves horses and reindeer. It was no easy task, even for the horses, who regularly “broke through” the surface layers, ending chest deep in mud.&lt;/p&gt;

&lt;p&gt;Throughout this arduous journey for the horses, us humans were enjoying a relatively peaceful journey. The only sounds were the squelching of our horse’s legs navigating the thick bog. After an hour or two of marching, the landscape started to change. The terrain steepened, the ground firmed, and the treeline began to thicken.&lt;/p&gt;

&lt;p&gt;We descended further and further into a large river valley. After a quick traversal along the basin of the valley, far in the distance we saw a plume of smoke. As we got closer, we could see a clump of orts. After a four hour trek, we had arrived.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_05-10-40-837000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_05-10-40-837000_evan.webp&quot; alt=&quot;Triangulated between the orts lies more than 30 reindeer. Some young, some old, some white, some brown. The babies are tied to logs, and their concerned parents are close by.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_22-36-54-268000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_22-36-54-268000_cam.webp&quot; alt=&quot;Triangulated between the orts lies more than 30 reindeer. Some young, some old, some white, some brown. The babies are tied to logs, and their concerned parents are close by.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Triangulated between the orts lies more than 30 reindeer. Some young, some old, some white, some brown. The babies are tied to logs, and their concerned parents are close by.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;As we came into view, the herders’ dogs spotted us and started barking, running over to check out the new visitors.&lt;/p&gt;

&lt;p&gt;We dismounted our horses, and Anuka ushered us into one of the tents while Lhagwa tended to the horses.&lt;/p&gt;

&lt;p&gt;Inside we met Sartlong, one of the herders. She kindly gave us some milk tea to drink and was otherwise very shy. After warming up by the stove, Anuka and Sartlong’s husband, Godla, peeled back the animal skin door and entered.&lt;/p&gt;

&lt;p&gt;This camp contained three families and three orts. First there was Godla and his wife, Sartlong. Strangely, they met because of a film that was made about a reindeer herder who could catch wild reindeer. They wanted it to be authentic, and Godla could genuinely catch wild reindeer, so he was cast into the role. Sartlong was suggested as the love interest role. Fast forward, they are happily married with a kid, who just left for school. She was staying in the dormitories in Tsagaannuur and would have been at the festivities we had witnessed that morning.&lt;/p&gt;

&lt;p&gt;Then there is Sartlong’s brother and his family, who lived in the tent next door. Tragically, their son passed away last month, just a year old. The baby was running a high fever, so they rushed him by reindeerback to the village. Unfortunately, there was no doctor there, nor was there a nurse. So the baby died, and apparently the police were investigating. During our visit they were resupplying in Tsagaannuur.&lt;/p&gt;

&lt;p&gt;Finally, a single man lived in the third ort. Since they knew Anuka would be bringing tourists to come visit, he had rented his ort to us for two nights.&lt;/p&gt;

&lt;p&gt;Anuka explained that we could ask some questions to Godla, and she would translate.&lt;/p&gt;

&lt;p&gt;I inquired, “What do you love about Mongolia?”&lt;/p&gt;

&lt;p&gt;Godla responded, “The freedom. In the village, it’s too busy. Out here, there are no rules. We can live with our animals and do whatever we want.”&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-02_01-44-40-355000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-02_01-44-40-355000_cam.webp&quot; alt=&quot;Godla holding onto his reindeer steed.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Godla holding onto his reindeer steed.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Observing how the nomads live, I began to truly understand the depth of his answer. Back in our hometowns, we reside in bustling cities governed by countless regulations.&lt;/p&gt;

&lt;p&gt;“Are you concerned about the upcoming winter?”&lt;/p&gt;

&lt;p&gt;Before sharing his thoughts, I’d like to provide some context for this question. Every November, the women migrate south, closer to where the weather is milder. This enables them to stay nearer to their children, who attend the village school if they’re of school age, purchase supplies more conveniently, and reach a hospital faster in case of emergencies. Meanwhile, the men venture deep into the Taiga, tracking the reindeer, which thrive in the harsh winter conditions. They remain here with the reindeer, either in solitude or alongside others if their reindeer herds happen to converge. Generally, reindeer are not herded; the only reliable way to manage these particularly wild livestock is to tether their young near the campsite.&lt;/p&gt;

&lt;p&gt;Anyways, Godla confidently answered the question about his concerns for winter, stating that he wasn’t afraid at all—in fact, he was excited. He added that if he misses his wife, he’ll return after a month; otherwise, he’ll extend his stay. They exchanged knowing glances and shared a laugh.&lt;/p&gt;

&lt;p&gt;“How has technology changed since your father was a herder?”&lt;/p&gt;

&lt;p&gt;“So many things. We have TVs to tune into the weather station (and watch soap operas), solar panels, chainsaws, too many things to count.”&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_06-02-54-127000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_06-02-54-127000_evan.webp&quot; alt=&quot;(left) Godla&apos;s neighbor&apos;s ort is equipped with a solar panel and satellite dish. (right) Inside, Lhagwa and Sartlong watch a soap opera.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/6/__2023-09-02_06-23-16-910000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/6/__2023-09-02_06-23-16-910000_evan.webp&quot; alt=&quot;(left) Godla&apos;s neighbor&apos;s ort is equipped with a solar panel and satellite dish. (right) Inside, Lhagwa and Sartlong watch a soap opera.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) Godla&apos;s neighbor&apos;s ort is equipped with a solar panel and satellite dish. (right) Inside, Lhagwa and Sartlong watch a soap opera.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;“Do you want more technology or do you want to balance tradition with modernity?”&lt;/p&gt;

&lt;p&gt;“We want more technology but nothing works here except solar.”&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;We needed more fire for the stove, so Godla slung a chainsaw over his back and gathered two reindeer. He mounted a saddle on the first and hopped on the second. He crossed the river and disappeared into the adjacent woods, leading the first reindeer with a rope. A few minutes later we heard the chainsaw busy at work, and soon after he re-emerged from the woods with our wood for the night.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_06-13-01-703000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_06-13-01-703000_cam.webp&quot; alt=&quot;Godla crosses the stream back to camp, riding his reindeer bare back. A second reindeer, exhausted by its payload, drags two large logs on either side of its saddle through the creek bed.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Godla crosses the stream back to camp, riding his reindeer bare back. A second reindeer, exhausted by its payload, drags two large logs on either side of its saddle through the creek bed.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;To show our gratitude, I chopped enough wood to get both orts through the night.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_06-18-51-798000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_06-18-51-798000_cam.webp&quot; alt=&quot;Ready to chop wood.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Ready to chop wood.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;hr /&gt;

&lt;p&gt;Reindeer herding is a life consumed by following–and most importantly–protecting the reindeer, as they are constantly being hunted by bears. Godla and Sartlong had lost five just in the last two weeks.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_22-37-26-850000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_22-37-26-850000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Sure enough, right as twilight was turning to night, when were back in our ort with Anuka, we heard the dogs barking relentlessly. Then, a gunshot! Rushing outside, we met Godla outside his tent, rifle in hand.&lt;/p&gt;

&lt;p&gt;He was looking across the river, where the dogs were still barking. In the tent, Anuka translated that he wasn’t aiming to kill, as it’s against shamanistic religion to kill it unless it threatens your life. He showed us his gun, a dated Russian rifle. I realized it would be a great time to gift Godla with a pair of pocket binoculars we had bought at the black market. When I gave it to him, he took it with both hands, touched it to his head out of appreciation, and inspected every feature. He looked up at me, grinned, and said, bayarlala (thank you).&lt;/p&gt;

&lt;h2 id=&quot;day-6-shaman-ritual&quot;&gt;Day 6: Shaman ritual&lt;/h2&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/6/__2023-09-02_01-30-07-459000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/6/__2023-09-02_01-30-07-459000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;This morning it was raining so we played a card game called &lt;em&gt;13&lt;/em&gt; for hours in the ort.&lt;/p&gt;

&lt;p&gt;When the rain stopped we went and picked blueberries. Anuka paid for her university tuition by picking blueberries for 8 hours a day for 20 days of each summer. She said 500g could be sold for about 5000 MNT. Near the end Anuka asked if we were bitten by mosquitoes. She said that since we took from the forest, it’s only fair that we give back.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/6/__2023-09-02_05-03-09-016000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/6/__2023-09-02_05-03-09-016000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/6/__2023-09-02_04-13-49-275000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/6/__2023-09-02_04-13-49-275000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/6/__2023-09-02_06-11-07-421000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/6/__2023-09-02_06-11-07-421000_evan.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Our diet with the herders consisted of beef ribs that Anuka brought and hung in the ort, bread and fried bread with blueberry jam, milk tea, and noodles, all prepared fresh. Anuka is a more capable cook with limited ingredients, a pot, a wood fire stove, and a river of fresh water than I am with a grocery store and an electric appliance kitchen.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/6/__2023-09-02_07-01-54-095000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/6/__2023-09-02_07-01-54-095000_evan.webp&quot; alt=&quot;(left) Anuka making a blueberry jam with part of our harvest. (right) A Tsaatan kitchen.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-02_22-32-10-518000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-02_22-32-10-518000_evan.webp&quot; alt=&quot;(left) Anuka making a blueberry jam with part of our harvest. (right) A Tsaatan kitchen.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) Anuka making a blueberry jam with part of our harvest. (right) A Tsaatan kitchen.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-01_06-03-20-640000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-01_06-03-20-640000_cam.webp&quot; alt=&quot;Kourtney and I getting comfortable at camp.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/5/__2023-09-02_01-50-11-611000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/5/__2023-09-02_01-50-11-611000_cam.webp&quot; alt=&quot;Kourtney and I getting comfortable at camp.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Kourtney and I getting comfortable at camp.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;That evening we were told that Godla was going to perform a ceremony. It was a special occasion because he only does them once or twice a month. It is an incredible burden, physically, emotionally, and spiritually. While we didn’t come for a ceremony, we felt honoured that Godla was willing to perform one.&lt;/p&gt;

&lt;p&gt;Before the ritual, the evening started off extremely casually. We played cards. We were laughing and chatting. In the finale of one particular game, Lhagwa was coaching Sartlong on a final hand over her shoulder. Anuka, mirroring Lhagwa, was behind Godla’s shoulder, pointing at suggestions as to how he should play his hand. It was husband versus wife. Who would take the prize money? In the end, Sartlong won. Shy as she was, she couldn’t help but laugh. She said she was glad we came because she hadn’t played in months.&lt;/p&gt;

&lt;p&gt;At the head of the ort, Godla had prepared his ceremonial stage. Hundreds of torn cloth pieces spanned across the altar, with white pieces on the left, and black and red pieces on the right. Each strip represented a ceremony that he’s done. White strips represented people who he’s helped within the first half of the moon cycle. These are sanctioned, whereas the red and black strips were rituals that took place in the second half of the moon cycle. Such ceremonies are considered taboo, and are traditionally paid for with blood sacrifice–though that itself is now considered taboo.&lt;/p&gt;

&lt;p&gt;An incandescent lightbulb hung from above, powered by a clunky portable battery, casting its glow across the sacred space. Beside the stage stood his magnificent drum. Crafted from the skin of a young deer, the drum’s wooden frame had been fashioned from special timber—either a bough struck by lightning or wood from ancient trees deemed sacred.&lt;/p&gt;

&lt;p&gt;With Godla turned away from us, Sartlong and Lhagwa started dressing him in a weighted and heavily ornate collage of animal skins, furs, and fabrics upon fabrics. He needed the help of two people to get it on. It was big, voluminous–even asymmetric, giving him the appearance of a hunched back. He looked like a yak with dreadlocks.&lt;/p&gt;

&lt;p&gt;On his head he wore a vulture and eagle feather mask and something that covered his eyes. Although he was a shorter man, silhouetted by the dimly lit ort, he looked large and powerful.&lt;/p&gt;

&lt;p&gt;Then he started drumming. Softly at first. Each hit generated a deep, muted thud. He moved to and from the altar as he drummed. At the altar he held the drum low and by the center of the ort he held it high. His movement was controlled. The drum beats got progressively louder. He then started speaking in Tuvan. Like the drumming, it got progressively more intense. His voice became strained, high pitched, and mantric. As the ceremony proceeded, he began disengaging with reality. His movements became more erratic and animalistic. Often he would cock his head and turn it swiftly from side to side, the movement exaggerated by his mask’s feathers. The cadence of his voice, distorted. Hissing through gritted teeth he chanted–ever more stressing and ever less human. And throughout all of this, the drum beat continued relentlessly. At times he was completely out of his own control and would throw himself backwards, raising the drum above his head, as if puppeteered by skyward spirits. Lhagwa was behind him the whole time, ensuring he could catch him if thrown back.&lt;/p&gt;

&lt;p&gt;I’d never witnessed someone have such an unrelenting and burdensome out-of-body experience before. It was beautiful and terrible simultaneously, holding me in a sort of trance-like purgatory. I think since I’d never experienced anything this different from my own cultural bubble, I caught myself perpetually disassociating from what was happening. “This is what a shaman ceremony &lt;em&gt;is like&lt;/em&gt;”, I would think to myself. I’d have to remind myself that I wasn’t watching a re-enactment, or viewing through a TV screen. I was watching a shaman ritual. He was, according to his religion, convening with spirits. And it was happening in front of us.&lt;/p&gt;

&lt;p&gt;The ceremony was relentless and lasted around an hour and a half. It was an incredible feat of stamina, to do all of that in heavy and restrictive garb. Even as observers, we found the experience incredibly draining.&lt;/p&gt;

&lt;p&gt;Whether you believe in shamanism or not, history has been shaped by these rituals. War decisions of humanity’s largest empire under Chinggis Khaan were decided by shamans. Although Kourtney and I to this day question the extent that our tourism exploited Godla, Mongolians seek his guidance and he delivers his message through similar ceremony.&lt;/p&gt;

&lt;p&gt;After a crescendo I almost couldn’t bear, he stopped suddenly, and slouched as if dead. He was dragged outside by Sartlong and Lhagwa, all the while his mantra whispered autonomously from his lips.&lt;/p&gt;

&lt;p&gt;No one said anything. I could hear the river outside and for the first time in 90 minutes the world felt bigger than our cacooning ort. I was shell-shocked and speechless. In the wake of what we witnessed, Anuka softly explained that the ceremony was over, and that he was outside deliberating with the spirits. “Didn’t he seem so strong?”, she admiringly whispered.&lt;/p&gt;

&lt;p&gt;Five minutes later he returned, not as a medium of spirits, but once more as Godla. We had sought answers to questions we had about directions to take in our currently tumultuous lives, and he divulged the answers he had learned from within the spirit world.&lt;/p&gt;

&lt;p&gt;As he spoke, I strained to listen to the sounds of his voice. Mongolian is such a beautiful language. I felt so fragile after the ritual that even without hearing Anuka’s translation, I wanted to cry.&lt;/p&gt;

&lt;p&gt;After the ceremony, we went straight to our ort. It was 2am and the night was bitterly cold. Anuka started a small fire for us to fall asleep to. What just happened? As I lay curled in my sleeping bag in the silence of night, the sounds of his rhythmically distorted voice replayed in my mind at a deafening volume, like I was on a powerful hallucinogen. I drifted off into a feverish dream. The dogs started barking far in the distance.&lt;/p&gt;

&lt;h2 id=&quot;day-7-the-bear-strikes-again&quot;&gt;Day 7: The bear strikes again&lt;/h2&gt;

&lt;p&gt;In the morning Anuka came back with news from the other ort. A reindeer had been killed by the bear last night. Godla and Sartlong had already processed the body. I took the camera, kicked on my boots, and rushed outside. By the river I found what remained of the reindeer.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-02_21-57-55-531000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-02_21-57-55-531000_cam.webp&quot; alt=&quot;(left) The reindeer&apos;s stomach resting where the animal was dressed. (right) One of the dogs picking at the intesines.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-02_22-02-51-687000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-02_22-02-51-687000_cam.webp&quot; alt=&quot;(left) The reindeer&apos;s stomach resting where the animal was dressed. (right) One of the dogs picking at the intesines.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) The reindeer&apos;s stomach resting where the animal was dressed. (right) One of the dogs picking at the intesines.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-02_21-59-15-694000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-02_21-59-15-694000_cam.webp&quot; alt=&quot;The portions of desirable meat uneaten by the bear hang from an impromptu wooden tripod, out of reach from the dogs. Strands of sinew sway in the gentle breeze.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The portions of desirable meat uneaten by the bear hang from an impromptu wooden tripod, out of reach from the dogs. Strands of sinew sway in the gentle breeze.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-02_22-02-08-533000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-02_22-02-08-533000_cam.webp&quot; alt=&quot;The reindeer&apos;s rib cage partially encases its skull, its eye fixated skywards in an immortal gaze.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The reindeer&apos;s rib cage partially encases its skull, its eye fixated skywards in an immortal gaze.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-02_22-25-59-146000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-02_22-25-59-146000_cam.webp&quot; alt=&quot;The hide is staked taut so a nice leather forms.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-02_22-26-30-977000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-02_22-26-30-977000_cam.webp&quot; alt=&quot;The hide is staked taut so a nice leather forms.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    The hide is staked taut so a nice leather forms.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Last night was the first day after the full moon, making last night’s event a “blood” ceremony. Did the ceremony cause this to happen? Is this our fault? I asked Anuka. She said it’s possible, but she tried to make light of it that the dogs are happy because they will eat well. I felt so guilty.&lt;/p&gt;

&lt;p&gt;But Godla and Sartlong seemed to be in good spirits, unfazed by what transpired. Anuka said that a reindeer costs 1M MNT, but that well crafted reindeer boots, which require the hide of one reindeer, also sell for 1M MNT. It’s better to keep the reindeer alive, but not all is lost. Before, the loss of a reindeer was much worse but now with urbanizing economy, where reindeer boots are fashionable (as well as useful), the loss is not as bad. It is a blatant example of a globalizing world threatening the Tsaatan: their reindeer are worth as much dead as alive.&lt;/p&gt;

&lt;p&gt;After giving them some gifts we had prepared, a multi use flashlight, a headlamp, a lightbulb, we departed. To rid myself of guilt, I told Godla I was very sorry for the loss of his reindeer. Anuka translated, “don’t worry, it became sick yesterday”. Reading between the lines, I think he was telling me he doesn’t think it was because of the ceremony.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_00-05-51-331000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_00-05-51-331000_cam.webp&quot; alt=&quot;Me, Sartlong, Godla, and Kourtney.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Me, Sartlong, Godla, and Kourtney.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_00-09-31-000000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_00-09-31-000000_cam.webp&quot; alt=&quot;Godla, the movie star, posing coy by Kourtney&apos;s horse.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Godla, the movie star, posing coy by Kourtney&apos;s horse.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;The trip back to Tsagaannuur was peaceful.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_00-57-16-060000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_00-57-16-060000_cam.webp&quot; alt=&quot;On the journey into the Taiga, Lhagwa rode his horse while pulling the baggage horse with a leash. But on the return trip, because my ineptitude at riding was slowing the group down, this time me and the baggage horse got swapped. How embarassing. And just as I&apos;m feeling most down about it, Lhagwa turns back, points at me with a shit-eating grin, and says one of the only English words he knows: &apos;baby&apos;.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    On the journey into the Taiga, Lhagwa rode his horse while pulling the baggage horse with a leash. But on the return trip, because my ineptitude at riding was slowing the group down, this time me and the baggage horse got swapped. How embarassing. And just as I&apos;m feeling most down about it, Lhagwa turns back, points at me with a shit-eating grin, and says one of the only English words he knows: &apos;baby&apos;.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;At the meeting point where Baagii was supposed to be, he wasn’t. But a bunch of reindeer herders were. According to one of them, they were waiting for a group of Mongolian tourists, so we waited with them. A young kid, his father, and his grandpa in law.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_02-29-47-689000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_02-29-47-689000_evan.webp&quot; alt=&quot;The most common forms of transportation: horse, motorcyle, reindeer.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_02-35-09-268000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_02-35-09-268000_cam.webp&quot; alt=&quot;The most common forms of transportation: horse, motorcyle, reindeer.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    The most common forms of transportation: horse, motorcyle, reindeer.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;The grandpa was an incredibly small man with a crinkled face and few teeth. He brought over three reindeer and invited us to pose beside them.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_03-06-25-975000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_03-06-25-975000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Afterwards we gave the kid a gift packet. Kourtney exchanged the Mickey Mouse eraser for a Spider-Man eraser, since he was wearing Spider-Man sneakers. He was very rambunctious and full of energy. We asked him what he wants to be when he grows up. His answer: a reindeer herder, just like his dad. Although I already knew the answer, I asked whether or not he was old enough to ride a reindeer.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_03-16-35-935000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_03-16-35-935000_cam.webp&quot; alt=&quot;With a boastful confidence, the boy articulately commands his reindeer with a dynamic range of rein maneuvring, heel kicks, and a stick he picked up off the ground to whip its ass with. His velcro Spiderman sneakers, the only thing resembling his age.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    With a boastful confidence, the boy articulately commands his reindeer with a dynamic range of rein maneuvring, heel kicks, and a stick he picked up off the ground to whip its ass with. His velcro Spiderman sneakers, the only thing resembling his age.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_03-16-32-742000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_03-16-32-742000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Then, a car pulled up, and out popped a bunch of people. Anuka recognized all of them. After a while Anuka started to piece it together: Gambut was just joking when he said he was waiting for tourists. He was in fact, just waiting for his family, and here they were.&lt;/p&gt;

&lt;p&gt;Commence the chaos. Out of the car they came, pulling and carrying around various supplies and gear. A large rack of meat was transferred from a thick plastic bag into a large rice sack. Saddles, guns, axes, food, and miscellaneous bags with unknown content were all being arranged on the ground. Clearly it was a very successful trip into the village. One by one the reindeer were getting saddled with all the gear. Everyone was working hard and hardly working. Older siblings were taking turns wrestling with the young kid while saddling their reindeer. Everyone was casually chatting and laughing as they readied their gear. In the midst of the activity someone had made a small fire and brewed up milk tea that got passed around with sour chalky cheese pucks. In contrast to every road trip I went on as a kid, they had no apparent anxiety about whether they were leaving on time, whether everything was packed, or whether the front door was locked. They were booming with a confidence gained only through a lifetime of nomadic living, where both everywhere and nowhere is a place to call home.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_03-44-46-370000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_03-44-46-370000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;And when they were ready, they mounted their reindeer and left as casually as they came. As I watched them disappear into the forest, I couldn’t help but feel a pang of envy for their carefree spirit.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_03-52-57-862000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_03-52-57-862000_cam.webp&quot; alt=&quot;Loaded with supplies from Tsagaannuur, the reindeer herders start their journey back to the great wilderness, a place they call home.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Loaded with supplies from Tsagaannuur, the reindeer herders start their journey back to the great wilderness, a place they call home.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/7/__2023-09-03_03-53-41-469000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/7/__2023-09-03_03-53-41-469000_cam.webp&quot; alt=&quot;The little boy riding on the back of his father&apos;s motorcycle back to Tsagaannuur.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The little boy riding on the back of his father&apos;s motorcycle back to Tsagaannuur.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;h2 id=&quot;day-8-to-the-eastern-reindeer-herders&quot;&gt;Day 8: To the eastern reindeer herders&lt;/h2&gt;

&lt;p&gt;In the morning we went to the kindergarten. Unlike the jovial ceremony we witnessed a few days ago, this time there was no song and dance. This was the first &lt;em&gt;real&lt;/em&gt; day of school.&lt;/p&gt;

&lt;p&gt;Outside we were greeted by a happy, plump woman in a short red dress, and her makeup done. She has been the headmaster since Anuka went to the kindergarten over 20 years ago, and it was the 30th year anniversary of the kindergarten. Before that, there was no school in the village, and people sent their children to boarding school at a distant town–or didn’t educate them. Needless to say, the headmaster was in a great mood, and her pride showed in her unwipable smile as she opened the door for us.&lt;/p&gt;

&lt;p&gt;However, upon crossing through the door’s archway, we were greeted by terrorized screams of around 30 kindergarteners who had just been stripped away from their families, dumped in a town full of strangers, and here to stay for the foreseeable future. I’m sure the first day of school is tough for most kids, but for many of these children, this was a boarding school, and they knew they wouldn’t be seeing their parents for a long time. Needless to say, they were completely inconsolable, and as horrible as it was, I couldn’t help but smile at the stark contrast between the joyful, celebratory mood of the headmaster and the sudden, overwhelming outbreak of tears and cries from the children.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-03_22-31-13-636000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-03_22-31-13-636000_evan.webp&quot; alt=&quot;(left) The entrance to the kindergarten. (right) Four boys playing on an airplane in the kindergarten playground.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-03_22-44-33-845000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-03_22-44-33-845000_evan.webp&quot; alt=&quot;(left) The entrance to the kindergarten. (right) Four boys playing on an airplane in the kindergarten playground.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) The entrance to the kindergarten. (right) Four boys playing on an airplane in the kindergarten playground.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Then we went to the store. There again happened to be the kindergarten headmaster. Anuka wanted to buy her gifts, so we agreed to pay for some wine and chocolate, and Anuka gave her the equivalent in cash. She was so happy. Later in the car, Anuka reflected on how grateful the headmaster was. She had had a really tough year. Her husband passed away last Winter. He was a jack of all trades, very successful. And one day he was helping a husband and wife cross a body of water that had frozen over. Since it was some technical offroad driving, he offered to drive the car, and suggested that the wife should get out of the passenger side seat because it was too dangerous. She didn’t, the ice broke, and they both drowned.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-03_22-59-11-809000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-03_22-59-11-809000_evan.webp&quot; alt=&quot;The Tsagaannuur store.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The Tsagaannuur store.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After our morning stroll through the village, we headed out for our second adventure into the Taiga, this time to the Eastern Reindeer Herders.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-03_23-50-35-065000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-03_23-50-35-065000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-04_00-11-00-320000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-04_00-11-00-320000_cam.webp&quot; alt=&quot;On our way to the starting point, Baagii points out a Mongolian Irish Pub, which is Baagii&apos;s joke for a group of Mongolians who, through happenstance, find themselves on a hillside drinking vodka, a not all too uncommon occurrence. Beside them are parked three motorcycles and a horse. And upon further inspection, one of them is Lhagwa, our horse wrangler! Down rolls the windows and Baagii and Anuka yell at him. Presumably something like, &apos;get your ass to the start point!&apos;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    On our way to the starting point, Baagii points out a Mongolian Irish Pub, which is Baagii&apos;s joke for a group of Mongolians who, through happenstance, find themselves on a hillside drinking vodka, a not all too uncommon occurrence. Beside them are parked three motorcycles and a horse. And upon further inspection, one of them is Lhagwa, our horse wrangler! Down rolls the windows and Baagii and Anuka yell at him. Presumably something like, &apos;get your ass to the start point!&apos;
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After some practice rounds of 13, where Baagi demonstrated some advanced strategies by pointing out at various groupings of cards within his hand, Lhagwa finally arrived and we began the trek. I had the same temperamental horse that I was riding yesterday. When we stopped for lunch, Lhagwa explained I was riding a sheep horse, which is what Mongolian children use to herd sheep. No wonder he called me baby.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-04_00-41-23-759000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-04_00-41-23-759000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;The journey was thematically similar to the first one. Through some rolling mountains, a descent into a valley, and then follow the river to the camp. There was no trail, just suggestions that probably start as animal trails and then are reinforced by mounted horse and reindeer. But the terrain is so spongy and wet that by the time a trail is formed, it’s already a mud pit.&lt;/p&gt;

&lt;p&gt;Given this, we established our own way through the marsh, bushland, creeks, bogs, and small timber forests. Once again Lhagwa led the charge with instincts he’s been honing since he himself was a 4 year old child herding sheep.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-04_05-54-15-948000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-04_05-54-15-948000_evan.webp&quot; alt=&quot;Lhagwa and Anuka reattaching the bags after they fell off the horse. I take the opportunity to stretch my legs.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Lhagwa and Anuka reattaching the bags after they fell off the horse. I take the opportunity to stretch my legs.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Interestingly, there exists a cultural divide between reindeer herders depending on the region they inhabit, either the western or eastern Taiga. In the west, herders like Godla and Sartlong live in small communities, congregating in small nuclear families, or if their reindeer herding patterns happen to coincide. This was in stark contrast to the Eastern reindeer herders, which live in just one or two large nomadic communities.&lt;/p&gt;

&lt;p&gt;I’m not sure of the exact reasons, but perhaps owing to the large community that the Eastern reindeer herders live in, they are much more commonly visited by foreign and domestic tourists than their Western counterparts. Tourists are coming all the time. And when guests arrive, they visit whoever they want to. It’s no doubt an over generalization to say that everyone wants guests, but it certainly seemed that way because there is an expectation to bring gifts, and of course to pay for their room and board.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-04_18-30-11-084000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-04_18-30-11-084000_evan.webp&quot; alt=&quot;The Eastern reindeer herders Summer camp.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The Eastern reindeer herders Summer camp.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Anyways, we opted to stay with an elderly woman. Her daughter was classmates with Anuka and just had a baby a month ago, so she was all alone, except for her grandson who just finished high school. Her name is Buyantogtokh–or just Buyan for short. She was very nice, immediately asking if we were warm enough, and if we wanted tea. She’s also incredible, being completely self sufficient at managing her reindeer and living in the harsh conditions of the taiga.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/people/__2023-09-05_22-56-46-666000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/people/__2023-09-05_22-56-46-666000_cam.webp&quot; alt=&quot;Buyantogtokh.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Buyantogtokh.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;She housed Lhagwa, Anuka, Kourtney and I in her ort next door. Once we settled, she came over for dinner that Anuka made. Inevitably the cards got pulled out, along with the vodka. After several days of playing, I finally won my very first game of 13. Maybe it was those lessons Baagii gave me. Feeling pretty rich with the 5K bill Lhagwa just handed me, I was quickly reminded that Anuka had beaten me the prior round, and so the money really belonged to her. Everyone laughed at my short-lived riches. Before handing it over I decided to take a picture of the bill, serving as perhaps the only proof that I ever won a game. Everyone thought it was hilarious that I took a picture of it.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 70%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/8/__2023-09-04_10-05-16-665000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/8/__2023-09-04_10-05-16-665000_evan.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;I went to bed feeling thankful for all the laughs we had shared that night. I felt amongst friends. The crackling sound of the fire was overlaid with Lhagwa’s drunken snoring. The flickering flames illuminated the animal skin walls. The last thing I remember before falling asleep was looking at a bright star through the chimney hole of the ort.&lt;/p&gt;

&lt;h2 id=&quot;day-9-autumnal-migration&quot;&gt;Day 9: Autumnal migration&lt;/h2&gt;

&lt;p&gt;We arrived at a very lucky time. The herders were undertaking their annual Autumnal migration to their final camp before winter. This happens because the reindeer need to move into the forest, where they can plump up before winter, where there is a lot of grass. Also in the valley where we spent the night, the reindeer travel far and can be difficult to herd, whereas in the new location they stay closer.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_20-27-02-700000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_20-27-02-700000_evan.webp&quot; alt=&quot;Rise and shine. A curious reindeer pokes its head into the ort as Anuka prepares breakfast, before eventually being shooed away.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_20-27-58-812000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_20-27-58-812000_cam.webp&quot; alt=&quot;Rise and shine. A curious reindeer pokes its head into the ort as Anuka prepares breakfast, before eventually being shooed away.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Rise and shine. A curious reindeer pokes its head into the ort as Anuka prepares breakfast, before eventually being shooed away.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_20-48-05-635000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_20-48-05-635000_cam.webp&quot; alt=&quot;Morning light outlines a peacefully grazing stag, haloing its antlers in pure white.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Morning light outlines a peacefully grazing stag, haloing its antlers in pure white.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-16-00-120000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-16-00-120000_cam.webp&quot; alt=&quot;Outside Buyantogtokh&apos;s ort, freshly cut antlers hang from a tree. Some of them still have velvet fuzz attached to them. It is through this velvet that arteries supply the antlers with ample blood flow, allowing them to grow quickly.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-17-01-094000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-17-01-094000_evan.webp&quot; alt=&quot;Outside Buyantogtokh&apos;s ort, freshly cut antlers hang from a tree. Some of them still have velvet fuzz attached to them. It is through this velvet that arteries supply the antlers with ample blood flow, allowing them to grow quickly.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Outside Buyantogtokh&apos;s ort, freshly cut antlers hang from a tree. Some of them still have velvet fuzz attached to them. It is through this velvet that arteries supply the antlers with ample blood flow, allowing them to grow quickly.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;From around 9 AM the community was bustling with activity.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_21-11-20-377000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_21-11-20-377000_evan.webp&quot; alt=&quot;Buyantogtokh leads Kourtney and I to her reindeer milk stash in the forest. Dug under the moss near a slowly moving creek, she looks around, and in a eureka moment peels off a nondescript clump of moss, revealing 40 plastic bottles of milk. Together we bring back her milk reserves to camp where her grandson starts bagging them.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_21-36-21-834000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_21-36-21-834000_evan.webp&quot; alt=&quot;Buyantogtokh leads Kourtney and I to her reindeer milk stash in the forest. Dug under the moss near a slowly moving creek, she looks around, and in a eureka moment peels off a nondescript clump of moss, revealing 40 plastic bottles of milk. Together we bring back her milk reserves to camp where her grandson starts bagging them.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Buyantogtokh leads Kourtney and I to her reindeer milk stash in the forest. Dug under the moss near a slowly moving creek, she looks around, and in a eureka moment peels off a nondescript clump of moss, revealing 40 plastic bottles of milk. Together we bring back her milk reserves to camp where her grandson starts bagging them.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Each herder was deconstructing their ort and placing the entirety of their possessions into plastic, fabric, and leather bags. Solar panels, lightbulbs, mattresses, furnaces, ropes, dishes, antlers, leather, reindeer milk, chainsaws, cutting boards, axes: everything these people owned was to be transported to the their new home.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_20-44-22-645000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_20-44-22-645000_cam.webp&quot; alt=&quot;Bit by bit, the Tsaatan pack up camp. With the canvases covering their orts now stripped away, the entirety of their material belongings now lay in just a few piles next to the frames of their once homes.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_22-02-56-914000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_22-02-56-914000_cam.webp&quot; alt=&quot;Bit by bit, the Tsaatan pack up camp. With the canvases covering their orts now stripped away, the entirety of their material belongings now lay in just a few piles next to the frames of their once homes.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Bit by bit, the Tsaatan pack up camp. With the canvases covering their orts now stripped away, the entirety of their material belongings now lay in just a few piles next to the frames of their once homes.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 60%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-38-50-874000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-38-50-874000_evan.webp&quot; alt=&quot;A battle worn solar panel lays next to the frame of an uncovered ort.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    A battle worn solar panel lays next to the frame of an uncovered ort.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;While some were busy breaking camp, others were herding their reindeer together for the migration. Each and every reindeer had to be accounted for.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-05-58-162000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-05-58-162000_cam.webp&quot; alt=&quot;(top-left) An elder corrals a pair of her reindeer, as indicated by the branded letter &apos;A&apos; on each of their torsos. (top-right) A huge stag is led to the staging area. Its antlers have been cut to avoid infighting within the herd. (bottom-left) To discourage endless wandering of the mother reindeer, their babies--which they will not abandon--are often tied down. (bottom-right) A calf gets one last suckle of milk before the migration starts.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_21-36-23-331000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_21-36-23-331000_cam.webp&quot; alt=&quot;(top-left) An elder corrals a pair of her reindeer, as indicated by the branded letter &apos;A&apos; on each of their torsos. (top-right) A huge stag is led to the staging area. Its antlers have been cut to avoid infighting within the herd. (bottom-left) To discourage endless wandering of the mother reindeer, their babies--which they will not abandon--are often tied down. (bottom-right) A calf gets one last suckle of milk before the migration starts.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_22-12-16-169000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_22-12-16-169000_cam.webp&quot; alt=&quot;(top-left) An elder corrals a pair of her reindeer, as indicated by the branded letter &apos;A&apos; on each of their torsos. (top-right) A huge stag is led to the staging area. Its antlers have been cut to avoid infighting within the herd. (bottom-left) To discourage endless wandering of the mother reindeer, their babies--which they will not abandon--are often tied down. (bottom-right) A calf gets one last suckle of milk before the migration starts.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-06-19-244000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-06-19-244000_cam.webp&quot; alt=&quot;(top-left) An elder corrals a pair of her reindeer, as indicated by the branded letter &apos;A&apos; on each of their torsos. (top-right) A huge stag is led to the staging area. Its antlers have been cut to avoid infighting within the herd. (bottom-left) To discourage endless wandering of the mother reindeer, their babies--which they will not abandon--are often tied down. (bottom-right) A calf gets one last suckle of milk before the migration starts.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (top-left) An elder corrals a pair of her reindeer, as indicated by the branded letter &apos;A&apos; on each of their torsos. (top-right) A huge stag is led to the staging area. Its antlers have been cut to avoid infighting within the herd. (bottom-left) To discourage endless wandering of the mother reindeer, their babies--which they will not abandon--are often tied down. (bottom-right) A calf gets one last suckle of milk before the migration starts.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Once the reindeer were gathered, a core set of each family’s herd would be saddled for the purpose of either riding or carrying possessions.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-00-23-784000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-00-23-784000_cam.webp&quot; alt=&quot;(top-left) Two men carefully secure an ort canvas to a reindeer’s flank, ensuring a balanced load on both sides. (top-right) Anuka holds the lead of a small caravan of loaded reindeer, while their owner finishes their final preparations. (bottom-left) A modern solar panel, neatly tied to the reindeer’s pack, offers a glimpse of contemporary influence (bottom-right) A striking white reindeer, adorned with matching white packs, stands poised.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-06-53-046000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-06-53-046000_cam.webp&quot; alt=&quot;(top-left) Two men carefully secure an ort canvas to a reindeer’s flank, ensuring a balanced load on both sides. (top-right) Anuka holds the lead of a small caravan of loaded reindeer, while their owner finishes their final preparations. (bottom-left) A modern solar panel, neatly tied to the reindeer’s pack, offers a glimpse of contemporary influence (bottom-right) A striking white reindeer, adorned with matching white packs, stands poised.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-11-08-520000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-11-08-520000_cam.webp&quot; alt=&quot;(top-left) Two men carefully secure an ort canvas to a reindeer’s flank, ensuring a balanced load on both sides. (top-right) Anuka holds the lead of a small caravan of loaded reindeer, while their owner finishes their final preparations. (bottom-left) A modern solar panel, neatly tied to the reindeer’s pack, offers a glimpse of contemporary influence (bottom-right) A striking white reindeer, adorned with matching white packs, stands poised.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-25-38-614000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-25-38-614000_cam.webp&quot; alt=&quot;(top-left) Two men carefully secure an ort canvas to a reindeer’s flank, ensuring a balanced load on both sides. (top-right) Anuka holds the lead of a small caravan of loaded reindeer, while their owner finishes their final preparations. (bottom-left) A modern solar panel, neatly tied to the reindeer’s pack, offers a glimpse of contemporary influence (bottom-right) A striking white reindeer, adorned with matching white packs, stands poised.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (top-left) Two men carefully secure an ort canvas to a reindeer’s flank, ensuring a balanced load on both sides. (top-right) Anuka holds the lead of a small caravan of loaded reindeer, while their owner finishes their final preparations. (bottom-left) A modern solar panel, neatly tied to the reindeer’s pack, offers a glimpse of contemporary influence (bottom-right) A striking white reindeer, adorned with matching white packs, stands poised.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_22-51-38-432000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_22-51-38-432000_cam.webp&quot; alt=&quot;The wood-fire stove, just like the reindeer that bear its weight, is a truly indispensible asset to the Tsaatan.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_22-52-18-055000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_22-52-18-055000_cam.webp&quot; alt=&quot;The wood-fire stove, just like the reindeer that bear its weight, is a truly indispensible asset to the Tsaatan.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_22-59-36-121000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_22-59-36-121000_cam.webp&quot; alt=&quot;The wood-fire stove, just like the reindeer that bear its weight, is a truly indispensible asset to the Tsaatan.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-03-36-464000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-03-36-464000_cam.webp&quot; alt=&quot;The wood-fire stove, just like the reindeer that bear its weight, is a truly indispensible asset to the Tsaatan.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    The wood-fire stove, just like the reindeer that bear its weight, is a truly indispensible asset to the Tsaatan.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;They tied the saddle-backed reindeer in lines, with the biggest in front, and the smallest in the back. The majority of reindeer, who were not saddled, were herded on the fly by skilled horsemen and reindeermen.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_22-53-16-528000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_22-53-16-528000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-05-35-261000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-05-35-261000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Then the herders took off down the valley in the direction from which they had migrated from a few months prior.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-45-33-001000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-45-33-001000_cam.webp&quot; alt=&quot;After finishing his cigarette, a gun-slinging nomad mounts his horse, grabs the rope tethering his reindeer caravan, and rides over the hill and out of sight.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-45-36-621000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-45-36-621000_cam.webp&quot; alt=&quot;After finishing his cigarette, a gun-slinging nomad mounts his horse, grabs the rope tethering his reindeer caravan, and rides over the hill and out of sight.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-45-37-901000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-45-37-901000_cam.webp&quot; alt=&quot;After finishing his cigarette, a gun-slinging nomad mounts his horse, grabs the rope tethering his reindeer caravan, and rides over the hill and out of sight.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_23-46-12-944000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_23-46-12-944000_cam.webp&quot; alt=&quot;After finishing his cigarette, a gun-slinging nomad mounts his horse, grabs the rope tethering his reindeer caravan, and rides over the hill and out of sight.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    After finishing his cigarette, a gun-slinging nomad mounts his horse, grabs the rope tethering his reindeer caravan, and rides over the hill and out of sight.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-04_22-48-35-022000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-04_22-48-35-022000_cam.webp&quot; alt=&quot;A horse contends with entrencheningly deep mud as its rider holds tightly to his tethered reindeer. Compared to horses, reindeer navigate this terrain more easily because they weigh less.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    A horse contends with entrencheningly deep mud as its rider holds tightly to his tethered reindeer. Compared to horses, reindeer navigate this terrain more easily because they weigh less.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Within a few hours, the camp was mostly deserted, except for a few people who were still waiting for their reindeer to return, Buyantogtokh included.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_00-30-14-769000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_00-30-14-769000_cam.webp&quot; alt=&quot;(left) As she waits for her unaccounted for reindeer to return, Buyantogtokh gathers her remaining, scattered possessions. Her loyal companion follows behind. (right) Lhagwa and I load our gear back onto the horses. It feels good to be trusted by our horse wrangler with the responsibility.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_00-43-20-005000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_00-43-20-005000_cam.webp&quot; alt=&quot;(left) As she waits for her unaccounted for reindeer to return, Buyantogtokh gathers her remaining, scattered possessions. Her loyal companion follows behind. (right) Lhagwa and I load our gear back onto the horses. It feels good to be trusted by our horse wrangler with the responsibility.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) As she waits for her unaccounted for reindeer to return, Buyantogtokh gathers her remaining, scattered possessions. Her loyal companion follows behind. (right) Lhagwa and I load our gear back onto the horses. It feels good to be trusted by our horse wrangler with the responsibility.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_01-02-20-626000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_01-02-20-626000_cam.webp&quot; alt=&quot;Anuka and Buyantogtokh waiting patiently for Buyantogtokh&apos;s reindeer to show up. At this point, almost all of the other families have left.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Anuka and Buyantogtokh waiting patiently for Buyantogtokh&apos;s reindeer to show up. At this point, almost all of the other families have left.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After hanging out with Buyantogtokh for a bit, Anuka, Lhagwa, Kourtney and I saddled up our horses and followed suit down the valley.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_01-23-46-441000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_01-23-46-441000_cam.webp&quot; alt=&quot;In the marshy valley, our path converges with a herd of 20 reindeer and their owner heading the same direction. For a fleeting minute, while Kourtney frantically gets the camera out, we find ourselves engulfed in the center of their herd, and I feel like I&apos;ve animorphed into one of them and joined their herd entirely.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    In the marshy valley, our path converges with a herd of 20 reindeer and their owner heading the same direction. For a fleeting minute, while Kourtney frantically gets the camera out, we find ourselves engulfed in the center of their herd, and I feel like I&apos;ve animorphed into one of them and joined their herd entirely.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;The journey took around 2 hours. In the final half hour, reindeer with empty saddles were being led back to the original camp to pick up what couldn’t be carried on the first trip.&lt;/p&gt;

&lt;p&gt;We arrived at the new camp and it was chaos. Piles of belongings were piled besides orts, which were all in the process of being erected. All the while, horses, reindeer, dogs, and humans were active all throughout the sparse forest.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_03-02-38-867000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_03-02-38-867000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_03-05-28-673000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_03-05-28-673000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
&lt;/figure&gt;

&lt;p&gt;From the moment we arrived at the new campsite, until well past dark, we joined the chaos and helped however we could. Primarily, we helped erect orts for newly arrived herders, since it requires the coordination of many hands to properly cover the teepee-like structure with the canvas.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_06-11-13-606000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_06-11-13-606000_evan.webp&quot; alt=&quot;(top-left) Stuff, everywhere. (top-right) Anuka, Kourtney, and Lhagwa assembling the ort for a family that returned to Summer camp to fetch the rest of their reindeer. (bottom-left) Getting full coverage of the canvas over the ort is half art, half science. (bottom-right) The ort construction begins with a central knot that ties 3 logs together. Once the tripod is assembled all of the other logs are leaned against the formed center.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_02-52-17-294000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_02-52-17-294000_evan.webp&quot; alt=&quot;(top-left) Stuff, everywhere. (top-right) Anuka, Kourtney, and Lhagwa assembling the ort for a family that returned to Summer camp to fetch the rest of their reindeer. (bottom-left) Getting full coverage of the canvas over the ort is half art, half science. (bottom-right) The ort construction begins with a central knot that ties 3 logs together. Once the tripod is assembled all of the other logs are leaned against the formed center.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_02-57-45-549000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_02-57-45-549000_evan.webp&quot; alt=&quot;(top-left) Stuff, everywhere. (top-right) Anuka, Kourtney, and Lhagwa assembling the ort for a family that returned to Summer camp to fetch the rest of their reindeer. (bottom-left) Getting full coverage of the canvas over the ort is half art, half science. (bottom-right) The ort construction begins with a central knot that ties 3 logs together. Once the tripod is assembled all of the other logs are leaned against the formed center.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_06-16-31-473000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_06-16-31-473000_evan.webp&quot; alt=&quot;(top-left) Stuff, everywhere. (top-right) Anuka, Kourtney, and Lhagwa assembling the ort for a family that returned to Summer camp to fetch the rest of their reindeer. (bottom-left) Getting full coverage of the canvas over the ort is half art, half science. (bottom-right) The ort construction begins with a central knot that ties 3 logs together. Once the tripod is assembled all of the other logs are leaned against the formed center.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (top-left) Stuff, everywhere. (top-right) Anuka, Kourtney, and Lhagwa assembling the ort for a family that returned to Summer camp to fetch the rest of their reindeer. (bottom-left) Getting full coverage of the canvas over the ort is half art, half science. (bottom-right) The ort construction begins with a central knot that ties 3 logs together. Once the tripod is assembled all of the other logs are leaned against the formed center.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;h2 id=&quot;day-10-making-the-rounds&quot;&gt;Day 10: Making the rounds&lt;/h2&gt;

&lt;p&gt;The plan for the day was to hang out with the herders and then ride our horses back to Tsagaannuur.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_02-51-14-809000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_02-51-14-809000_evan.webp&quot; alt=&quot;A very curious calf exploring us and its new home.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_02-51-16-302000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_02-51-16-302000_evan.webp&quot; alt=&quot;A very curious calf exploring us and its new home.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    A very curious calf exploring us and its new home.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/9/__2023-09-05_03-04-51-984000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/9/__2023-09-05_03-04-51-984000_cam.webp&quot; alt=&quot;Kourtney saying hello.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Kourtney saying hello.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-05_22-06-30-666000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-05_22-06-30-666000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;After breakfast, we made our way around and gave away the rest of our gifts to families that we had met. I really liked how well Anuka knew everyone because all of our gifts ended up being quite personalized.&lt;/p&gt;

&lt;p&gt;We met with the tribal leader and his wife, who were very interested to hear I was from Canada. They knew that reindeer were in Canada and asked if we had reindeer herders. I wasn’t sure, but I guessed yes. Turns out I wasn’t totally wrong either. In the 1930s there was an effort to introduce reindeer herding to northern communities, a programme dubbed the &lt;a href=&quot;https://en.wikipedia.org/wiki/Reindeer_Station&quot;&gt;Canadian Reindeer Project&lt;/a&gt;. It wasn’t as successful as projected, but it’s still practiced today by very small number of people.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-05_22-51-56-387000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-05_22-51-56-387000_cam.webp&quot; alt=&quot;Husband and wife.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-05_22-52-03-777000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-05_22-52-03-777000_cam.webp&quot; alt=&quot;Husband and wife.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Husband and wife.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Then we found Buyantogtokh. She got in late last night but had clearly settled well because she was out and about. We gave her some durable rubber twist ties that my sister bought me for Christmas a couple years ago. We thought it could save her arthritic hands from some of the endless rope tying necessitated by herder lifestyle. We also gave her a waterproof sack.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-05_22-56-13-315000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-05_22-56-13-315000_cam.webp&quot; alt=&quot;Buyantogtokh resting easy after another successful migration.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Buyantogtokh resting easy after another successful migration.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;hr /&gt;

&lt;p&gt;One of the herders came into our ort with a branding iron and placed it in the coals of the wood fire stove. Lhagwa followed in soon after, pointing at the iron with a grin on his face, making sure we understood something exciting was going to unfold. Anuka filled us in: a horse branding was underway.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_00-26-33-506000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_00-26-33-506000_cam.webp&quot; alt=&quot;Four men wrestle the unrelenting chestnut horse to ground. With its front legs tied together, the horse bucks, thrashes, and whinnies as the men gradually restrict its movement. Eventually, after considerable effort from both parties, the horse submits and the branding can begin.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_00-28-51-184000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_00-28-51-184000_cam.webp&quot; alt=&quot;Four men wrestle the unrelenting chestnut horse to ground. With its front legs tied together, the horse bucks, thrashes, and whinnies as the men gradually restrict its movement. Eventually, after considerable effort from both parties, the horse submits and the branding can begin.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_00-29-11-935000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_00-29-11-935000_cam.webp&quot; alt=&quot;Four men wrestle the unrelenting chestnut horse to ground. With its front legs tied together, the horse bucks, thrashes, and whinnies as the men gradually restrict its movement. Eventually, after considerable effort from both parties, the horse submits and the branding can begin.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_00-29-50-577000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_00-29-50-577000_cam.webp&quot; alt=&quot;Four men wrestle the unrelenting chestnut horse to ground. With its front legs tied together, the horse bucks, thrashes, and whinnies as the men gradually restrict its movement. Eventually, after considerable effort from both parties, the horse submits and the branding can begin.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Four men wrestle the unrelenting chestnut horse to ground. With its front legs tied together, the horse bucks, thrashes, and whinnies as the men gradually restrict its movement. Eventually, after considerable effort from both parties, the horse submits and the branding can begin.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_00-31-41-244000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_00-31-41-244000_cam.webp&quot; alt=&quot;One of the men fetches the branding iron from the coals of a nearby fire, using his sleeve to protect his hands with the heat. While two of the men restrain the horse with taut ropes, Lhagwa and the wielding man drive the iron into the horse&apos;s hindquarters. After a second of motionless obedience, the horse kicks hard and unexpectedly, destabilizing the men and freeing its hind from the branding.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    One of the men fetches the branding iron from the coals of a nearby fire, using his sleeve to protect his hands with the heat. While two of the men restrain the horse with taut ropes, Lhagwa and the wielding man drive the iron into the horse&apos;s hindquarters. After a second of motionless obedience, the horse kicks hard and unexpectedly, destabilizing the men and freeing its hind from the branding.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_00-32-50-948000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_00-32-50-948000_cam.webp&quot; alt=&quot;The men reconfigure for a second attempt. The horse&apos;s owner hovers the branding iron above the horse&apos;s hind until it superposes with the partial branding, and then presses in with an ironclad grip, finishing what he started. Exhausted, and with no potential recourse, the horse lays dutifully motionless.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The men reconfigure for a second attempt. The horse&apos;s owner hovers the branding iron above the horse&apos;s hind until it superposes with the partial branding, and then presses in with an ironclad grip, finishing what he started. Exhausted, and with no potential recourse, the horse lays dutifully motionless.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;We were planning to leave in the next couple of hours, but we had yet to give away our potentially most prized gift: the bullets we had bought in the Ulaanbaatar black market. As a gift for hosting us in his ort, we gave our host 25 bullets, and as we were deciding what to do with the remaining 25, someone entered the ort to play 13. Then another, and then another. Clearly the news was out that a tourist couple knows how to play. Typically each loser pays 5,000 MNT, but we had the idea to offer either 5,000 MNT or 2 bullets. And then it was off to the races. The energy was high. People were smoking, slamming their cards down, laughing, and most importantly, trying to win some prized bullets. After a couple games, we had to call quits and head back to Tsagaannuur. Since there wasn’t enough time for the herders to win all the bullets legitimately, we ended up divvying the remaining bullets to the players as gifts.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_02-00-24-103000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_02-00-24-103000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Before we left, our host called us over. To thank us for the bullets we gave him, he had saddled one of his reindeer and allowed us to ride it.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_03-10-25-657000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_03-10-25-657000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_03-12-35-044000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_03-12-35-044000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/10/__2023-09-06_03-15-19-583000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/10/__2023-09-06_03-15-19-583000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Then as quickly as we came, we were gone. The horse ride back was beautiful, and we went fast. We trotted for at least an hour through beautiful forest. I cantered with my horse, marking the first time during the trip that my horse and I synergized. Back in Tsagaannuur Anuka’s mom made white fish soup. Just like turkey dinner, it’s known to make you tired. It sure did because we were completely knocked out in no time.&lt;/p&gt;

&lt;h2 id=&quot;day-11-sheep-dumplings&quot;&gt;Day 11: Sheep dumplings&lt;/h2&gt;

&lt;p&gt;In the morning Baagii went to buy a sheep because we had run out of meat. We wanted to check it out, so we went out back where Baagii had a sheep tied up by the car.&lt;/p&gt;

&lt;p&gt;He dragged it over to the back of the house, positioned it on its back, and with a relatively small knife tried to make an incision through its underbelly. But the knife wasn’t sharp enough so he called over Anuka’s mom who brought a bigger knife. He got through all the wool and made an incision without the sheep making a fuss. Then he maneuvered his hand through the sheep, at which point the sheep started to kick, and then with a mighty force he ripped his bloody hand out. He showed us later, during the dissection, that he had plucked a main artery that runs just below the sheep’s spinal cord. Without any fight, the sheep was dead within a minute.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-06_21-27-07-349000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-06_21-27-07-349000_cam.webp&quot; alt=&quot;(top-left) Baagii maneuvering his hand through sheep&apos;s innards, searching for the artery that runs below the sheep&apos;s spinal cord. (top-right) After plucking the artery, Baagii steadies the sheep in its final moments. (bottom-left) Baagii beckons for me to pass the front right leg through an incision in the upper chest as a way of peeling back the sheep&apos;s underside, better exposing its cavity. Yet because I have no idea what I&apos;m doing, I for some reason go to shake his hand. He howls with laughter, nearly falling over. Later, he tells Anuka that he will think of that moment every time he kills a sheep and laugh. (bottom-right) With the innards removed, a large pool of blood remains, which Baagii scoops into a bucket with a ladle.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-06_21-27-39-441000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-06_21-27-39-441000_cam.webp&quot; alt=&quot;(top-left) Baagii maneuvering his hand through sheep&apos;s innards, searching for the artery that runs below the sheep&apos;s spinal cord. (top-right) After plucking the artery, Baagii steadies the sheep in its final moments. (bottom-left) Baagii beckons for me to pass the front right leg through an incision in the upper chest as a way of peeling back the sheep&apos;s underside, better exposing its cavity. Yet because I have no idea what I&apos;m doing, I for some reason go to shake his hand. He howls with laughter, nearly falling over. Later, he tells Anuka that he will think of that moment every time he kills a sheep and laugh. (bottom-right) With the innards removed, a large pool of blood remains, which Baagii scoops into a bucket with a ladle.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-06_21-50-02-048000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-06_21-50-02-048000_evan.webp&quot; alt=&quot;(top-left) Baagii maneuvering his hand through sheep&apos;s innards, searching for the artery that runs below the sheep&apos;s spinal cord. (top-right) After plucking the artery, Baagii steadies the sheep in its final moments. (bottom-left) Baagii beckons for me to pass the front right leg through an incision in the upper chest as a way of peeling back the sheep&apos;s underside, better exposing its cavity. Yet because I have no idea what I&apos;m doing, I for some reason go to shake his hand. He howls with laughter, nearly falling over. Later, he tells Anuka that he will think of that moment every time he kills a sheep and laugh. (bottom-right) With the innards removed, a large pool of blood remains, which Baagii scoops into a bucket with a ladle.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-06_21-53-06-064000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-06_21-53-06-064000_evan.webp&quot; alt=&quot;(top-left) Baagii maneuvering his hand through sheep&apos;s innards, searching for the artery that runs below the sheep&apos;s spinal cord. (top-right) After plucking the artery, Baagii steadies the sheep in its final moments. (bottom-left) Baagii beckons for me to pass the front right leg through an incision in the upper chest as a way of peeling back the sheep&apos;s underside, better exposing its cavity. Yet because I have no idea what I&apos;m doing, I for some reason go to shake his hand. He howls with laughter, nearly falling over. Later, he tells Anuka that he will think of that moment every time he kills a sheep and laugh. (bottom-right) With the innards removed, a large pool of blood remains, which Baagii scoops into a bucket with a ladle.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (top-left) Baagii maneuvering his hand through sheep&apos;s innards, searching for the artery that runs below the sheep&apos;s spinal cord. (top-right) After plucking the artery, Baagii steadies the sheep in its final moments. (bottom-left) Baagii beckons for me to pass the front right leg through an incision in the upper chest as a way of peeling back the sheep&apos;s underside, better exposing its cavity. Yet because I have no idea what I&apos;m doing, I for some reason go to shake his hand. He howls with laughter, nearly falling over. Later, he tells Anuka that he will think of that moment every time he kills a sheep and laugh. (bottom-right) With the innards removed, a large pool of blood remains, which Baagii scoops into a bucket with a ladle.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-06_21-42-34-933000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-06_21-42-34-933000_evan.webp&quot; alt=&quot;Baagii&apos;s daughter stands quietly behind him, pacifier in mouth, while he processes the freshly slaughtered animal. In Mongolia, there&apos;s no separation between meat and its source, and children grow up seeing the entire process from an early age. Observing her father provide for his family is a casual, everyday experience, one that fosters a deep respect for where their food comes from.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Baagii&apos;s daughter stands quietly behind him, pacifier in mouth, while he processes the freshly slaughtered animal. In Mongolia, there&apos;s no separation between meat and its source, and children grow up seeing the entire process from an early age. Observing her father provide for his family is a casual, everyday experience, one that fosters a deep respect for where their food comes from.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;We had some left over gifts to give the children, so we made a stop by the school. A group of girls were fascinated with Kourtney’s hair, and indiscreetly whispered to each other, wide-eyed and shy. We found a group of three girls and two boys who all shared a room together. Kourtney laid out the remaining pencil cases that fashioned Disney or superhero characters for the kids to select from. Without any arguing they all picked their favorites. We talked with them for a bit. They were very shy so to break the ice we had Kourtney count to 10 in Mongolian, which conjured some laughter. Then one of the girls counted to 10 in English. We asked them what they wanted to be when they grow up: basketball players, singers, and teachers.&lt;/p&gt;

&lt;p&gt;We returned to pack up our stuff. Anuka’s mom gave us some pendants made of reindeer antlers. Anuka prepared us a plate of sheep heart, liver, intestine, and stomach.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_00-33-32-433000_kourtney.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_00-33-32-433000_kourtney.webp&quot; alt=&quot;(left) A small plate of sheep innards prepared for us, including heart, liver, intestine, and stomach. (right) A bowl of the innards being shared with the neighbors.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_01-14-42-277000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_01-14-42-277000_evan.webp&quot; alt=&quot;(left) A small plate of sheep innards prepared for us, including heart, liver, intestine, and stomach. (right) A bowl of the innards being shared with the neighbors.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) A small plate of sheep innards prepared for us, including heart, liver, intestine, and stomach. (right) A bowl of the innards being shared with the neighbors.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;This was truly a delicacy amongst Mongolians. Thighs and ribs can be smoked and stored. But fresh innards last for just a few hours. We tried the heart. It was very different but palatable. Then we tried a piece of the stomach which sent me into gag territory. Embarrassed by our foreign palates, we returned the plate mostly untouched.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-06_22-43-39-472000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-06_22-43-39-472000_cam.webp&quot; alt=&quot;The sheep meat hangs to dry alongside a clothesline outside Anuka&apos;s parents&apos; front porch while an innards feast takes place inside.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The sheep meat hangs to dry alongside a clothesline outside Anuka&apos;s parents&apos; front porch while an innards feast takes place inside.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Inside, there was an innards boil and the energy was high. Everyone was gobbling it up like Christmas dinner. People were picking up the small intestine and knifing off pieces. Anuka explained to us that the younger generation, especially from the city, considers innards to be dog food, and that it’s uncivilized. But it is a part of Anuka’s culture, and although Kourtney and I found the taste and texture to be disgusting, the practice of making full use of the animal was admirable and beautiful. Equally, I found the experience to serve as powerful evidence that taste preference is entirely relative and dictated by cultural norms.&lt;/p&gt;

&lt;p&gt;Afterwards Kourtney and I had a long discussion about our meat consumption habits. In our Western society, there is an increasingly perpetuated disconnection between the meat we buy at the grocery store and the animal who supplied the meat. This disconnect both protects us from questioning the morality of our meat consumption habits and turns a blind eye to increasingly unethical factory farming practices that drive our meat supply. It was eye-opening to observe a way of life where animal and meat are inextricably linked. Where animal’s are given free-roaming lives, never killed prematurely, and where no part goes unused when they are slaughtered.&lt;/p&gt;

&lt;p&gt;During the feast, someone came over to borrow Anuka’s mom’s weigh scale. They picked a bunch of blueberries and wanted to know how much. They were invited in, and they gladly took part in the feast. Afterwards, they weighed the blueberries. 58kg at 8,000 MNT per kilo, or 464,000 MNT ($131 USD). Not too shabby. When they left, the gate was left open and three cows came onto the property, happy to have found fresh grass. Anuka shooed them away.&lt;/p&gt;

&lt;p&gt;The plan for the night was to visit Lhagwa’s Autumn home near Tsagaannuur. It was a beautiful little one room cabin.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_21-28-29-207000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_21-28-29-207000_cam.webp&quot; alt=&quot;(left) The view from Lhagwa&apos;s cabin. (right) Kourtney petting a plump little cat, cats being somewhat uncommon in Mongolia. It seemed that Anuka was slightly superstitious about cats. I wonder if Mongolians think this way in general.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_04-35-38-166000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_04-35-38-166000_evan.webp&quot; alt=&quot;(left) The view from Lhagwa&apos;s cabin. (right) Kourtney petting a plump little cat, cats being somewhat uncommon in Mongolia. It seemed that Anuka was slightly superstitious about cats. I wonder if Mongolians think this way in general.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) The view from Lhagwa&apos;s cabin. (right) Kourtney petting a plump little cat, cats being somewhat uncommon in Mongolia. It seemed that Anuka was slightly superstitious about cats. I wonder if Mongolians think this way in general.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Inside, his mother-in-law prepared some surcream with bread, chalky sour dairy pucks, and of course, milk tea.&lt;/p&gt;

&lt;p&gt;Two of the four cabin walls were covered in innumerable horse racing medals mounted on the walls that Lhagwa had won. Clearly Anuka chose the right horse wrangler for us.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_02-37-00-428000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_02-37-00-428000_evan.webp&quot; alt=&quot;(left) A section of the cabin wall dedicated to Lhagwa&apos;s esteemed horse racing track record. (right) Wrestling medals earned by one of Lhagwa&apos;s sons.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_02-37-29-002000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_02-37-29-002000_evan.webp&quot; alt=&quot;(left) A section of the cabin wall dedicated to Lhagwa&apos;s esteemed horse racing track record. (right) Wrestling medals earned by one of Lhagwa&apos;s sons.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) A section of the cabin wall dedicated to Lhagwa&apos;s esteemed horse racing track record. (right) Wrestling medals earned by one of Lhagwa&apos;s sons.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;We ate some surcream and bread then wandered outside to catch a view of Tsagaannuur and Lhagwa’s animals.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_03-10-18-162000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_03-10-18-162000_cam.webp&quot; alt=&quot;(top-left, top-right, bottom-left) Happy, free-roaming livestock enjoying their lives. (bottom-right) Kourtney and Anuka messing around in typical, unhurried, nomadic fashion. Tsagaannuur in the distance.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_03-17-29-808000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_03-17-29-808000_cam.webp&quot; alt=&quot;(top-left, top-right, bottom-left) Happy, free-roaming livestock enjoying their lives. (bottom-right) Kourtney and Anuka messing around in typical, unhurried, nomadic fashion. Tsagaannuur in the distance.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_03-20-16-948000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_03-20-16-948000_cam.webp&quot; alt=&quot;(top-left, top-right, bottom-left) Happy, free-roaming livestock enjoying their lives. (bottom-right) Kourtney and Anuka messing around in typical, unhurried, nomadic fashion. Tsagaannuur in the distance.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_03-40-37-454000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_03-40-37-454000_cam.webp&quot; alt=&quot;(top-left, top-right, bottom-left) Happy, free-roaming livestock enjoying their lives. (bottom-right) Kourtney and Anuka messing around in typical, unhurried, nomadic fashion. Tsagaannuur in the distance.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (top-left, top-right, bottom-left) Happy, free-roaming livestock enjoying their lives. (bottom-right) Kourtney and Anuka messing around in typical, unhurried, nomadic fashion. Tsagaannuur in the distance.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_03-37-10-146000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_03-37-10-146000_cam.webp&quot; alt=&quot;The hindquarters of the sheep Baagii killed in the morning being hung and smoked by a dung pie. Once sufficiently smoked, the meat can be preserved for around a week unrefrigerated. In Mongolia, and especially in the Steppe, where trees aren&apos;t bountiful, it&apos;s common to use dried livestock poo as a source of fire. Nearby, a sheep belt hangs over the handlebars of a motorcycle.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    The hindquarters of the sheep Baagii killed in the morning being hung and smoked by a dung pie. Once sufficiently smoked, the meat can be preserved for around a week unrefrigerated. In Mongolia, and especially in the Steppe, where trees aren&apos;t bountiful, it&apos;s common to use dried livestock poo as a source of fire. Nearby, a sheep belt hangs over the handlebars of a motorcycle.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Last spring was a terrible spring, with bitter winds and no grass. Mongolia had 72 million livestock. Now Anuka thinks it could be less than 60 million. When there is such profound levels of death, it’s too much meat to eat, and the quality of the meat is so poor, so herders either bury the bodies or cut open their abdomens and let the vultures and various other birds dissect the carcasses. Lhagwa, with his 400 livestock, suffered great loss during this tough year, losing about 200 goats and sheep, Anuka predicted. This uncertainty comes with the territory of being a nomad. You can’t put your money into a bank, it roams freely.&lt;/p&gt;

&lt;p&gt;After a few hours Lhagwa called. It was getting dark and he needed some help taking down some tourist gers with his wife. So Baagii, Kourtney and I jumped in the car to help.&lt;/p&gt;

&lt;p&gt;Lhagwa was loading up two gers into his old Russian truck.&lt;/p&gt;

&lt;p&gt;After around thirty minutes the truck was loaded, but Lhagwa couldn’t get the truck started. As he tried, Baagii pointed to the truck and said, “danger car, big danger car” while laughing. Our path forward was a relatively steep slope on wet grass, and so after Baagii’s observation, Kourtney used her body language to convince Lhagwa’s wife to ride with us, rather than in the death-mobile. Eventually Lhagwa got the car into a chortling start, and we followed behind him.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_07-00-07-135000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_07-00-07-135000_evan.webp&quot; alt=&quot;Helping Lhagwa deconstruct a guesthouse ger I assume he manages. The tourist season is winding down in anticipation of the cold Autumn to come.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_06-59-59-701000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_06-59-59-701000_evan.webp&quot; alt=&quot;Helping Lhagwa deconstruct a guesthouse ger I assume he manages. The tourist season is winding down in anticipation of the cold Autumn to come.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_06-31-58-621000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_06-31-58-621000_evan.webp&quot; alt=&quot;Helping Lhagwa deconstruct a guesthouse ger I assume he manages. The tourist season is winding down in anticipation of the cold Autumn to come.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Helping Lhagwa deconstruct a guesthouse ger I assume he manages. The tourist season is winding down in anticipation of the cold Autumn to come.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_19-31-30-248000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_19-31-30-248000_cam.webp&quot; alt=&quot;Lhagwa&apos;s UAZ-452, known as a &apos;Bukhanka&apos; in Russian (bread loaf). This iconic Soviet-era utility vehicle is a modern convenience in Mongolia, with a simple, serviceable design and high ground clearance. A heavy mist hangs in the air, shielding the looming mountainous wilderness from the simple pastoral grounds that the mixed flock in the foreground lounge within.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Lhagwa&apos;s UAZ-452, known as a &apos;Bukhanka&apos; in Russian (bread loaf). This iconic Soviet-era utility vehicle is a modern convenience in Mongolia, with a simple, serviceable design and high ground clearance. A heavy mist hangs in the air, shielding the looming mountainous wilderness from the simple pastoral grounds that the mixed flock in the foreground lounge within.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;We followed behind as Lhagwa barrelled across the slope side, slipping several times in what I thought would be a tip, potentially rolling into the lake. His wife, safely in our car, would go “ooooooooo!” and then nervously laugh each time it happened. To cap off the adrenaline rush, Lhagwa undertook a short horse herding adventure by veering brazenly off-course in an attempt to herd a group of his horses towards the direction of his cabin.&lt;/p&gt;

&lt;p&gt;When we got back, Anuka was in the middle of preparing sheep steamed dumplings from one of the hind legs. Before, during, and after eating dumplings, we played cards on a fold up table under a flickering hospital white light bulb. I won the first round but Lhagwa reigned supreme for the rest of the night with an impressive winning streak. Must have been home court advantage.&lt;/p&gt;

&lt;h2 id=&quot;day-12-farewell-lhagwa&quot;&gt;Day 12: Farewell Lhagwa&lt;/h2&gt;

&lt;p&gt;Our plan today was to make it to Khövsgöl lake, where Anuka had rented a small tourist cabin.&lt;/p&gt;

&lt;p&gt;In the morning we said goodbye to Lhagwa, his wife, his mother-in-law, and their 400 goats, sheep, cows, and horses. We gifted Lhagwa and his family a flashlight setup that can be powered multiple different ways and a pair of pocket binoculars. And inadvertently, my sandals.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/11/__2023-09-07_20-35-38-501000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/11/__2023-09-07_20-35-38-501000_evan.webp&quot; alt=&quot;Thank you Lhagwa.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Thank you Lhagwa.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Although Kourtney and I shared zero linguistic overlap with Lhagwa, he had become a dear friend of ours over the last week. He guided us safely through the Taiga. We broke bread with him, assembled orts, and disassembled gers. We found the overlap in our humour. He inspired us with his selfless attitude towards hard work that benefits those around him. And what he’s no doubt most proud about, he beat us in cards many times, and laughed hard at the wins and losses.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;“How long is the drive to Khövsgöl lake?” I asked.&lt;/p&gt;

&lt;p&gt;“250km. The time I cannot say”, Anuka answered while laughing.&lt;/p&gt;

&lt;p&gt;A question impossible to answer, no doubt. It was a rainy day, which will affect which mountain passes we choose. We would get further information from cars passing from the other direction. Perhaps this river would be too high, this steep section too muddy. And thus from our available options, our path would emerge. Whatever would come to be, we knew it would be a long day, as even during the rarest and most ideal stretches of land, our speed wouldn’t surpass 30 mph.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-07_21-35-33-181000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-07_21-35-33-181000_cam.webp&quot; alt=&quot;Our journey to Khövsgöl lake (I).&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_00-06-43-132000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_00-06-43-132000_cam.webp&quot; alt=&quot;Our journey to Khövsgöl lake (I).&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Our journey to Khövsgöl lake (I).
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_02-52-05-904000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_02-52-05-904000_cam.webp&quot; alt=&quot;Our journey to Khövsgöl lake (II).&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_03-01-23-339000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_03-01-23-339000_cam.webp&quot; alt=&quot;Our journey to Khövsgöl lake (II).&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_03-10-06-615000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_03-10-06-615000_cam.webp&quot; alt=&quot;Our journey to Khövsgöl lake (II).&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_05-55-37-138000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_05-55-37-138000_cam.webp&quot; alt=&quot;Our journey to Khövsgöl lake (II).&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    Our journey to Khövsgöl lake (II).
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;After car pushes, countless river crossings, failing power steering and subsequent calls to Baagii’s car mechanic, we stopped to let the car cool down for the 5th or 6th time.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_02-49-34-376000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_02-49-34-376000_cam.webp&quot; alt=&quot;Seemingly every Mongolian is an amateur mechanic, and Baagii is no exception to this generalization.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Seemingly every Mongolian is an amateur mechanic, and Baagii is no exception to this generalization.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_06-05-06-231000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_06-05-06-231000_cam.webp&quot; alt=&quot;(top) A nomad rolls up. Seeing Kourtney&apos;s camera, he wants her to take a picture. (bottom) When I introduce myself in Mongolian, he looks at me with a mix of bewilderment and amusement.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_06-05-08-201000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_06-05-08-201000_cam.webp&quot; alt=&quot;(top) A nomad rolls up. Seeing Kourtney&apos;s camera, he wants her to take a picture. (bottom) When I introduce myself in Mongolian, he looks at me with a mix of bewilderment and amusement.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_06-05-40-343000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_06-05-40-343000_cam.webp&quot; alt=&quot;(top) A nomad rolls up. Seeing Kourtney&apos;s camera, he wants her to take a picture. (bottom) When I introduce myself in Mongolian, he looks at me with a mix of bewilderment and amusement.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/12/__2023-09-08_06-05-41-924000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/12/__2023-09-08_06-05-41-924000_cam.webp&quot; alt=&quot;(top) A nomad rolls up. Seeing Kourtney&apos;s camera, he wants her to take a picture. (bottom) When I introduce myself in Mongolian, he looks at me with a mix of bewilderment and amusement.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (top) A nomad rolls up. Seeing Kourtney&apos;s camera, he wants her to take a picture. (bottom) When I introduce myself in Mongolian, he looks at me with a mix of bewilderment and amusement.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;At 7:30, 10 hours since our departure, we made it to asphalt. Just two minutes away was the longitude-latitude coordinates 50’100’, with an architectural monument pinpointing the spot. Even more incidentally, Anuka’s father, an artist, was commissioned by the mayor of the nearby village to design the monument, and was promised some property on Khövsgöl Lake as his commission. But after the monument was built and before the keys were handed over, the election cycle brought a new mayor into office, who never acknowledged the contract. Kourtney and I hiked up to the spot begrudgingly, while Baagii cooked instant ramen. In solidarity of this grievance, I won’t share a picture, but it was a cool monument.&lt;/p&gt;

&lt;p&gt;For the last 45km to Khövsgöl Lake, Baagii, with unwavering concentration, navigated the two-lane highway. By western standards it was shoddy, but after 8 days of roadless mountain driving, it felt like the Autobahn. As the road signs flew past us at a blistering 40mph, I daydreamt about taking Anuka and Baagii to an American amusement park. We arrived at Khövsgöl Lake in the dark and clambered into our sleeping bags.&lt;/p&gt;

&lt;h2 id=&quot;day-13-batsur&quot;&gt;Day 13: Batsur&lt;/h2&gt;

&lt;p&gt;Our plan for the day was to hang out at the lake, kick our feet up, and relax.&lt;/p&gt;

&lt;p&gt;In the morning we went for a motorboat ride. The boat driver was Batsur, one of Anuka and Baagii’s university friends. He worked hard as a boat driver for 10 years until he could buy his own speed boat. Now he makes good money driving his own boat.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/13/__2023-09-08_23-06-37-413000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/13/__2023-09-08_23-06-37-413000_evan.webp&quot; alt=&quot;(left) Baagii in his friend&apos;s speed boat. (right) Batsur, Baagii and I playing a rock throwing game, in which the goal is to throw up one rock, and then strike it with a second before the first hits the ground.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/13/__2023-09-08_23-39-50-927000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/13/__2023-09-08_23-39-50-927000_cam.webp&quot; alt=&quot;(left) Baagii in his friend&apos;s speed boat. (right) Batsur, Baagii and I playing a rock throwing game, in which the goal is to throw up one rock, and then strike it with a second before the first hits the ground.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) Baagii in his friend&apos;s speed boat. (right) Batsur, Baagii and I playing a rock throwing game, in which the goal is to throw up one rock, and then strike it with a second before the first hits the ground.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Khövsgöl lake is a big deal for Mongolians. Visiting the landlocked country’s largest lake is a tourist destination for foreigners and natives alike. And for Mongolians, most who can’t swim, ripping around on a motorboat is a thrilling novelty. While foreign tourism has mostly finished for the year, there was still plenty of domestic tourism in mid September. Lining the shore were mini cottages, houses, and shacks for rent.&lt;/p&gt;

&lt;p&gt;After breaking the ice with Batsur with a self-explanatory rock-throwing contest, we got it on our heads that the 5 of us would go to a nearby karaoke bar. To prepare, we went to the shop and bought 15 beers of various Mongolian brands. But after calling the establishment, we realized they closed early, and so we drank the night away in our cottage house. For dinner we had more sheep dumplings, this time prepared by Anuka &lt;em&gt;and&lt;/em&gt; Kourtney.&lt;/p&gt;

&lt;style&gt;
  .image-gallery {
    overflow: hidden;
    margin-bottom: 0;
    margin-left: 0.5%;
    margin-right: 0.5%;
    margin-top: 0.5%;
    list-style-type: none;
    padding: 0;
  }

  .image-gallery li {
    float: left;
    display: block;
    width: 49%;
    margin: 0.5% 0.5% 0.5% 0.5%;
    padding: 0;
  }

  .image-gallery li a {
    text-align: center;
    text-decoration: none !important;
    color: #777;
  }

  .image-gallery li a img {
    width: 100%;
    display: block;
  }

  .gallery-caption {
    background-color: #f0f0f0;
    border-radius: 0px 0px 8px 8px;
    width: auto;
    padding-left: 0.5em;
    padding-top: 0;
    display: block;
    font-style: italic;
    margin-bottom: 1rem;
  }

  .gallery-caption:empty {
    display: none;
  }

  .image-gallery-figure {
    border-radius: 8px 8px 8px 8px;
    /*background-color: #f0f0f0;*/
    background-image: linear-gradient(to bottom, transparent, #f0f0f0);
  }
&lt;/style&gt;

&lt;figure class=&quot;image-gallery-figure&quot; style=&quot;width: 100%; margin: auto;&quot;&gt;
  &lt;ul class=&quot;image-gallery&quot;&gt;
    
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/13/__2023-09-09_01-15-26-946000_kourtney.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/13/__2023-09-09_01-15-26-946000_kourtney.webp&quot; alt=&quot;(left) Round two of sheep dumplings, prepared by Kourtney and Anuka. The worst constructed one was made by me. (right) The sheep bone picked clean by Baagii.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      &lt;figure class=&quot;photo-fig-gallery-element&quot; style=&quot;width: 100%;&quot;&gt;
        &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/13/__2023-09-09_08-40-17-363000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
          &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/13/__2023-09-09_08-40-17-363000_evan.webp&quot; alt=&quot;(left) Round two of sheep dumplings, prepared by Kourtney and Anuka. The worst constructed one was made by me. (right) The sheep bone picked clean by Baagii.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
        &lt;/a&gt;
      &lt;/figure&gt;
    &lt;/li&gt;
    
  &lt;/ul&gt;
  
  &lt;figcaption class=&quot;gallery-caption&quot;&gt;
    (left) Round two of sheep dumplings, prepared by Kourtney and Anuka. The worst constructed one was made by me. (right) The sheep bone picked clean by Baagii.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;The night was a mix of laughter and emotional conversations. I found it very impressive how communicative Baagii was despite having a miniscule English vocabulary. We learned that Batsur and Baagii used to play Counterstrike together at university. And that Baagii once drank 6L of kumis. Jokingly, he claimed that from the center of the ger he was projectile vomiting outside the door.&lt;/p&gt;

&lt;p&gt;On a separate note, Baagii thanked us for respecting his culture by participating in the sheep slaughter the previous day. As we drank, Baagii took the remainder of the sheep leg that Anuka used to make dumplings, grabbed a knife, and picked the bone clean. Anuka clarified to us that it wasn’t because he was necessarily still hungry, she said. Instead, it’s because it is both practical an honorable to revere the animal’s sacrifice by letting nothing go to waste. She also said if you are a guest and you similarly pick the bone clean, the owners will be “so, so appreciative” because it means you’re fully respecting their hospitality.&lt;/p&gt;

&lt;h2 id=&quot;day-14-coming-full-circle&quot;&gt;Day 14: Coming full circle&lt;/h2&gt;

&lt;p&gt;In the morning we drove to Murun to get the car fixed. On the way we visited some ancient &lt;a href=&quot;https://en.wikipedia.org/wiki/Deer_stones_culture&quot;&gt;deer stones&lt;/a&gt;. Just like stonehenge, the meaning of these ancient monuments remains a mystery to this day, although there are some pre-historic traditions in the region that remain steadfast, such as burying Murun people in mountains surrounding these deer stones. I wonder if it’s related?&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 50%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/14/__2023-09-09_23-58-51-530000_cam.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/14/__2023-09-09_23-58-51-530000_cam.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
&lt;/figure&gt;

&lt;p&gt;Stuck behind a huge flock of sheep being herded down a road on the outskirts of Murun, Anuka, Kourtney and I jumped out of the car to visit the monastery to our right while Baagii went to fix the car. We went for a stroll through the monastery. Anuka brought us inside the building where some kind of Buddhist reading was taking place. It was jam packed with people, with no place to sit. Everyone’s staring gaze pushed us back through the doors we came. Anuka thought it was a funeral procession of some sort.&lt;/p&gt;

&lt;p&gt;Then we walked to the supermarket where we would meet up with Baagii. Along the way we saw some statues, street art, illegally high apartment buildings, and a congregation of people attending a wedding event. Half the men were in suits, half in deels.&lt;/p&gt;

&lt;p&gt;At the market Baagii had bought us gifts. For Kourtney, he noticed her phone screen was broken so he bought her a new screen protector. And for both of us he bought us a deck of novelty cards, where each card was a picture of a famous Mongolian wrestler in their traditional (and revealing) garb. As he placed the cards in my hands he winked and said, “Mongolian playboy”.&lt;/p&gt;

&lt;p&gt;Then we were back on the (paved 🙏) road. We decided we would stay with Handa and her husband, the nomads we encountered a week ago, who had invited us into their ger. That doesn’t mean we arranged this with them–our plan was to just show up. Anuka wasn’t kidding when she said Mongolians have no word for “go away”. When we arrived, Handa was just finishing a horse milking session and seamlessly transitioned into hostess mode, preparing milk tea and cookies with surcream, and of course, kumis.&lt;/p&gt;

&lt;p&gt;Their kid had left for school, living in a 50 bed dormitory in a village school around 100km away. He was the kid who wanted us to send a message to Michael Jordan. In his place, was Kourtney’s newest obsession: a small pregnant cat gnawing a sheep’s thigh bone.&lt;/p&gt;

&lt;p&gt;It felt full circle to be spending our last rural night at the very place where we experienced the greatest culture shock of the trip. A week ago, we had no idea about nomadic life when we were invited into Handa and her husband’s life. Without context, I was handed a bowl of fermented horse milk from a large cow skin full of unrefridgerated dairy product, and eating rock-hard cheese completely unique to my palette. Just moments before that, Kourtney was milking a horse and I was hitching a ride on a stranger’s motorcycle. The experience gave me such a culture shock that I was fighting back a bit of an adrenaline rush.&lt;/p&gt;

&lt;p&gt;But this time was different. This time we felt like seasoned veterans of this lifestyle. In the last week, we’d travelled across roadless mountain passes, traversed the treacherous Northern Mongolian landscape on horseback to visit the Tsaatan peoples. We’d had our prophecies told during a life-altering Shamanic ritual, witnessed a bear-attack that killed one of Godla’s reindeer. We’d participated in a seasonal reindeer herding migration, and lived in orts underneath the stars. We’d butchered a sheep, watched a backyard horse branding, and rode reindeer.&lt;/p&gt;

&lt;p&gt;So in comparison to all of that, we felt comfortable with Handa and her family. I played a friendly game of 13 while sipping kumis. Kourtney helped herd the baby cows. Then we stirred the newest milk contribution into the massive cow skin bag.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 60%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/14/__2023-09-10_07-38-37-922000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/14/__2023-09-10_07-38-37-922000_evan.webp&quot; alt=&quot;To transform the fresh horse milk into kumis, the batch is whisked vigorously with a wooden stick for an hour continuously. Over this time, yeast alcoholizes and carbonates the milk while Lactobacillus acidifies it.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    To transform the fresh horse milk into kumis, the batch is whisked vigorously with a wooden stick for an hour continuously. Over this time, yeast alcoholizes and carbonates the milk while Lactobacillus acidifies it.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p&gt;Although we felt we were fitting in, that’s not to say we’re fit for this lifestyle. I cannot overstate how tremendously hard nomadic life is compared to most Western lifestyles. This is a non-controversial opinion for any “Westerner” who has spent as little time here as we have. Case in point: Handa’s daily summer routine. Every two hours she milks the horses. From 5am to 9pm. And at the end of the day, she needs to mix the new milk with the forever soup of kumis by stirring it vigorously for 90 minutes continuously. This helps incorporate the yeast and kickstarts the fermentation. Needless to say, she was visibly happy that Baagii, Kourtney, Anuka and I took shifts so she could rest her body.&lt;/p&gt;

&lt;p&gt;Late in the night I got a bad case of the dairy trots from the kumis that I was so confidently sipping earlier in the night. A further reminder of my un-Mongolianness.&lt;/p&gt;

&lt;h2 id=&quot;day-15-home&quot;&gt;Day 15: Home&lt;/h2&gt;

&lt;p&gt;On our final day together, we visited Khustai National Park, where we witnessed herds of the only truly wild horses left on earth. We spotted around 40 of them, representing around a tenth of the entire population.&lt;/p&gt;

&lt;p&gt;As dusk fell we began our final journey back to Ulaanbaatar.&lt;/p&gt;

&lt;p&gt;“What would Baagii do for work if he moved to Canada?” I wondered aloud as we drove, trying to imagine this horseman, herder, and all-around handyman transplanted into our world.&lt;/p&gt;

&lt;p&gt;“Butcher,” we all agreed after some discussion. He would excel.&lt;/p&gt;

&lt;p&gt;“And he could live with us!” Kourtney and I suggested enthusiastically.&lt;/p&gt;

&lt;p&gt;Anuka translated, then added with laughter, “And pick the meat off the bones to eat for free!” referencing our conversation from days earlier. Through the rearview mirror, I watched Baagii’s eyes crinkle with amusement.&lt;/p&gt;

&lt;p&gt;The landscape gradually transformed as we approached the capital. Gers and petrol stations gave way to taller buildings and metropolitan development. Neon signs in Korean and Cyrillic punctuated the darkness, advertising restaurants and karaoke bars.&lt;/p&gt;

&lt;p&gt;After maybe an hour navigating, we finally arrived at the hotel where our journey had begun two weeks earlier. The symmetry felt right, somehow. We had traveled in a great circle through Mongolia, and here we were all over again.&lt;/p&gt;

&lt;p&gt;For our final meal together, we spoiled ourselves to the Korean restaurant attached to the hotel. We sat around the table and shared our food family-style, without the awkwardness that had characterized our first meal together.&lt;/p&gt;

&lt;p&gt;Back in Anuka’s apartment, surrounded once again by the trappings of modern comfort, I felt a strange displacement. Part of me still wished to be in the Taiga, still riding across boggy terrain, still waking to the sound of reindeer hooves outside an ort.&lt;/p&gt;

&lt;p&gt;I thought about the word “nomad”—how it implies constant movement, no fixed home. Yet the nomads we’d met seemed more rooted, more connected to their place in the world than people with permanent addresses, myself included. What, then, did home mean to them? I couldn’t pretend to know, as I’d only glimpsed fragments. But perhaps the answer can be found somewhere within Handa’s unquestioning hospitality to strangers. Or in how Lhagwa’s family arranged their treasured medals on cabin walls they’d soon leave behind. Or in how the Eastern reindeer herders dismantled their entire camp in one morning. Maybe it was in Godla’s words when asked what he loved about Mongolia: “We can live with our animals and do whatever we want”.&lt;/p&gt;

&lt;p&gt;Whatever the case, I realized that Anuka and Baagii had given Kourtney and I a profound gift: they had made Mongolia feel, however briefly, like home to us. Not by chaperoning us through the “must-see” attractions of Mongolia, but by providing us an unadulterated lens into Mongolian culture.&lt;/p&gt;

&lt;p&gt;Indeed, as I reflect on this trip now, nearly two years later, it wasn’t the spectacular landscapes or exotic experiences that I regularly think of, but the simple moments centered around people: Baagii’s laughter when I mistakenly reached to shake his blood-covered hand during the sheep butchering. Or the way that Anuka looked up at Godla, admiringly, and in a hushed whisper remarked to me how powerful he seemed. Or the bunch of us in Lhagwa’s cabin, huddled around a card game underneath a flickering lightbulb.&lt;/p&gt;

&lt;p&gt;Mongolia revealed itself to us as a way of being proud, generous, and profoundly connected to what matters most. And for that lesson–to Anuka, Baagii, Lhagwa, Godla, Sartlong, Handa, Buyantogtokh, Batsur, and everyone else who welcomed us into their lives–no amount of &lt;em&gt;bayarlala&lt;/em&gt; could ever be enough.&lt;/p&gt;

&lt;figure class=&quot;photo-fig&quot; style=&quot;width: 100%;&quot;&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/ekiefl/images/main/images/2023-09-14-north-mongolia/15/__2023-09-11_01-18-49-358000_evan.webp&quot; target=&quot;_blank&quot; class=&quot;nohover&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/ekiefl/images/main/thumbnails/2023-09-14-north-mongolia/15/__2023-09-11_01-18-49-358000_evan.webp&quot; alt=&quot;Kourtney, Baagii, Anuka, and I share a final countryside meal together, talking about relationships, marriage, and what makes a good life.&quot; loading=&quot;lazy&quot; style=&quot;display: block; margin: auto; width: 100%;&quot; /&gt;
  &lt;/a&gt;

  
  &lt;figcaption class=&quot;photo-caption&quot;&gt;
    Kourtney, Baagii, Anuka, and I share a final countryside meal together, talking about relationships, marriage, and what makes a good life.
  &lt;/figcaption&gt;
  
&lt;/figure&gt;

&lt;p class=&quot;notice&quot;&gt;If you want to travel to Mongolia, we would highly recommend getting in touch with Anuka. Her website is &lt;a href=&quot;https://www.reindeertoursmongolia.com/&quot;&gt;here&lt;/a&gt; and her Instagram is &lt;a href=&quot;https://www.instagram.com/reindeer_mongolia/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2025/03/09/north-mongolia/&quot;&gt;Northern Mongolia: a journey into the Taiga&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on March 09, 2025.&lt;/p&gt;

  </content>
</entry>


<entry>
  <title type="html"><![CDATA[Pooltool is alive and well]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2023/05/03/update/" />
  <id>https://ekiefl.github.io/2023/05/03/update</id>
  <published>2023-05-03T00:00:00+00:00</published>
  <updated>2023-05-03T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    
&lt;h2 id=&quot;its-been-a-while&quot;&gt;It’s been a while&lt;/h2&gt;

&lt;p&gt;It’s been almost 2 years since my last update on pooltool. Usually that’s an indication that the project is dead, but I’m here doing CPR. In fact, despite the radio silence, I’ve been working on pooltool alongside what has been an eventful 2 years with finishing my PhD, starting a new job, moving countries, yada, yada, yada. During that time I could never find the energy to give a formal update on the project, but that’s coming to an end right now!&lt;/p&gt;

&lt;p&gt;Since it’s been so long, there’s really no hope of giving any satisfyingly detailed report with commit hashes, correlating features with code snippets, etc. Instead I’m keep things relaxed with a devlog update. Please enjoy:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/dkH8vPI-rds&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;


    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2023/05/03/update/&quot;&gt;Pooltool is alive and well&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on May 03, 2023.&lt;/p&gt;

  </content>
</entry>


<entry>
  <title type="html"><![CDATA[Pooltool just underwent a massive graphics overhaul]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2021/10/26/graphics/" />
  <id>https://ekiefl.github.io/2021/10/26/graphics</id>
  <published>2021-10-26T00:00:00+00:00</published>
  <updated>2021-10-26T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    
&lt;h2 id=&quot;backstory&quot;&gt;Backstory&lt;/h2&gt;

&lt;p&gt;In the &lt;a href=&quot;https://ekiefl.github.io/2021/04/17/going-3d/&quot;&gt;last&lt;/a&gt; post I turned pooltool from a code-based simulation into a 3D-rendered interactive game. In this post I extend this direction of the project by transitioning from primitive cartoon graphics to something a bit more realistic.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/before_after.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/before_after.gif&quot; alt=&quot;before_after&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sitting down to write this post has made me realize just how far down the rabbit hole I am. When I first started pooltool, I never even expected it to be rendered in 3D, let alone with eye-catching graphics. Fast forward to now, and &lt;strong&gt;I’ve inadvertently produced a video game&lt;/strong&gt; that’s almost ready for an alpha release. Honestly, I don’t even know how I ended up here, but it’s been a pleasure to let the project evolve so organically.&lt;/p&gt;

&lt;p&gt;Before this post, pooltool was full of primitive, textureless, and ugly assets, and I wanted to change that.&lt;/p&gt;

&lt;p&gt;I found assets online, but none of them fully suited my needs. I wanted full control, which meant I would have to learn some basic 3D modeling. And so began my journey into the amazing open source modeling software, &lt;a href=&quot;https://www.blender.org/&quot;&gt;Blender&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;cue&quot;&gt;Cue&lt;/h2&gt;

&lt;p&gt;To start out, I decided that modeling a cue would be relatively easy because of its simple geometry. After countless youtube tutorials, I ended up with the following model.&lt;/p&gt;

&lt;div style=&quot;display: block; position: relative; width: 100%; height: 0; padding-top: 56.25%;&quot;&gt;
    &lt;iframe style=&quot;display: block; position: absolute; left: 0; top: 0; right: 0; bottom: 0; width: 100%; height: 100%;&quot; src=&quot;https://sketchfab.com/models/3664838f0b324d089b998bb278091251/embed&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Then, at the cost of some casualties, I managed to get it into the scene.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/cue_model.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/cue_model.gif&quot; alt=&quot;cue_model&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
(&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/0d544244e953b9cb1cc50f8c3e2795b706d3b771&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;h2 id=&quot;table&quot;&gt;Table&lt;/h2&gt;

&lt;p&gt;Taking a big leap in complexity, I started a several week process of trying to model a pool table, which involved many iterations.&lt;/p&gt;

&lt;p&gt;As a practice round, I just tried to freestyle something that looked like a pool table, without trying to adhere to specific shapes and dimensions. What I ended up with was not half bad in my opinion.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_1.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_1.png&quot; alt=&quot;table_1&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
(&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/74430d320b11e9bcf6a2f10c61eaf995b633817c&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;With that under my belt, I iterated the process, this time with physically accurate measurements. I thought it would be most fun to use my own pool table as a reference, so I took about 30 reference photos. To get an idea, here is a few of them.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/refs.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/refs.png&quot; alt=&quot;refs&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next day I had an 8-hour flight, which I spent every minute of modeling. By the end of the flight, I had a physically accurate representation of my pool table inside pooltool.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_2.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_2.png&quot; alt=&quot;table_2&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
(&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/828a497bfc987378a80f1a677508dea699a9a49e&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Soon after that I figured out how to apply some UV textures.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_3.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_3.png&quot; alt=&quot;table_3&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
(&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/490e4c1b02b226d61ff774f7c79b445e89a6f555&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;At this point, it should be kept in mind that the simulation is completely disjoint from the model. For example, the simulation has its own set of parameters that define the cushion geometry, and I’m rendering this geometry using simple grey lines.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_4.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_4.png&quot; alt=&quot;table_4&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can tell, they don’t line up that well with the model. To make sure they do, I created a INI formatted file that specifies all of the model’s geometry in terms of the parameters used in the simulation. It looks like this:&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;[7_foot]
type = pocket
model = models/table/7_foot.glb
table_length = 1.9812
table_width = 0.9906
table_height = 0.708
cushion_width = 0.0508
cushion_height = 0.036576
corner_pocket_width = 0.118
corner_pocket_angle = 5.3
corner_pocket_depth = 0.0398
corner_pocket_radius = 0.0612
corner_jaw_radius = 0.02095
side_pocket_width = 0.137
side_pocket_angle = 7.14
side_pocket_depth = 0.00437
side_pocket_radius = 0.0645
side_jaw_radius = 0.00795
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;These parameters define the exact cushion geometry for the model. If I add more models in the future, I would simply add their geometrical parameters to this file and pooltool would be able to create the simulation geometry from these parameters.&lt;/p&gt;

&lt;p&gt;Now, the simulation geometry matches the model geometry nearly perfectly.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_5.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/table_5.png&quot; alt=&quot;table_5&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;room&quot;&gt;Room&lt;/h2&gt;

&lt;p&gt;I wanted pooltool to be an immersive experience, so I decided to create a room. I saved this for last because it is the most involved. Thanks to Kourtney’s interior design skills, I think it turned out really well.&lt;/p&gt;

&lt;p&gt;Here is an in-game screenshot of the room when it was about 75% finished.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/room_1.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/room_1.png&quot; alt=&quot;room_1&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
(&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/3f9268ea737d76e1ad446f6b0b8d943eb5d7a1a5&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;And here is an interactive model of it fully completed:&lt;/p&gt;

&lt;div style=&quot;display: block; position: relative; width: 100%; height: 0; padding-top: 56.25%;&quot;&gt;
    &lt;iframe style=&quot;display: block; position: absolute; left: 0; top: 0; right: 0; bottom: 0; width: 100%; height: 100%;&quot; src=&quot;https://sketchfab.com/models/e85a4318aa6d4a139e468a9d05fbfbc8/embed&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;If you look around, you should be able to spot Dexter.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/dexter.png&quot; class=&quot;center-img width-50&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/dexter.png&quot; alt=&quot;dexter&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is actually a super realistic pencil crayon drawing we have in our apartment. The artist is &lt;a href=&quot;https://www.renaissancepup.com/&quot;&gt;&lt;em&gt;Renaissance Pup&lt;/em&gt;&lt;/a&gt;. All of the other art in the room is in the public domain.&lt;/p&gt;

&lt;h2 id=&quot;balls&quot;&gt;Balls&lt;/h2&gt;

&lt;p&gt;Then I turned my attention to the pool balls, which thus far have been black blobs.&lt;/p&gt;

&lt;p&gt;To give my game a unique feel, I designed my own ball set. The strategy is to first design an image for each ball that will eventually get wrapped onto a sphere. This is called a UV image and here is the collection of UV images I created.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/ball_uvs.png&quot; class=&quot;center-img width-50&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/ball_uvs.png&quot; alt=&quot;ball_uvs&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re curious, the font is &lt;em&gt;ITF Devanagari Marathi&lt;/em&gt;. Using Blender, these images can be wrapped around spheres which is how all of the ball models are made. For example, here is the 10-ball.&lt;/p&gt;

&lt;div style=&quot;display: block; position: relative; width: 100%; height: 0; padding-top: 56.25%;&quot;&gt;
    &lt;iframe style=&quot;display: block; position: absolute; left: 0; top: 0; right: 0; bottom: 0; width: 100%; height: 100%;&quot; src=&quot;https://sketchfab.com/models/7fc404bd19104df89695ccc3b38ca9a9/embed&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Eventually I’ll have to model more balls for games like 3-cushion. But for now this is good.&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;Want these files? SVG, PNG, BLEND, and GLB file formats for all of these balls can be found &lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/main/models/balls/set_1&quot;&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;optimizing&quot;&gt;Optimizing&lt;/h2&gt;

&lt;p&gt;After adding the balls in, this was the state of things:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/status_1.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/status_1.png&quot; alt=&quot;status_1&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
(&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/34858c7e61fb3dede1abded6b3dd2af3c24a05b9&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;It looks good, but I was starting to think the table textures looked kind of bad. So I went to &lt;a href=&quot;www.ambientcg.com&quot;&gt;ambientCG&lt;/a&gt;, a database of public domain physical-based rendering (PBR) materials, and replaced the wood and cloth with textures I thought looked more realistic.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/status_2.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/status_2.png&quot; alt=&quot;status_1&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
(&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/dac618d55bff804afcdc2feb2880763007423be8&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Then I did the same thing with the floors. Also also replaced one of the walls with brick, and added some trim so the contrast between floor and wall wasn’t so stark.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/status_3.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/status_3.png&quot; alt=&quot;status_3&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
(&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/c21e8a66e564ade11d155e34ce715f00a7b2d660&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;I also added some lights under the bar to add a nice ambience.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/status_4.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/status_4.png&quot; alt=&quot;status_4&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;performance&quot;&gt;Performance&lt;/h2&gt;

&lt;p&gt;With PBR materials, lights, and shadows, performance is starting to become an issue on my poor macbook. So I created a little INI file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/settings&lt;/code&gt; (introduced in &lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/271f652b71fb82a125caa54de1a8064fafa2d126&quot;&gt;this commit&lt;/a&gt;) that can be used to toggle various graphical settings. Right now you have the option to toggle the room, the floor, lights, shadows, shaders, whether PBR materials are used, and the target FPS.&lt;/p&gt;

&lt;p&gt;As much as a like the high quality graphics I’ve been illustrating, I also think &lt;strong&gt;the following settings create a great cartoony vibe&lt;/strong&gt;:&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;[graphics]
room = 0
floor = 0
lights = 0
shadows = 0
shader = 1
physical_based_rendering = 0
fps = 30
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/cartoon.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/cartoon.png&quot; alt=&quot;cartoon&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;cue-avoidance&quot;&gt;Cue avoidance&lt;/h2&gt;

&lt;p&gt;Once the graphics started to look quite realistic, I found it no longer acceptable for the cue to intersect geometry:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/cue_avoid_1.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/cue_avoid_1.png&quot; alt=&quot;cue_avoid_1&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This unrealistic behavior is in stark contrast to the graphics.&lt;/p&gt;

&lt;p&gt;To fix this, I created a routine that runs every frame and calculates the minimum cue elevation required to avoid intersecting any rails or balls. If the cue elevation is less than this, the cue is raised. While all of the collision detection is handled by built-in Panda3D capabilities, all of the collision resolution was handled by my own equations, which &lt;strong&gt;almost caused me to go insane&lt;/strong&gt;, as evidenced by my notebook during this 3 day period.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/notebook.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/notebook.png&quot; alt=&quot;notebook&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/sunny.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/sunny.jpg&quot; alt=&quot;sunny&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reason why I found this so difficult is because the cue stick doesn’t always aim at the center of the cue ball, which introduces several degrees of freedom. Nevertheless, I made it through, and the result looks good and feels intuitive.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/cue_avoid.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/cue_avoid.gif&quot; alt=&quot;cue_avoid&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All of the changes required for this feature can be found in &lt;a href=&quot;https://github.com/ekiefl/pooltool/pull/23&quot;&gt;this pull request&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;gallery&quot;&gt;Gallery&lt;/h2&gt;

&lt;p&gt;All in all, I think the game looks great now. Far better than I ever had expectations for. Here are some screenshots I’ve taken.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_1.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_1.png&quot; alt=&quot;gallery_1&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_2.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_2.png&quot; alt=&quot;gallery_2&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_3.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_3.png&quot; alt=&quot;gallery_3&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_4.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_4.png&quot; alt=&quot;gallery_4&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_5.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_5.png&quot; alt=&quot;gallery_5&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_6.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_6.png&quot; alt=&quot;gallery_6&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_7.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_7.png&quot; alt=&quot;gallery_7&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;next-up&quot;&gt;Next up&lt;/h2&gt;

&lt;p&gt;While pooltool is looking good, the physics engine needs some work. So next up on the agenda is increasing the physical realism.&lt;/p&gt;


    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2021/10/26/graphics/&quot;&gt;Pooltool just underwent a massive graphics overhaul&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on October 26, 2021.&lt;/p&gt;

  </content>
</entry>


<entry>
  <title type="html"><![CDATA[Pooltool is now an open source billiards video game rendered in 3D]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2021/04/17/going-3d/" />
  <id>https://ekiefl.github.io/2021/04/17/going-3d</id>
  <published>2021-04-17T00:00:00+00:00</published>
  <updated>2021-04-17T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    
&lt;h2 id=&quot;goal&quot;&gt;Goal&lt;/h2&gt;

&lt;p&gt;In the &lt;a href=&quot;https://ekiefl.github.io/2021/03/25/pooltool-start/&quot;&gt;last&lt;/a&gt; post I implemented a prototype that visualizes shots with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pygame&lt;/code&gt;. But my plans are much more ambitious than visualizing shots on a pixel-art table. &lt;strong&gt;My goal is to turn this simulation into an interactive, 3D game/tool&lt;/strong&gt;. That’s what this post is about.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/before_after.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/before_after.gif&quot; alt=&quot;before_after&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;my-credentials-for-making-a-game&quot;&gt;My credentials for making a game&lt;/h2&gt;

&lt;p&gt;None.&lt;/p&gt;

&lt;h2 id=&quot;game-engine-selection&quot;&gt;Game engine selection&lt;/h2&gt;

&lt;p&gt;If I was a game developer, I would have written this in C# or C++, using &lt;a href=&quot;https://unity.com/&quot;&gt;Unity&lt;/a&gt; or &lt;a href=&quot;https://www.unrealengine.com/en-US/&quot;&gt;Unreal Engine&lt;/a&gt; as my game engine. But I’m not, so I decided to use a &lt;strong&gt;game engine with Python support&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It turns out, there is only 1 option that deserves consideration: &lt;strong&gt;&lt;a href=&quot;https://www.panda3d.org/&quot;&gt;panda3d&lt;/a&gt;&lt;/strong&gt;. Fortunately, it’s by no means a bad option. Originally developed by Disney, it was the game engine for the critically acclaimed &lt;a href=&quot;https://www.youtube.com/watch?v=-CEX1DLK5WU&quot;&gt;ToonTown&lt;/a&gt; (among a few other titles), and then they later open-sourced it in a collaboration with Carnegie Mellon University for the purposes of using it as an educational tool.&lt;/p&gt;

&lt;p&gt;Today, panda3d is a fully-featured, open-source game engine that remains under active development. It has good documentation and a relatively small, yet charitable and welcoming community. I must say, I feel a small sense of home every time I visit the &lt;a href=&quot;https://discourse.panda3d.org/c/general-discussion&quot;&gt;Discourse&lt;/a&gt; and in general, I’ve had a wonderful time using panda3d so far.&lt;/p&gt;

&lt;h2 id=&quot;getting-something-going&quot;&gt;Getting something going&lt;/h2&gt;

&lt;p&gt;After successfully completing the &lt;a href=&quot;https://docs.panda3d.org/1.10/python/introduction/tutorial/index&quot;&gt;panda3d tutorial&lt;/a&gt;, I had created somewhat of a masterpiece:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/panda.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/panda.gif&quot; alt=&quot;panda&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But Michelangelo didn’t stop painting after he completed the Sistine Chapel. After some time, I managed to squeeze a pool table into the scene.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/jungle_pool.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/jungle_pool.gif&quot; alt=&quot;jungle_pool&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/8b47cc03b526a2dc442f730da2ac72c9c9465f85&quot;&gt;[Browse code]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The animation is the wrong speed, there is no camera controls, and the balls are rendered as little smiley faces. Nevertheless, pooltool is officially 3D.&lt;/p&gt;

&lt;p&gt;Next, I started working on a mouse-based camera system. To orient the camera based on mouse movements, I needed to track mouse movements. To no one’s surprise, panda3D has a mouse-watcher object called &lt;a href=&quot;https://docs.panda3d.org/1.10/python/reference/panda3d.core.MouseWatcher&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mouseWatcherNode&lt;/code&gt;&lt;/a&gt; that can query the mouse’s current position. So I wrote a class called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mouse&lt;/code&gt; that uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mouseWatcherNode&lt;/code&gt; and a &lt;a href=&quot;https://docs.panda3d.org/1.10/python/reference/panda3d.core.ClockObject&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClockObject&lt;/code&gt;&lt;/a&gt; to track changes in mouse movement over time:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ClockObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ClockObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouseWatcherNode&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tracking&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;


    &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;staticmethod&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WindowProperties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setCursorHidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;win&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;requestProperties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;staticmethod&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;WindowProperties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setCursorHidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;win&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;requestProperties&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;track&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tracking&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hasMouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_xy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getFrameTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tracking&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getMouseX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getFrameTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getMouseY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getFrameTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_xy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getFrameTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_dx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;last_x&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_x&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;last_x&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_dy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;last_y&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last_y&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;last_y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/582ac6e8d7c28d5e1646e90ac3f0bc89e0cac5e4/psim/ani/mouse.py&quot;&gt;[Browse code]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most important methods in this class are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_dx&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_dy&lt;/code&gt; which return how far and in what direction the mouse has moved since last queried.&lt;/p&gt;

&lt;p&gt;Next I setup a task called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_camera&lt;/code&gt; that’s called every single frame. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_camera&lt;/code&gt; gets the change in mouse position by calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_dx&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_dy&lt;/code&gt; and then associates changes in mouse position with changes in the camera’s position relative to a focal point.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update_camera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fine_control&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;alpha_x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dummy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_dx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;alpha_y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dummy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_dy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;45&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dummy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;alpha_x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Move view laterally
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dummy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;alpha_y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Move view vertically
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/582ac6e8d7c28d5e1646e90ac3f0bc89e0cac5e4/psim/ani/animate3d2.py#L136&quot;&gt;[Browse code]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here it is in action.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/camera_rotate.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/camera_rotate.gif&quot; alt=&quot;camera_rotate&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, I added a cue stick that follows the camera’s orientation:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/cue.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/cue.gif&quot; alt=&quot;cue&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/9869fd9d5c6f7c48aaf2b2196dcbc937723fc2e3&quot;&gt;[Browse code]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also added the ability to stroke the cue by holding ‘s’ and moving the mouse up and down:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/stroke.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/stroke.gif&quot; alt=&quot;stroke&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/9869fd9d5c6f7c48aaf2b2196dcbc937723fc2e3&quot;&gt;[Browse code]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just like in real pool where you often walk around the table to check out the angles, the camera shouldn’t be pinned to a single ball. So when you hold down ‘v’, you can around the table. And by holding down left-click, you can zoom in or out with the mouse:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/pan_and_zoom.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/pan_and_zoom.gif&quot; alt=&quot;pan_and_zoom&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/0c8d5eed54e009642e913e9c476060c09be0ce21&quot;&gt;[Browse code]&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;robust-object-oriented-design&quot;&gt;Robust, object-oriented design&lt;/h2&gt;

&lt;p&gt;If I keep moving at this speed the codebase is going to turn into spaghetti. I need to start carving out a modular, object-oriented code structure.&lt;/p&gt;

&lt;h3 id=&quot;i-mode-management&quot;&gt;(I) Mode management&lt;/h3&gt;

&lt;p&gt;The first problem I noticed is that tasks are not being properly managed.&lt;/p&gt;

&lt;p&gt;A task is just a method that’s called each frame. For example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_camera&lt;/code&gt; shown above was a task that controlled camera movement with the mouse. If you wanted, you could flatten the code complexity and have just a single task carries out all your desired functionality. For example, here’s a task that accomplishes all of the above functionality:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;master_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_rotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_pan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_stroke_position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cont&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This task delegates to respective methods and all is good. Except, depending on different scenarios, the mouse should do different things. Holding ‘s’, it should control the cue’s displacment; holding ‘left-click’, it should zoom the camera; holding ‘v’, it should pan the camera, etc. Yet in the above example, it does all of these things simultaneously. Let’s be a little more careful:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;master_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;v&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_pan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;left-click&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;s&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_stroke_position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_rotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cont&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Good, now what if I want to allow panning and zooming at the same time?&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;master_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;v&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;left-click&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_pan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;v&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_pan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;left-click&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;s&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_stroke_position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_camera_rotation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cont&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The logic is becoming more branched and complex, and the game barely does anything. Further down this ill-conceived design, this method is going to start blowing up with nested conditionals. So my solution was to define &lt;strong&gt;game modes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A game mode, or mode for short, represents a state that the game can be in. For example, the menu system is a mode. Aiming the cue stick is a mode. Viewing a shot is a mode. All modes manage their own tasks, that turn on and off depending on which mode is active. Whenever the game switches modes, an exit method is called for the current mode that tears down all of the current tasks, rendered objects, etc., and then an enter method is called for the next mode that builds up all of the new tasks, rendered objects, etc.&lt;/p&gt;

&lt;p&gt;All modes inherit from this simple base class:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;abc&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ABC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;abstractmethod&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ABC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;NotImplementedError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Child classes of Mode must have &apos;keymap&apos; attribute&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;abstractmethod&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;


    &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;abstractmethod&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mode&lt;/code&gt; inherits from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ABC&lt;/code&gt; (from the abstract class library &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;abc&lt;/code&gt;) that offers some defensive coding relating to inheritance. For example, any and all modes must possess &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exit&lt;/code&gt; methods, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@abstractmethod&lt;/code&gt; decorator ensures that. So an inheriting class without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exit&lt;/code&gt; methods can’t instantiate without error:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;NewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;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;TypeError: Can&apos;t instantiate abstract class NewMode with abstract methods enter, exit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The solution is to define &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exit&lt;/code&gt; methods:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;NewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Some more defensive coding: each mode must possess a dictionary called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keymap&lt;/code&gt; that defines actions and whether or not they are active. By defining &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keymap&lt;/code&gt; as a class attribute and setting it to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;None&lt;/code&gt;, and then complaining in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__init__&lt;/code&gt; if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keymap is None&lt;/code&gt;, it is ensured that any inheriting class will fail object instantiation unless it explicitly defines its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keymap&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;NewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;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;NotImplementedError: Child classes of Mode must have &apos;keymap&apos; attribute
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The solution is to define a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keymap&lt;/code&gt; either as a class attribute or instance attribute&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;keymap&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;s&quot;&gt;&apos;zoom&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;NewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here is a currently implemented game mode, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ViewMode&lt;/code&gt;. This refers to the mode in which the player possesses a free-form camera that can be rotated, panned, and zoomed.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/view_mode.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/view_mode.gif&quot; alt=&quot;view_mode&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ViewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CameraMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# rotate camera
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# pan the camera
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# exit to menu
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# zoom the camera
&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;relative&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;track&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;escape&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;mouse1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;mouse1-up&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;a&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;v&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;v-up&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;view_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;view_task&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;quit_task&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remove_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;view_task&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remove_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;quit_task&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;view_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;change_mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;aim&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zoom_camera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move_camera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rotate_camera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue_stick_too&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cont&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;quit_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;change_mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;menu&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;close_scene&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cont&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;zoom_camera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;s&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;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_dy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setPos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;autils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;multiply_cw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getPos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;move_camera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;dxp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dyp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_dx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_dy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;180&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;dx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dxp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dyp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;dy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dxp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dyp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.6&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getY&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotate_camera&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fine_control&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;alpha_x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fx&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_dx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;alpha_y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_dy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;90&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;alpha_x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Move view laterally
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;alpha_y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Move view vertically
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;From the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keymap&lt;/code&gt; you can see there are 4 different actions that this mode supports&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# rotate camera
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# pan the camera
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# exit to menu
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# zoom the camera
&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Whenever this mode is entered, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt; is called:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;relative&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mouse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;track&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;escape&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;mouse1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;mouse1-up&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;a&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;v&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;v-up&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;view_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;view_task&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;quit_task&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So what happens when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt; is called? First, some changes to the mouse are made. The mouse is made invisible, and set to a &lt;a href=&quot;https://docs.panda3d.org/1.10/python/programming/hardware-support/mouse-support&quot;&gt;relative mode&lt;/a&gt; where the cursor is repositioned to the center of the screen so it never moves out of the game window. Then, mouse tracking is turned on so changes in movement can be associated to camera movements. Next, the actions in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keymap&lt;/code&gt; are explicitly associated to mouse/keyboard inputs. Upon defining these key-bindings, the values in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keymap&lt;/code&gt; will be set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt; every frame depending on which keys are being pressed.&lt;/p&gt;

&lt;p&gt;And finally, 2 tasks called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view_task&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;quit_task&lt;/code&gt; are added to a panda3d built-in task manager. If you look at the guts of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view_task&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;quit_task&lt;/code&gt;, you’ll see they carry out operations depending on which keys are being pressed.&lt;/p&gt;

&lt;p&gt;Whenever this mode is exited, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exit&lt;/code&gt; is called:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remove_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;view_task&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remove_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;quit_task&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Quite simply, the two tasks that were added to the task manager in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt; are removed from the task manager in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In total, I currently have 5 game modes which can be found in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pooltool.ani.modes&lt;/code&gt; &lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/main/pooltool/ani/modes&quot;&gt;module&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To manage switching between modes and reliably carrying out build-up and tear-down operations, I wrote a &lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/c20df041fb8469ee76d0a7f2dfc5a66ef7e37d5c/pooltool/ani/animate.py#L26&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ModeManager&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ModeManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MenuMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AimMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;StrokeMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ViewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ShotMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Init every Mode class
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;MenuMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;AimMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;StrokeMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ViewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ShotMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;modes&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;s&quot;&gt;&apos;menu&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MenuMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;aim&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AimMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;stroke&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;StrokeMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;view&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ViewMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;shot&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ShotMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Store each classes current keymap as its default
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action_state_defaults&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;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;modes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action_state_defaults&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;default_state&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;modes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
                &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action_state_defaults&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;default_state&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update_keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action_state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action_state&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;task_action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keystroke&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action_state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Add action to keymap to be handled by tasks&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;accept&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keystroke&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;action_state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;change_mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exit_kwargs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;enter_kwargs&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;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;modes&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;end_mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exit_kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Build up operations for the new mode
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;modes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;modes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;enter_kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;end_mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Stop watching actions related to mode
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ignoreAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Tear down operations for the current mode
&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;modes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reset_action_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;reset_action_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action_state_defaults&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change_mode&lt;/code&gt; changes modes by carrying out build-up and tear-down operations for the respective modes. Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ModeManager&lt;/code&gt; inherits all of the modes, methods in modes can call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change_mode&lt;/code&gt; to facilitate mode switching. For example, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;quit_task&lt;/code&gt; method in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ViewMode&lt;/code&gt; changes the mode to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MenuMode&lt;/code&gt; by calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.change_mode(&apos;menu&apos;)&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;quit_task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keymap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;change_mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;menu&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;close_scene&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cont&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;ii-the-inheritance-diagram&quot;&gt;(II) The inheritance diagram&lt;/h3&gt;

&lt;p class=&quot;notice&quot;&gt;Me from the future, here. pooltool is no longer based on the following inheritance diagram, which as I’ve progressed as a developer, I’ve come to realize is strife with a lack of cohesion and an overabundance of coupling. One day I looked at the code and realized everything was spaghetti. Since this initial design, I’ve learned a lot about python software design, and have started favoring composition over inheritance and making heavy use of some more modern python features like dataclasses. I spent about a month completely refactoring the code, and I’m happy to say it’s in a much better place. All this is to say, don’t model anything after the following inheritance diagram.&lt;/p&gt;

&lt;p&gt;So that’s how mode management works. This is how &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ModeManagement&lt;/code&gt; fits into the inheritance diagram for the game:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/inheritance.jpeg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/inheritance.jpeg&quot; alt=&quot;inheritance&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Red lines show the direction of inheritance (&lt;em&gt;e.g.&lt;/em&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ViewMode&lt;/code&gt; inherits from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mode&lt;/code&gt;) and blue lines indicates composition (&lt;em&gt;e.g.&lt;/em&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interface&lt;/code&gt; makes an instances of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Table&lt;/code&gt;). On the right hand side are all of the objects like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Table&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cue&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ball&lt;/code&gt;. Each of these inherit from a base class &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Object&lt;/code&gt;, and further inherit from respective &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Render&lt;/code&gt; classes that contain all logic relating to rendering procedures. By packaging this rendering responsibility into separate classes, objects remain fully operational for non-rendered circumstances.&lt;/p&gt;

&lt;p&gt;The class that glues everything together is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Interface&lt;/code&gt;, which inherits from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ModeManager&lt;/code&gt;, and therefore can switch between game modes. It also holds instances of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Table&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cue&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ball&lt;/code&gt; objects, which brings these objects into the namespace of tasks setup by the game modes. This is critical because it enables tasks to create, modify, and destroy objects and their rendered states based off of user action.&lt;/p&gt;

&lt;h2 id=&quot;taking-a-shot&quot;&gt;Taking a shot&lt;/h2&gt;

&lt;p&gt;With a thoughtful design in place, its time to simulate shots. From the last post, I already know the simulator works, its just a matter of detecting when and how hard the player’s cue strikes the cue ball. The player gains control of the cue whenever they hold down ‘s’, which activates the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StrokeMode&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;To detect if the player hit the ball, I setup a task in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StrokeMode&lt;/code&gt; that checks whether the cue’s position is negative, which implies the cue stick moves through (intersects) the cue ball. If this happens, an impact velocity is determined from a trajectory of the cue stick’s position, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.change_mode(&apos;shot&apos;)&lt;/code&gt; is called which changes the mode to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShotMode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShotMode.enter&lt;/code&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShotMode.run_simulation&lt;/code&gt; in a parallel process that begins the simulation. While this takes place, a translucent overlay is put onto the screen to indicate that the shot is being calculated. Since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run_simulation&lt;/code&gt; is called in a parallel process, the player retains control over the camera while the shot’s being calculated.&lt;/p&gt;

&lt;p&gt;Once done, the simulation animation is played on a loop.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/first_shot.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/first_shot.gif&quot; alt=&quot;first_shot&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;cushions&quot;&gt;Cushions&lt;/h2&gt;

&lt;p&gt;It looks kind of ugly how the balls bounce off invisible walls, so I modified &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TableRender&lt;/code&gt; to render the edges of cushion segments:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/cushion_lines.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/cushion_lines.gif&quot; alt=&quot;cushion_lines&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/82cdc3bac49e87f34ae6cf9803caf1fe9e581306&quot;&gt;[Browse commit]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s not much better, but at least now I can start to visually sketch out the pockets. So I flipped back to when I drew this diagram for the &lt;a href=&quot;[second](https://ekiefl.github.io/2020/12/20/pooltool-alg/)&quot;&gt;algorithmic theory post&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/cushion_count.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/cushion_count.jpg&quot; alt=&quot;cushion_count&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, I modified &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Table.__init__&lt;/code&gt; by hardcoding in these cushion segment dimensions, which resulted in something that actually takes the appearance of a pocket billiards table:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/cushion_line2.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/cushion_line2.gif&quot; alt=&quot;cushion_line2&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[Browse commits: &lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/ffa621f2cdb13d3d0db44a241e535e00be19d66f&quot;&gt;1&lt;/a&gt; &amp;amp; &lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/589c28ca10e58a62f5c1cafc8cbaa7a562ecbcc5&quot;&gt;2&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;It’s awesome to see that I’m now at a point where the game is starting to be actually fun to play. For now I’ll ignore that the balls don’t fall into pockets. There is actually a much more pressing issue. Check this out this bug:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/problem_example.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/problem_example.gif&quot; alt=&quot;problem_example&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a little while I realized there was a gap in the detection boundary of the cushion that existed near the intersection of the two cushion segments. This figure explains the situation:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/problem_diagram.jpeg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/problem_diagram.jpeg&quot; alt=&quot;problem_diagram&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 1&lt;/strong&gt;. A depiction of the bug. Blue lines represent the edge of the cushion segments, and red lines show where the center of the ball (purple) must pass in order for a collision (the collision segments). The left-hand side shows what has led to the bug, and the right-hand side illustrates the solution: to add a circular cushion segment.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The figure illustrates, in red, where the center of the ball must cross in order for a collision to occur. However, the current implementation (on the left-hand side), has a clear gap that leads to the behavior observed. The solution is to explicitly define a circular cushion segment at the intersection between the linear cushion segments, as shown on the right-hand side.&lt;/p&gt;

&lt;p&gt;Ok, so I need to introduce the concept of a circular cushion segment, which I started toying around with here:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/circular_segment1.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/circular_segment1.gif&quot; alt=&quot;circular_segment1&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/a7d4da92c4fdbd95c9b5650c15211ea31bfaccda&quot;&gt;Browse commit&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The circular segment in the middle of the table exists as an attribute of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Table&lt;/code&gt;, and is properly rendered, but I haven’t written the code to predict ball collisions with circular cushions. I have worked out a similar situation though, during my theoretical treatment of the &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#-ball-pocket-collision-times&quot;&gt;ball-pocket collision&lt;/a&gt;, so I’m going to reuse that math… Essentially, one must solve a 4th order polynomial to determine if/when a ball collides with a circular cushion segment.&lt;/p&gt;

&lt;p&gt;Well, this was surprisingly straight forward. I’d encourage you to check out the associated commit:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/circular_segment2.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/circular_segment2.gif&quot; alt=&quot;circular_segment2&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/00d68a8d8973d58b4445b3e4b5be39c30add3461&quot;&gt;Browse commit&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;It’s kind of amazing, and easy to forget, that this is not a discretely evolved system. All of these collisions are &lt;strong&gt;predicted&lt;/strong&gt; ahead of time.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/circular_segment3.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/circular_segment3.gif&quot; alt=&quot;circular_segment3&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/16c58462324772df27181aaa6df4c256e07ee090&quot;&gt;Browse commit&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;With circular cushion segments now fully operational, I added circular segments (with 0 radius) to each of the cushion corners via the prescription shown in Figure 1. This patches up the gap in the collision segment depicted in Figure 1. The result is a properly behaving edge case:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/fixed.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/fixed.gif&quot; alt=&quot;fixed&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/88298e40410146847c0f85915c7fba16f6d100b9&quot;&gt;Browse commit&lt;/a&gt;]&lt;/p&gt;

&lt;h2 id=&quot;pockets&quot;&gt;Pockets&lt;/h2&gt;

&lt;p&gt;The elephant in the room is that there’s no pockets for the balls to fall into. Let’s change that.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/rendered_pockets.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/rendered_pockets.png&quot; alt=&quot;rendered_pockets&quot; /&gt;&lt;/a&gt;
[&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/749f2b35384a931892a08ff8529c63b7993ae4b0&quot;&gt;Browse commit&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Alright pockets exist and are rendered, but they do not yet interact with the balls. For this, I need to implement a collision event I have &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#-ball-pocket-collision&quot;&gt;previously theorized&lt;/a&gt;: the ball-pocket collision. Here is the class I came up with:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BallPocketCollision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Collision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;event_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type_ball_pocket&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state_start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state_end&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pooltool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pocketed&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;Collision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;body1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;body2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;agents&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Ball is placed at the pocket center
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;depth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;        &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;         &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;           &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;        &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;         &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;           &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;        &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;         &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;           &lt;span class=&quot;p&quot;&gt;]])&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state_end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_next_transition_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BallPocketCollision&lt;/code&gt; inherits from &lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/main/pooltool/events.py&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Collision&lt;/code&gt;&lt;/a&gt;, which is a base class I had written for several of the other collision events. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Collision&lt;/code&gt; doesn’t do much, but it does force all inheriting classes to define a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resolve&lt;/code&gt; method, which I’ve done above. Resolving this collision is very easy. I update the ball’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rvw&lt;/code&gt; state vectors so that the ball has no spin, linear velocity, or angular velocity, and then place the ball at the bottom of the pocket. For good measure, I add the ball’s ID to the pocket, so that I can track which balls are in which pockets.&lt;/p&gt;

&lt;p&gt;So now the concept of a ball-pocket collision exists. But I need to be able to predict when they happen, like I do for all of the other events. Fortunately I’ve already developed the &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#-ball-pocket-collision-times&quot;&gt;math&lt;/a&gt;, which amounts to solving a 4th order polynomial. I implemented this algebra in &lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/fe8ceeb1fcae31d4800af7b9042b81ebcaa4f475/pooltool/physics.py#L308&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;physics.get_ball_pocket_collision_time&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Besides that, I just had to add a routine to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evolution.EvolveShotEventBased.get_next_event&lt;/code&gt; that considers all ball-pocket pairs and returns the ball-pocket collision that occurs in the least amount of time (this time may in fact be $\infty$, which basically means you suck at pool):&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_min_ball_pocket_event_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Returns minimum time until next ball-pocket collision&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;dtau_E_min&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;inf&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;involved_agents&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;tuple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DummyBall&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NonObject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()])&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pooltool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nontranslating&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pockets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;dtau_E&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;physics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_ball_pocket_collision_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;radius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;mu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;u_s&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pooltool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sliding&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;u_r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;R&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtau_E&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtau_E_min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;involved_agents&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pocket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;dtau_E_min&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtau_E&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;dtau_E&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtau_E_min&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BallPocketCollision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;involved_agents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&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;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtau_E&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here is the end result:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/pockets_work.gif&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-going-3d/pockets_work.gif&quot; alt=&quot;pockets_work&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[&lt;a href=&quot;https://github.com/ekiefl/pooltool/commit/488680d1d3c4c78d2ef8952363b6b5a2a21a259f&quot;&gt;Browse commit&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;For now, the ball suddenly appears at the bottom of the pocket rather than falling into the pocket naturally. I’ll have to do something about that in the future. Perhaps a prebaked animation.&lt;/p&gt;

&lt;p&gt;Because I already had a strong class framework for events, adding pockets was easy. If you want to check out just how easy it was, check out the &lt;a href=&quot;https://github.com/ekiefl/pooltool/pull/15&quot;&gt;pull request&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;from-interaction-to-game&quot;&gt;From interaction to game&lt;/h2&gt;

&lt;p&gt;At this point, all the major mechanics are laid out. But what’s missing is the extra fluff that turns this interactive experience into an actual game. Currently there is nothing to facilitate games like 8-ball or 9-ball. No rules to tell you whether you scratched the ball, what the win condition is, whose turn it is, and so on.&lt;/p&gt;

&lt;p&gt;Well I decided I would end this blog post by filling in all of that fluff, which ended up in this &lt;a href=&quot;https://github.com/ekiefl/pooltool/pull/18&quot;&gt;huge PR&lt;/a&gt;. Now, pooltool is a primitively functioning game, with a menu, a heads-up display, different game modes, winners and losers, etc. I decided to summarize the current state of affairs in this feature video:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/aqjX0-A-YUw&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;next-up&quot;&gt;Next up&lt;/h2&gt;

&lt;p&gt;Next on the agenda is a graphics overhaul. See you then.&lt;/p&gt;

    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2021/04/17/going-3d/&quot;&gt;Pooltool is now an open source billiards video game rendered in 3D&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on April 17, 2021.&lt;/p&gt;

  </content>
</entry>


<entry>
  <title type="html"><![CDATA[Creating a billiards simulator: the transition from equations to code]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2021/03/25/pooltool-start/" />
  <id>https://ekiefl.github.io/2021/03/25/pooltool-start</id>
  <published>2021-03-25T00:00:00+00:00</published>
  <updated>2021-03-25T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    
&lt;h2 id=&quot;outline&quot;&gt;Outline&lt;/h2&gt;

&lt;p&gt;In the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/&quot;&gt;first&lt;/a&gt; and &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/&quot;&gt;second&lt;/a&gt; posts of this series, I discussed &lt;em&gt;ad nauseam&lt;/em&gt; the physics and algorithmic theory behind pool simulation. With this all now behind me, it’s time to &lt;strong&gt;take this theory to the streets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/before_after.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/before_after.gif&quot; alt=&quot;before_after&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;This project started with 2 main modules: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;engine.py&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;physics.py&lt;/code&gt;. The rationale for this design was to separate the &lt;strong&gt;physics&lt;/strong&gt; from the &lt;strong&gt;objects&lt;/strong&gt; that the physics acts on (balls, cues, cushions, etc).&lt;/p&gt;

&lt;p&gt;With this in mind, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;engine.py&lt;/code&gt; implements the shot evolution algorithm by coordinating when object states should be modified, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;physics.py&lt;/code&gt; implements the physics that provides the specific rules for how the modification should be carried out. This separation of responsibility allows different physics models to be plugged in or out at will.&lt;/p&gt;

&lt;p&gt;Though the codebase has changed dramatically since this original implementation, this central design principle has remained unchanged.&lt;/p&gt;

&lt;h2 id=&quot;ball-trajectories&quot;&gt;Ball trajectories&lt;/h2&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Want to follow along?&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;If you want to follow along, go ahead and clone the repository, and then checkout this branch&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;git clone https://github.com/ekiefl/pooltool.git
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;pooltool
git checkout f79c801_offshoot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/div&gt;

&lt;p&gt;My first goal was to start dead simple: visualize the trajectory of the cue ball that has been struck with a cue stick, assuming there are no cushions, pockets, or other balls. I implemented this in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;engine.ShotSimulation&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;Let’s peer in.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.engine&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ShotSimulation&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ShotSimulation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup_test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shot&lt;/code&gt; instantiates objects you wouldn’t be surprised to find in a pool simulation: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ball&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Table&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cue&lt;/code&gt; objects. For example, here are the attributes of the cue ball.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;vars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s&quot;&gt;&apos;m&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.170097&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s&quot;&gt;&apos;R&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.028575&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s&quot;&gt;&apos;I&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;5.555576388825e-05&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s&quot;&gt;&apos;rvw&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
     &lt;span class=&quot;p&quot;&gt;[[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.686&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.33&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
 &lt;span class=&quot;s&quot;&gt;&apos;s&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s&quot;&gt;&apos;history&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;t&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;rvw&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;s&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Of these attributes, the most important is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rvw&lt;/code&gt;, which stores the &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#what-is-the-system-state&quot;&gt;ball state&lt;/a&gt; as a $3 \times 3$ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;numpy&lt;/code&gt; array. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rvw&lt;/code&gt; is named after the 3 state vectors $\vec{r}(t)$, $\vec{v}(t)$, and $\vec{\omega}(t)$.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rvw[0,:]&lt;/code&gt; is the displacement vector $\vec{r}(t)$&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rvw[1,:]&lt;/code&gt; is the velocity vector $\vec{v}(t)$&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rvw[2,:]&lt;/code&gt; is the angular velocity vector $\vec{\omega}(t)$.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means the velocity of the cue ball is&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,:]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In other words, it’s not moving. To simulate something meaningful, energy has to be added to the system. In billiards, that’s done with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cue&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;vars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;M&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.567&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;brand&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Predator&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is a 20oz Predator–these things are expensive. Unsurprisingly, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shot.cue&lt;/code&gt; has a method for striking balls.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;?&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Docstring&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
Strike a ball
                          , - ~  ,
◎───────────◎         , &apos;          &apos; ,
│           │       ,             ◎    ,
│      /    │      ,              │     ,
│     /     │     ,               │ b    ,
◎    / phi  ◎     ,           ────┘      ,
│   /___    │     ,            -a        ,
│           │      ,                    ,
│           │       ,                  ,
◎───────────◎         ,               &apos;
  bottom rail           &apos; - , _ , -
                 ______________________________
                          playing surface
Parameters
==========
ball : engine.Ball
    A ball object
V0 : positive float
    What initial velocity does the cue strike the ball?
phi : float (degrees)
    The direction you strike the ball in relation to the bottom rail
theta : float (degrees)
    How elevated is the cue from the playing surface, in degrees?
a : float
    How much side english should be put on? -1 being rightmost side of ball, +1 being
    leftmost side of ball
b : float
    How much vertical english should be put on? -1 being bottom-most side of ball, +1 being
    topmost side of ball
File:      ~/Software/pooltool_testing/psim/engine.py
Type:      method
&quot;&quot;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p class=&quot;notice&quot;&gt;This method ultimately calls upon &lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/51552ff7704376682359059b5dbd8a093f4ded17/psim/physics.py#L102&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;physics.cue_strike&lt;/code&gt;&lt;/a&gt;, which implements the cue-ball interaction physics described &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-vi-ball-cue-interactions&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’m going to strike the cue ball with a center-ball hit $(a=0, b=0)$ straight down the table $(\phi=90)$ with the cue completely level with the table $(\theta = 0)$. I’ll use a relatively slow impact speed of $V_0 = 0.5 \, \text{m/s}$.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;90&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.686&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.33&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
       &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.877&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
       &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After calling the method, the cue ball now has a $0.88 \, \text{m/s}$ velocity in the $y-$direction. Since it was a center ball hit with no cue elevation, the ball has no spin. &lt;em&gt;i.e.&lt;/em&gt; $\vec{\omega} = \langle 0,0,0 \rangle$.&lt;/p&gt;

&lt;p&gt;At this point, no time has passed–the ball state has merely been modified according the physics of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shot.cue&lt;/code&gt; striking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.balls[&apos;cue&apos;]&lt;/code&gt;. So then &lt;strong&gt;how does the shot evolve&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Rather than implementing the event-based shot evolution algorithm I &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#continuous-event-based-evolution&quot;&gt;wouldn’t shut up about&lt;/a&gt;, I implemented some dinky discrete time evolution algorithm just to get the &lt;em&gt;ball rolling&lt;/em&gt;. It’s a for loop that increments by $50 \text{ms}$.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;q&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.05&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;physics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;evolve_ball_motion&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;u_s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;u_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;u_sp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;u_sp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;u_r&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;u_r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;psim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The workhorse is &lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/51552ff7704376682359059b5dbd8a093f4ded17/psim/physics.py#L25&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evolve_ball_motion&lt;/code&gt;&lt;/a&gt;, which calculates the new state for each time step. It delegates to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evolve_slide_state&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evolve_roll_state&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evolve_spin_state&lt;/code&gt;, all of which update the ball state according to the appropriate equations of motion.&lt;/p&gt;

&lt;p&gt;To test if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evolve_ball_motion&lt;/code&gt; and its delegates are behaving, I called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shot.start&lt;/code&gt;, which carries out the discrete time evolution and plots the ball’s trajectory over time.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/ball_traj_0.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/ball_traj_0.png&quot; alt=&quot;ball_traj_0&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Immediately after being hit, the ball is &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-4-sliding&quot;&gt;sliding&lt;/a&gt;. Yet after a short amount of time, the relative velocity converges to $\vec{0}$, which defines the transition from sliding to &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-3-rolling&quot;&gt;rolling&lt;/a&gt;. Once rolling, the ball continues to roll until it reaches the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-1-stationary&quot;&gt;stationary&lt;/a&gt; state.&lt;/p&gt;

&lt;p&gt;If a picture says a thousand words, a video says a thousand pictures. Before going any further, I needed a way to &lt;strong&gt;animate&lt;/strong&gt; shots because I’m already bored of these static plots. I wasn’t looking for perfection, I just needed something to animate trajectories. For this, I found &lt;a href=&quot;https://www.pygame.org/news&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pygame&lt;/code&gt;&lt;/a&gt;. It just celebrated its 20th anniversary, which is pretty impressive for a python package.&lt;/p&gt;

&lt;p&gt;I implemented the module &lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/f79c801_offshoot/psim/ani/animate.py&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psim.ani.animate&lt;/code&gt;&lt;/a&gt;, which animates ball trajectories using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pygame&lt;/code&gt;. For the sake of demonstration, this functionality already exists in the branch we’re using.&lt;/p&gt;

&lt;p&gt;Let’s animate the shot.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;animate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flip&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Flip orientation to be horizontal
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/ball_traj_0.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/ball_traj_0.gif&quot; alt=&quot;ball_traj_0&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s better.&lt;/p&gt;

&lt;p&gt;Getting a little more brave, I wanted to try a massé shot, which you can watch the pros do here:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/89g7sQ7zNqo&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;To achieve this effect, you have to strike down on the cue ball $(\theta)$ with a sizable amount of side-spin $(a)$.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;90&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;a&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;mf&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;animate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flip&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/ball_traj_2.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/ball_traj_2.gif&quot; alt=&quot;ball_traj_2&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And voila. Note that all of the curvature takes place in the sliding state. This is because the rolling state by &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-3-rolling&quot;&gt;definition&lt;/a&gt; has a relative velocity of $\vec{0}$, which is a requirement for curved trajectories.&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;By the way, all sliding state trajectories under the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#3-ball-with-arbitrary-spin&quot;&gt;arbitrary spin model&lt;/a&gt; take the form of a parabola. I never proved this but Dr. Dave Billiards did &lt;a href=&quot;https://billiards.colostate.edu/technical_proofs/new/TP_A-4.pdf&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, I tried to apply insane levels of massé, like this guy:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/t_ms6KjSoS8?t=29&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;Specifically, I tried to tune the parameters to remake the shot at 0:30s. After fumbling around, I ended up with these shot parameters.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.18&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.37&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;335&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;55&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;b&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;mf&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;animate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flip&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/comparison.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/comparison.gif&quot; alt=&quot;comparison&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This may not be perfect, but it’s close. Yes.&lt;/p&gt;

&lt;p&gt;Being able to recapitulate Florian’s shot allows me to answer the following question: what insane levels of spin are required to pull this shot off? Well, In RPMs, the initial rotational speed of the cue ball is&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;21&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linalg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;norm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;21&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;4374.123861245154&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;$4400$ RPM. That’s… that’s too much, right? Well, the same guy put out &lt;a href=&quot;https://www.youtube.com/watch?v=UG92u3rClhA&quot;&gt;this&lt;/a&gt; video where he purports his RPM for a different shot to be $3180$. So I’m certainly in the ball park. Maybe he can get up to $4400$ RPM, or maybe my simulated cloth had a higher coefficient of sliding friction, requiring higher RPM.&lt;/p&gt;

&lt;p&gt;Overall, these trajectories have me convinced I’m not screwing anything up royally.&lt;/p&gt;

&lt;h2 id=&quot;event-based-evolution-algorithm&quot;&gt;Event-based evolution algorithm&lt;/h2&gt;

&lt;p&gt;So far I’ve been evolving the simulation by incrementing time in small discrete steps (&lt;em&gt;aka&lt;/em&gt; a &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#discrete-time-evolution&quot;&gt;discrete time evolution algorithm&lt;/a&gt;). Yet moving forward, I’ve opted to use the event-based evolution algorithm for its superior accuracy and computational efficiency.&lt;/p&gt;

&lt;p&gt;The premise of the algorithm is this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;We have beautiful equations of motion for each ball that collectively describe the evolution of the &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#what-is-the-system-state&quot;&gt;system state&lt;/a&gt;. Great.&lt;/li&gt;
  &lt;li&gt;But &lt;strong&gt;events&lt;/strong&gt; between interfering parties (&lt;em&gt;e.g.&lt;/em&gt; a ball-ball collision) disrupt the validity of these equations, since they assume each ball acts in isolation.&lt;/li&gt;
  &lt;li&gt;Even still, the equations for each ball are valid &lt;strong&gt;up until&lt;/strong&gt; the next event.&lt;/li&gt;
  &lt;li&gt;So the algorithm works by evolving the system state directly up until the next event, at which time the event must be resolved (&lt;em&gt;e.g.&lt;/em&gt; a &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#-ball-ball-collision&quot;&gt;ball-ball collision event&lt;/a&gt; is resolved by applying the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-ii-ball-ball-interactions&quot;&gt;ball-ball interaction equations)&lt;/a&gt;, and then the process repeats itself: the next event is found and the system state is evolved up until the next event.&lt;/li&gt;
  &lt;li&gt;There’s only one way to calculate when the next event occurs: calculating the time until every single possible next event. By definition of &lt;strong&gt;next&lt;/strong&gt; event, the next event is the event that occurs in the least amount of time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p class=&quot;notice&quot;&gt;If you want an in-depth explanation on the event-based evolution algorithm, I may have created the most extensive learning resource on the topic in my &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/&quot;&gt;last post&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;implementing-transitions&quot;&gt;Implementing transitions&lt;/h3&gt;

&lt;p&gt;All events are either &lt;strong&gt;transitions&lt;/strong&gt;, or they are &lt;strong&gt;collisions&lt;/strong&gt; (details &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#2-what-are-events&quot;&gt;here&lt;/a&gt;). Since there are no collisions yet, I decided to implement the algorithm using just transition events to start. Transition events mark the transitioning of a ball from one motion state to another (&lt;em&gt;e.g.&lt;/em&gt; from &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-3-rolling&quot;&gt;rolling&lt;/a&gt; to &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-1-stationary&quot;&gt;stationary&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Let’s take a look.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Want to follow along?&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;For demo purposes, I compiled my progress into a &lt;a href=&quot;https://github.com/ekiefl/pooltool/tree/edfc866_offshoot&quot;&gt;branch&lt;/a&gt;. If you want to follow along, go ahead and check(out) it out.&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;git clone https://github.com/ekiefl/pooltool.git
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;pooltool
git checkout edfc866_offshoot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/div&gt;

&lt;p&gt;Like before, I created an instance of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShotSimulation&lt;/code&gt;, and then I set up a pre-baked system state specified by the keyword &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;straight_shot&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.engine&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ShotSimulation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup_test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;straight_shot&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, I struck down on the cue ball with bottom-left english.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.35&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;97&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;b&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;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To see what this system state looks like and how it evolves, here is the &lt;strong&gt;discrete time evolution&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;simulate_discrete_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;animate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flip&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/straight_shot_discrete.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/straight_shot_discrete.gif&quot; alt=&quot;straight_shot_discrete&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cue ball starts sliding (&lt;span style=&quot;color: red&quot;&gt;red&lt;/span&gt;) and then transitions to rolling (&lt;span style=&quot;color: green&quot;&gt;green&lt;/span&gt;). Offscreen, it transitions to stationary. Because I struck down with side english, there is a slight masse (curve) in the trajectory. In total, there are &lt;strong&gt;two transition events&lt;/strong&gt;: (1) a sliding-rolling transition event and (2) a rolling-stationary transition event.&lt;/p&gt;

&lt;p&gt;In comparison, this is what happens when the system state is evolved using the &lt;strong&gt;event-based algorithm&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup_test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;straight_shot&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.35&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;97&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;b&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;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;simulate_event_based&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;animate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flip&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/cts_1.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/cts_1.gif&quot; alt=&quot;cts_1&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There’s only 3 snapshots of the system state&lt;/strong&gt;: at the initial time, at the sliding-rolling transition time, and at the rolling-stationary transition time. That is the beauty of the event-based evolution algorithm–rather than evolving in small time steps, the system state is evolved directly to the next event. For this case, this directness has resulted in the shot evolution being compressed into just 3 system state snapshots.&lt;/p&gt;

&lt;p&gt;While algorithmically beautiful, it’s an eye-sore to look at. So I wrote a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;continuize&lt;/code&gt; method that calculates all of the intermediate system states.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup_test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;straight_shot&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.35&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;97&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;b&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;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;simulate_event_based&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;continuize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;animate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flip&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/cts_2.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/cts_2.gif&quot; alt=&quot;cts_2&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now the intermediate system states are calculated &lt;em&gt;ad hoc&lt;/em&gt; for the purposes of a nice animation. Nice.&lt;/p&gt;

&lt;h3 id=&quot;implementing-collisions&quot;&gt;Implementing collisions&lt;/h3&gt;

&lt;p&gt;The algorithm is a little boring without collisions. Let’s add some.&lt;/p&gt;

&lt;p&gt;With discrete time evolution, the only way to know if a ball-ball collision occurs is to use a collision detector to see if 2 balls are overlapping. However in the event-based algorithm, ball collisions are &lt;strong&gt;predicted&lt;/strong&gt; ahead of time by solving &lt;a href=&quot;(https://ekiefl.github.io/2020/12/20/pooltool-alg/#-ball-ball-collision-times)&quot;&gt;a quartic polynomial&lt;/a&gt; defined from the balls’ equations of motion.&lt;/p&gt;

&lt;p&gt;For everyone’s convenience, ball-ball collisions are already implemented in this demo branch, and can be turned on like so. If you’re interested, &lt;a href=&quot;https://github.com/ekiefl/pooltool/blob/6bb8fb451d964f350243268c5342c6b1c82a5c53/psim/physics.py#L14&quot;&gt;here&lt;/a&gt; is the code for solving the quartic polynomial.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ball_ball&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Running the simulation again now yields a more interesting picture.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/cts_3.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/cts_3.gif&quot; alt=&quot;cts_3&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a shame that the balls fly right off the table, so while we’re at it, let’s include ball-cushion collisions too.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ball_cushion&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p class=&quot;warning&quot;&gt;The implementation of the ball-cushion interaction in this branch is &lt;strong&gt;non-physical&lt;/strong&gt;. In fact, it’s not even close to accurate, I just wanted to add another collision event to test the algorithm. In this overly simplistic implementation, ball-cushion collisions are resolved by reversing the velocity component perpendicular to the cushion surface. Of course this is highly unrealistic–for example the cushion has no interaction with the ball’s spin. In the future, I will replace this with the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#3-han-2005&quot;&gt;(Han, 2005)&lt;/a&gt; physics model discussed previously.&lt;/p&gt;

&lt;p&gt;Now, things are really starting to take shape.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/cts_4.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/cts_4.gif&quot; alt=&quot;cts_4&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I find this to be a pretty illustrative visualization of how the event-based algorithm advances the system state through time.&lt;/p&gt;

&lt;p&gt;To me, its incredible to think that each event has been carefully chosen from the entire set of all possible next events. For example, the 3rd event is a sliding-rolling transition of the cue ball after its collision with the 8-ball. The 4th event is determined by considering all of the events in this diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/snapshot_1_2.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/snapshot_1_2.jpg&quot; alt=&quot;snapshot_1_2&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In total 15 possible events were considered, and the time until each of them was explicitly calculated. Based on the system state, it turned out that the one that physically occurs is a collision of the 8-ball (&lt;span style=&quot;color: black&quot;&gt;black&lt;/span&gt;) with the 3-ball (&lt;span style=&quot;color: red&quot;&gt;red&lt;/span&gt;), which you can see here:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/snapshot_2.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/snapshot_2.png&quot; alt=&quot;snapshot_2&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;comparing-the-algorithms&quot;&gt;Comparing the algorithms&lt;/h3&gt;

&lt;p&gt;How do these results compare to the discrete time algorithm? With discrete time, collisions are &lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/#discrete-time-evolution&quot;&gt;detected retrospectively&lt;/a&gt; by seeing if there is any overlapping geometry. This leads to an inherent inaccuracy. Sure, it can be reduced by &lt;strong&gt;decreasing the time step&lt;/strong&gt;. But how small does the timestep have to be and &lt;strong&gt;what effect does this have on performance&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;To compare the two algorithms, I moved the brown 7-ball from the previous simulation just slightly, such that the cue-ball barely grazes it when using the event-based algorithm.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/slight_graze.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/slight_graze.gif&quot; alt=&quot;slight_graze&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Any simulation evolved with a discrete time algorithm can’t possibly be considered accurate unless it can recapitulate this event.&lt;/p&gt;

&lt;p&gt;Let’s see the corresponding discrete time evolution using a timestep of 20ms.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.engine&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ball_ball&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ball_cushion&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ShotSimulation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup_test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;straight_shot&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.35&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;97&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;b&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;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;simulate_discrete_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.020&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;animate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flip&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/discrete_bad.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/discrete_bad.gif&quot; alt=&quot;discrete_bad&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wow, the cue didn’t even come close to hitting the brown 7-ball after bouncing off the cushion. And the 8-ball is supposed to collide with the red 3-ball, but missed entirely. The problem is that the collision angle between the cue and 8-ball is slightly off due to discrete time error, and the net result is that the overlapping geometry overestimates how thin the cut on the 8-ball is.&lt;/p&gt;

&lt;p&gt;So then how small must the timestep be for the sequence of events to match the event-based algorithm? To find out, I ran a series of shots evolved with smaller and smaller timesteps. For each, I considered it to be accurate if the presence and order of events matched what was found with the event-based algorithm. Here is the script:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;pandas&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;seaborn&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sns&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.utils&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.engine&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;include&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;s&quot;&gt;&apos;motion&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;ball_ball&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;ball_cushion&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Return a shot object that is ready for simulation&quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ShotSimulation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup_test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;straight_shot&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.35&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;97&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;b&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;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;is_accurate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;true_shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Tests if series of events matches event-based event history&quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;true_history&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;agents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;true_shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;event&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
                    &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_type&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ball-ball&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;ball-rail&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;agents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
               &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;event&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
               &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;true_history&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Simulate using event-based algorithm and store time taken
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cts_shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TimeCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cts_shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;simulate_event_based&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cts_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Init a dict that stores discrete time simulation stats
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;dt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;accurate&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Run many discrete time simulation with decreasing timestep
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logspace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;4.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TimeCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;simulate_discrete_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;dt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;accurate&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;accurate&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_accurate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cts_shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;inaccurate&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DataFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;sns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scatterplot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;dt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;accurate&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;dt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;dt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cts_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cts_time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;event-based&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;green&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xscale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;log&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;yscale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;log&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xlabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Timestep [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ylabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Calculation time [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;legend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tight_layout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/discrete_comp.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/discrete_comp.png&quot; alt=&quot;discrete_comp&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The results show that accurate simulations (&lt;em&gt;i.e.&lt;/em&gt; simulations where the events match the event-based simulation) &lt;strong&gt;require timesteps below about $250 \, \mu\text{s}$&lt;/strong&gt;. Unfortunately, this comes at a monumental speed cost. The fastest calculation time for accurate simulations was about 8s, which is about 80X slower than the compute time for the event-based algorithm (green line).&lt;/p&gt;

&lt;p&gt;This isn’t an extensive comparison between the two algorithms. But it illustrates the fundamental difference between them: except for inaccuracies arising from floating point precision, the event-based algorithm is as accurate as the underlying physics models, and performs reasonably fast. In contrast, discrete time algorithms produce an entire spectrum of accuracies, and choosing timesteps that yield sufficient accuracy typically leads to slow performance.&lt;/p&gt;

&lt;h3 id=&quot;strengths-and-weaknesses&quot;&gt;Strengths and weaknesses&lt;/h3&gt;

&lt;p&gt;The event-based algorithm has some features that may surprise people used to working with discrete time algorithms.&lt;/p&gt;

&lt;p&gt;For example, if you want to simulate how a protein folds using molecular dynamics, simulating a 1 microsecond process takes 10X longer than simulating a 100 nanosecond process, since you have to take 10X more discrete steps. In other words, a longer simulated time means a longer compute time.&lt;/p&gt;

&lt;p&gt;But for the event-based algorithm, &lt;strong&gt;simulated time has no influence on compute time&lt;/strong&gt;. A ball could roll for 10 seconds or 5 decades.&lt;/p&gt;

&lt;p&gt;Let me prove it to you. Let’s take a table that has a length equal to the earth’s circumference (and a width of 2m). Then put a ball on one end, strike it with an incoming cue speed of 10km/s, and let’s see how long the simulation takes to unfold.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.utils&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.engine&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.objects&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ball_cushion&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ShotSimulation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;40_075e3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# table as long as earth&apos;s circumference
&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Set up cue ball
&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&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;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;cue&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;89.9999&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# not quite straight down table, so it bangs into side cushions during its journey
&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;theta&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;touch_history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;p&quot;&gt;...:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TimeCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;simulate_event_based&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;✓&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Code&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;finished&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;after&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;00.736432&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It took 0.73s to simulate a shot that journeyed for…&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;17&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3600&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;17&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;8.923913780057543&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;9 hours. During which time the cue ball traveled the length of the earth and back. The reason it took so little compute time is because there were only 54 events.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;44&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;event&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;44&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;54&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So what determines the compute time is &lt;strong&gt;the number of events&lt;/strong&gt;, since that’s roughly equal to the number of computational tasks.&lt;/p&gt;

&lt;p&gt;I wanted to find out what else influences or doesn’t influence compute time. So I calculated the compute time with respect to two variables of interest: (1) size of table and (2) number of balls. To do this, I initialized random system states with varying table sizes and number of balls. In each case, the initial starting positions and velocities of each ball were randomized. Then, I evolved the system using event-based algorithm, taking note of the compute time.&lt;/p&gt;

&lt;p&gt;Here is the script.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;pandas&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;seaborn&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sns&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.utils&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.engine&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;psim.objects&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;matplotlib.colors&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LogNorm&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;include&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;s&quot;&gt;&apos;motion&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;ball_ball&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;ball_cushion&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_balls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Return a shot object that is ready for simulation

    Generates a random system state where each ball has initial speed `v`

    Parameters
    ==========
    num_balls : int, 10
        How many balls should be in the simulation? They are randomly placed.
    scale : float, 1.0
        Scale factor for how big the table should be. 1.0 Corresponds to a 9-foot table
    v : float, 1.0
        The speed each ball starts with. The direction is randomly assigned
    &quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;engine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ShotSimulation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;psim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table_width&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;psim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table_length&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Predator&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;new_ball&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Ensure no balls overlap
&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;new_ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;R&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;new_ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;R&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
                &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linalg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;norm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_ball&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Set position
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rvw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Set velocity
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;vel_angle&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;360&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;strike&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;balls&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;V0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;phi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vel_angle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sweet_spot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;touch_history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;scale&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;balls&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[],&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;ball_nums&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;scales&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linspace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball_num&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ball_nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scales&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball_num&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Measure time for event-based simulation
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball_num&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;2.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TimeCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;shot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;simulate_event_based&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;scale&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;balls&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ball_num&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Plot the results
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DataFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;sns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;heatmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pivot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;scale&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;balls&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;annot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;g&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cmap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;mako&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;norm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LogNorm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vmin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vmax&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Calculation time for event-based simulation [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This script produces a heatmap.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/event_heatmap.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-start/event_heatmap.png&quot; alt=&quot;event_heatmap&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at the heatmap, it’s clear that increasing the number of balls $(n)$ increases computation time. This is because (1) the state of every ball must be updated every time step ($\mathcal{O}(n)$ dependence on $n$), and (2) collision prediction of each ball with every other ball must be carried out every time step ($\mathcal{O}(n^2)$ dependence on $n$).&lt;/p&gt;

&lt;p&gt;You might expect that increasing the size of the table would increase computation time because there is more distance to travel. Yet keep in mind that in the event-based algorithm, collisions are predicted by solving the roots of polynomial equations. Whether the balls are far apart, close together, traveling fast, or traveling slow does not increase or decrease the computational complexity of solving the roots. The only effect these parameters have is changing the values of the coefficients. This is the power of the event-based algorithm.&lt;/p&gt;

&lt;p&gt;The real effect that table size has on computation is the number of events. More events means more computation time. And when the table size increases, the balls become more spread out, which decreases the likelihood of events. So in fact, we see a decrease in compute time with respect to table size.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;At this point in the project, I’m happy where things are. The ball trajectories match the trick shots of Florian Kohler, the event-based algorithm is working like a charm for transition events, ball-ball collisions, and ball-cushion collisions, and I have some primitive physics implementations of the ball-ball and ball-cushion interactions.&lt;/p&gt;

&lt;p&gt;But I’m sick of looking at 2D animations, and I’m sick of setting up simulations by writing code. In the next post, I’m turning pooltool into an interactive game with 3D visualization using panda3d.&lt;/p&gt;

&lt;p&gt;See you then.&lt;/p&gt;

    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2021/03/25/pooltool-start/&quot;&gt;Creating a billiards simulator: the transition from equations to code&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on March 25, 2021.&lt;/p&gt;

  </content>
</entry>


<entry>
  <title type="html"><![CDATA[Classifying dog barks for my dog-sitting software]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2021/03/14/maple-classifier/" />
  <id>https://ekiefl.github.io/2021/03/14/maple-classifier</id>
  <published>2021-03-14T00:00:00+00:00</published>
  <updated>2021-03-14T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    
&lt;h2 id=&quot;intro&quot;&gt;&lt;strong&gt;Intro&lt;/strong&gt;&lt;/h2&gt;

&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;

&lt;p&gt;This post details the step-by-step process of creating an audio classifier that tracks and responds to the behavior of my girlfriend’s dogs while we are gone. The resulting data is distilled into an interactive interface such as the one shown here.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/new_viz.html&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/new_viz.png&quot; alt=&quot;new_viz&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
[&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/new_viz.html&quot;&gt;&lt;strong&gt;Click for interactive plot&lt;/strong&gt;&lt;/a&gt;]&lt;/p&gt;

&lt;h3 id=&quot;where-i-left-off&quot;&gt;Where I left off&lt;/h3&gt;

&lt;p&gt;In the &lt;a href=&quot;https://ekiefl.github.io/2020/07/20/maple-intro/&quot;&gt;first post&lt;/a&gt; of this series, I made a virtual dog-sitter that detects audio events, responds to them, and stores them in a database that can be interactively visualized.&lt;/p&gt;

&lt;p&gt;Using this dog-sitter (&lt;a href=&quot;https://github.com/ekiefl/maple&quot;&gt;github here&lt;/a&gt;), I’ve been tracking the behavior of my girlfriend’s dog (Maple) when we leave her alone in the apartment.&lt;/p&gt;

&lt;p&gt;I ended that post saying I would report back after acquiring a lot more data and there are a few &lt;strong&gt;logistical updates&lt;/strong&gt; on that front.&lt;/p&gt;

&lt;p&gt;First, it is no longer just Maple we are tracking. We decided that Kourtney’s other dog, &lt;strong&gt;Dexter&lt;/strong&gt; (pictured above), should also be in the room. Originally we separated them based on anecdotal evidence that they barked more when together than when separated. As the data in this post will tell us, keeping them together is a &lt;strong&gt;very good idea&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Second, we don’t lock Maple in the crate anymore. This way she can interact and play with Dexter, and pick up new hobbies like destroying our bedroom door (more on that later).&lt;/p&gt;

&lt;p&gt;Finally, there were some complications, which means &lt;strong&gt;I don’t have as much data as I wanted&lt;/strong&gt;. Highlights include: I accidentally deleted about a month of data, the volume knob on my microphone changed and went unnoticed for a month, and my calibration system for event detection kept event rates inconsistent across sessions. All of this is sorted out now.&lt;/p&gt;

&lt;p&gt;So there isn’t enough data to do a comprehensive analysis on the dogs’ improvement over time, and whether or not praising/scolding has an effect on their behavior. Not yet anyways.&lt;/p&gt;

&lt;h3 id=&quot;creating-an-audio-classifier&quot;&gt;Creating an audio classifier&lt;/h3&gt;

&lt;p&gt;While I wait another couple months for the data to pour in, I decided that being able to classify audio events could greatly increase the accuracy and capabilities of the dog-sitter.&lt;/p&gt;

&lt;p&gt;In the last post I &lt;a href=&quot;https://ekiefl.github.io/2020/07/20/maple-intro/#praise&quot;&gt;developed flowcharts&lt;/a&gt; to determine when the dog-sitter should intervene with pre-recorded audio that either praises or scolds the dog. The logic relied on the assumption that &lt;strong&gt;loud is bad and quiet is good&lt;/strong&gt;. Yet if I could classify each audio event, I could step away from this one dimensional paradigm and begin developing more nuanced responses that understand the sentiment of the audio.&lt;/p&gt;

&lt;p&gt;For example, whining could trigger a console response. Playing could trigger an encouragement response. Implementing this with the old paradigm would be impossible, because whining and playing produce similar levels of noise.&lt;/p&gt;

&lt;p&gt;So that’s one thing an audio classifier would enable: &lt;strong&gt;nuanced response&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Another thing it would enable is &lt;strong&gt;protection against non-dog audio&lt;/strong&gt;. The dog-sitter may detect undesirable noise from nondescript sources, like a nearby lawnmower, a person talking in the alley, a dump truck beeping. If I train a good classifier, I could filter out non-dog audio from ever entering my databases. This would filter bad data out of downstream analyses, and prevent any mishaps like scolding a dog because of a lawnmower.&lt;/p&gt;

&lt;p&gt;And finally, creating a classifier would greatly improve the ability to understand what happens when I leave. After returning home, I would be able to immediately assess in a highly-resolved manner how well the dogs behaved while I was gone.&lt;/p&gt;

&lt;h3 id=&quot;deciding-on-a-classifier-algorithm&quot;&gt;Deciding on a classifier algorithm&lt;/h3&gt;

&lt;blockquote&gt;
Don&apos;t make anything more complicated than it needs to be.
&lt;/blockquote&gt;

&lt;p&gt;In my opinion, this quote should be attributed to the &lt;a href=&quot;https://en.wikipedia.org/wiki/Random_forest&quot;&gt;random forest algorithm&lt;/a&gt;. There is really no point starting with any other classifying algorithm. If the classifier is bad, I’ll try something more complicated.&lt;/p&gt;

&lt;p&gt;Moving on.&lt;/p&gt;

&lt;h2 id=&quot;section-i-data-transformation&quot;&gt;&lt;strong&gt;Section I&lt;/strong&gt;: Data transformation&lt;/h2&gt;

&lt;p&gt;Unfortunately, I can’t just throw the audio data into a random forest and be done with it. The data needs to be wrangled and transformed into a suitable format that’s consistent across samples and pronounces distinguishable features.&lt;/p&gt;

&lt;p&gt;Moving forward, I’ll use these two events as examples.&lt;/p&gt;

&lt;p&gt;The first is a door scratch:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.png&quot; alt=&quot;door_scratch&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.txt&quot;&gt;[click for raw text data]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second is a whine:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine.png&quot; alt=&quot;whine&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine.txt&quot;&gt;[click for raw text data]&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Using the maple API&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;I found these events using an interactive plot generated with&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;./main.py analyze &lt;span class=&quot;nt&quot;&gt;--session&lt;/span&gt; 2021_02_13_19_19_33
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

  &lt;p&gt;Once I found these two events, I plotted and exported the audio/text files using the &lt;a href=&quot;https://github.com/ekiefl/maple/blob/2be825f868942493c2d695c7c7a30de6e2d21300/maple/data.py#L197&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SessionAnalysis&lt;/code&gt;&lt;/a&gt; class in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maple.data&lt;/code&gt; module:&lt;/p&gt;

  &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.data&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;data/sessions/2021_02_13_19_19_33/events.db&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot_audio_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;160&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save_event_as_wav&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;160&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;door_scratch.wav&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save_event_as_txt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;160&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;door_scratch.txt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot_audio_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;160&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;xlim&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;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;4.53&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save_event_as_wav&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;689&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;whine.wav&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save_event_as_txt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;689&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;whine.txt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/div&gt;

&lt;p&gt;For anyone who doesn’t work with audio files, the data is simple. Audio signal is just a 1D array, where the value of each point is proportional to the pressure/strength of the sound. As you move through the array, you’re moving through time at a specified sampling rate. In my recordings I chose a standard sampling rate of 44100 Hz.&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;Why 44100 Hz? The human ear can perceive sound waves up to around 20,000 Hz. But resolving a sinusoidal wave requires sampling frequency that is &lt;a href=&quot;https://en.wikipedia.org/wiki/Nyquist_rate&quot;&gt;at least double&lt;/a&gt; the wave’s frequency. Hence we have 44100 Hz.&lt;/p&gt;

&lt;h3 id=&quot;denoising&quot;&gt;Denoising&lt;/h3&gt;

&lt;p&gt;There are some sources of noise that I have to contend with. Sometimes I leave a (non-rotating) fan on in the room that the mic picks up. Second, there exists some inherent hiss from the microphone. Since both of these noise sources are static (non-dynamic), applying some simple spectral gating can significantly reduce noise. I did exactly this using the python module &lt;a href=&quot;https://pypi.org/project/noisereduce/&quot;&gt;noisereduce&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a demonstration of how effective the denoising is. First is raw, second is denoised.&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/noise.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/denoise.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;This denoising procedure requires a sample of the background audio, so at the start of a session recording, a 2-second audio clip of the background noise is recorded during calibration. Event audio is then denoised upon detection using this background audio sample. This means the audio used for training and classification has already been denoised.&lt;/p&gt;

&lt;h3 id=&quot;data-chunking&quot;&gt;Data chunking&lt;/h3&gt;

&lt;p&gt;Each audio event (sample) has a unique length (number of features), yet most classifiers demand that &lt;strong&gt;every sample must have the same number of features&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To deal with this, I decided each audio event would be split into &lt;strong&gt;equal sized chunks&lt;/strong&gt; and the classifier would classify chunk-by-chunk.&lt;/p&gt;

&lt;p&gt;What length of time should each chunk be? I didn’t want to exclude any events that were shorter than the chunk size, so I took a look at the distribution of event lengths for some random session.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.data&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;data/sessions/2021_02_13_19_19_33/events.db&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;t_len&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bins&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;yscale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;log&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xlabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;event length [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/event_lengths.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/event_lengths.png&quot; alt=&quot;event_lengths&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It seems most event times are less than 2.5 seconds, and events are most frequently seen between 0.3s and 0.5s. To make sure events are composed of at least one chunk, &lt;strong&gt;I opted to use a chunk size of 0.25s&lt;/strong&gt;. Alternatively I could have picked a larger chunk size and zero-padded shorter events such that they contained at least one chunk.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;The minimum event length time&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;If you’re wondering why there are no events lower than 0.33s, it has to do with the event detection heuristics. If I used different values, by editing the &lt;a href=&quot;https://github.com/ekiefl/maple/blob/master/config&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config&lt;/code&gt;&lt;/a&gt; file, the minimum event length would have been different. Here are the settings I used for this session (and all sessions):&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;[detector]
# Standard deviations above background noise to consider start an event
event_start_threshold = 4
# The number of chunks in a row that must exceed event_start_threshold in order to start an event
num_consecutive = 4
# Standard deviations above background noise to end an event
event_end_threshold = 4
# The number of seconds after a chunk dips below event_end_threshold that must pass for the event to
# end. If during this period a chunk exceeds event_start_threshold, the event is sustained
seconds = 0.25
# If an event lasts longer than this many seconds, everything is recalibrated
hang_time = 180
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/div&gt;

&lt;h3 id=&quot;frequency-or-time-answer-both&quot;&gt;Frequency or time? Answer: both&lt;/h3&gt;

&lt;p&gt;I want to represent the data so that distinguishable features are accentuated for the classifier. Thinking in these terms, audio has two really important features: time and frequency.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.png&quot; alt=&quot;door_scratch&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Above is the 1D audio signal for the door scratching event. This plot basically illustrates how &lt;strong&gt;energy is spread along the axis of time&lt;/strong&gt;. And there is clearly distinguishing information in this view. For example, we can see that my door is being destroyed in discrete pulses that are roughly equally spaced through time.&lt;/p&gt;

&lt;p&gt;But frequency-space also holds critical information. We can visualize how &lt;strong&gt;energy is spread across the axis of frequency&lt;/strong&gt; by calculating a Fourier transform of both events:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.data&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;data/sessions/2021_02_13_19_19_33/events.db&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot_audio_event_freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;160&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot_audio_event_freq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;689&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch_freq.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch_freq.png&quot; alt=&quot;door_scratch_freq&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine_freq.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine_freq.png&quot; alt=&quot;whine_freq&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;You can really see how my door is being destroyed over a wide range of frequencies. If you listen carefully to the audio, you can hear the bassline and the high hat. In comparison, Maple’s whine is much more concentrated in frequency space, localized to a range of about 500-2500 Hz. This is reflected in the relative pureness of the audio, giving that whistley sort of sound.&lt;/p&gt;

&lt;p&gt;Clearly, time and frequency are giving complementary information that the classifier should use both of. To accomplish this, I decided to transform the data into &lt;strong&gt;spectrograms&lt;/strong&gt;, where the $x-$axis is time and the $y-$axis is frequency. This shows how &lt;strong&gt;energy is spread across time and frequency space&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.data&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;data/sessions/2021_02_13_19_19_33/events.db&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot_audio_event_spectrogram&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;160&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot_audio_event_spectrogram&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;689&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch_spectrogram.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch_spectrogram.png&quot; alt=&quot;door_scratch_spectrogram&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine_spectrogram.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine_spectrogram.png&quot; alt=&quot;whine_spectrogram&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p class=&quot;notice&quot;&gt;Note that I log-transformed the signal with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log=True&lt;/code&gt; so that low values are visualizable.&lt;/p&gt;

&lt;p&gt;This new view simultaneously illustrates distinguishing features in both time- and frequency-space, and the results are very promising. Look at how different the spectrograms look! And they are very informative. In the whine event, you can really see Maple’s pitch starts high and ends low, which perfectly matches the perceived audio.&lt;/p&gt;

&lt;p&gt;Keep in mind that I’ve shown the spectrograms for the &lt;strong&gt;entire&lt;/strong&gt; audio event. But the plan is to slice each audio event into 0.25 second samples, and then calculate spectrograms for each. This ensures each sample has the same number of features (length).&lt;/p&gt;

&lt;p&gt;As seen, a spectrogram is a 2D matrix of data, but a classifier expects samples that are 1D. I solve this by flattening each spectrogram by joining all the rows of data into one long 1D array. For example, here is what the &lt;strong&gt;first chunk&lt;/strong&gt; of the whine event ultimately looks like to the classifier:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.data&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.audio&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;get_spectrogram&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;data/sessions/2021_02_13_19_19_33/events.db&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;subevent_audio&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_subevent_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;689&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent_time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.25&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;S&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;get_spectrogram&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flatten&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;S&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;#165BAA&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ylabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Signal amplitude [16 bit]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xlabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Flattened axis&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine_flattened.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine_flattened.png&quot; alt=&quot;whine_flattened&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This may not look as informative as the 2D heatmaps shown above, but it contains exactly the same information.&lt;/p&gt;

&lt;h3 id=&quot;transformation-summary&quot;&gt;Transformation summary&lt;/h3&gt;

&lt;p&gt;A brief summary is in order.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Audio clips are denoised with spectral gating to remove static noise&lt;/li&gt;
  &lt;li&gt;Audio clips are segmented into 0.25 second chunks and will be classified chunk-by-chunk&lt;/li&gt;
  &lt;li&gt;Each chunk is transformed into a spectrogram to accentuate the most important qualities of sound: amplitude with respect to time, and amplitude with respect to pitch.&lt;/li&gt;
  &lt;li&gt;The spectrograms are flattened into 1D arrays for the sake of the classifier&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;section-ii-labeling&quot;&gt;&lt;strong&gt;Section II&lt;/strong&gt;: Labeling&lt;/h2&gt;

&lt;p&gt;The random forest algorithm requires training data. I considered two options for obtaining training data:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Use a database of labelled dog audio, assuming such a dataset exists freely on the internet somewhere.&lt;/li&gt;
  &lt;li&gt;Manually label a subset of the audio events I have recorded over the last several months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ultimately, I decided on option 2, but option 1 is not without merits.&lt;/p&gt;

&lt;p&gt;First of all, option 1 doesn’t require manual labeling, which is a huge investment of time. Second, such a broad database would presumably have data recorded under a multitude of audio settings and for a multitude of dogs, which is good and bad. It’s good because it makes the classifier more suitable for productionalization for when different mic settings, room acoustics, and dogs would in theory be used. But it also is bad because accuracy will suffer in comparison to a dataset trained specifically on Maple and Dexter with consistent mic settings and acoustic environments.&lt;/p&gt;

&lt;p&gt;Ultimately this is what swayed me to option 2. Additionally, manually labelled data reduces constraints because I can create whichever labels I want. For example, a pre-labeled database would be unlikely to have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scratch_door&lt;/code&gt; label, and if it did, it would be unlikely to sound like my particular door.&lt;/p&gt;

&lt;h3 id=&quot;picking-labels&quot;&gt;Picking labels&lt;/h3&gt;

&lt;p&gt;Before going through the process of manually labelling data, I needed to decide on the set of labels to use. After reviewing the audio, Kourtney and I decided on these 6 labels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whine&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At this point, you’re familiar with the whine. This is primarily a Maple special, although Dexter is also well-versed in the art.&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/whine.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;&lt;strong&gt;Howl&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The howl is a distinct escalation of the whine, exhibited exclusively by Maple.&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/howl.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;&lt;strong&gt;Bark&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bark space is dominated by Dexter. It’s characterized by its high pitch, and being miserable to listen to.&lt;/p&gt;

&lt;p class=&quot;warning&quot;&gt;You should turn your volume down.&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/bark.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;&lt;strong&gt;Play&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Maple and Dexter often play together. It may surprise you that it sounds like this:&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/play.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;Usually playing means tug of war with a toy, biting each other’s ears, or chasing each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Door scratch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one you’ve already heard. I haven’t collected any direct evidence for who is responsible for ruining my door, but I suspect it is Maple.&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/door_scratch.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This class is a catch-all for events that are not made by the dogs, or don’t fit into the other classes. For example, here is me greeting Dexter after returning home:&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/none.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;The more audio events I can label with this class, the more protection I will have against false-positives.&lt;/p&gt;

&lt;p&gt;With a set of labels, it’s time to label a boat-load of data.&lt;/p&gt;

&lt;h3 id=&quot;labeling&quot;&gt;Labeling&lt;/h3&gt;

&lt;p&gt;Manual labelling is so laborious that I wanted to streamline the process as best I could. Basically, I needed something that would subsample events from a collection of session databases, play them to me, splice them into chunks, and then store the label I attribute into a table. This is all handled by the class &lt;a href=&quot;https://github.com/ekiefl/maple/blob/b4733d66bb9ce8bf7da0636c22cca734b84a1de4/maple/classifier.py#L29&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LabelAudio&lt;/code&gt;&lt;/a&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maple.classifier&lt;/code&gt; module.&lt;/p&gt;

&lt;p&gt;I extended the command line utility to use the class with the following command:&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;./main.py label &lt;span class=&quot;nt&quot;&gt;--label-data&lt;/span&gt; label_data.txt &lt;span class=&quot;nt&quot;&gt;--session-paths&lt;/span&gt; training_session_paths
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s a demo.&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/u83qi84bUSM&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;With the labeler in hand, my initial goal was to label 10,000 audio chunks. But of the 50,621 audio chunks at my disposable, Kourtney and I were able to withstand labelling just &lt;strong&gt;2,745 audio chunks&lt;/strong&gt; before giving up. Hopefully it’s enough.&lt;/p&gt;

&lt;h2 id=&quot;section-iii-training&quot;&gt;&lt;strong&gt;Section III&lt;/strong&gt;: Training&lt;/h2&gt;

&lt;p&gt;The next step was to actually create and validate a random forest with this labelled data.&lt;/p&gt;

&lt;h3 id=&quot;class-description&quot;&gt;Class description&lt;/h3&gt;

&lt;p&gt;Basically, I needed to load in the label data, transform the audio into spectrograms, split the data into training and validation sets, build the classifier using the training set, validate the classifier with the validation set, and save the model for further use. To do this, I wrote a class called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Train&lt;/code&gt; which lives in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maple.classifier&lt;/code&gt; module. You can peruse it &lt;a href=&quot;https://github.com/ekiefl/maple/blob/2be825f868942493c2d695c7c7a30de6e2d21300/maple/classifier.py#L247&quot;&gt;here&lt;/a&gt; at your leisure if you want the full story. Here I’ll summarize some things.&lt;/p&gt;

&lt;p&gt;This class starts in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__init__&lt;/code&gt; method by loading in the label data as a dataframe.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read_csv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\t&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The label data looks like this:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;session_id&lt;/th&gt;
      &lt;th&gt;event_id&lt;/th&gt;
      &lt;th&gt;subevent_id&lt;/th&gt;
      &lt;th&gt;t_start&lt;/th&gt;
      &lt;th&gt;t_end&lt;/th&gt;
      &lt;th&gt;label&lt;/th&gt;
      &lt;th&gt;date_labeled&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_03_05_20_10_07&lt;/td&gt;
      &lt;td&gt;430&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0.0&lt;/td&gt;
      &lt;td&gt;0.25&lt;/td&gt;
      &lt;td&gt;bark&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:41:29.135011&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_03_05_20_10_07&lt;/td&gt;
      &lt;td&gt;430&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0.25&lt;/td&gt;
      &lt;td&gt;0.5&lt;/td&gt;
      &lt;td&gt;none&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:41:44.855221&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_01_19_13_59_02&lt;/td&gt;
      &lt;td&gt;39&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0.0&lt;/td&gt;
      &lt;td&gt;0.25&lt;/td&gt;
      &lt;td&gt;howl&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:42:07.451854&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_01_19_13_59_02&lt;/td&gt;
      &lt;td&gt;39&lt;/td&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;0.25&lt;/td&gt;
      &lt;td&gt;0.5&lt;/td&gt;
      &lt;td&gt;howl&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:42:08.434805&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_01_19_13_59_02&lt;/td&gt;
      &lt;td&gt;39&lt;/td&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;0.5&lt;/td&gt;
      &lt;td&gt;0.75&lt;/td&gt;
      &lt;td&gt;howl&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:42:09.842651&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_01_19_13_59_02&lt;/td&gt;
      &lt;td&gt;39&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;0.75&lt;/td&gt;
      &lt;td&gt;1.0&lt;/td&gt;
      &lt;td&gt;howl&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:42:10.959118&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_01_19_13_59_02&lt;/td&gt;
      &lt;td&gt;39&lt;/td&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;1.0&lt;/td&gt;
      &lt;td&gt;1.25&lt;/td&gt;
      &lt;td&gt;howl&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:42:12.299899&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_01_19_13_59_02&lt;/td&gt;
      &lt;td&gt;39&lt;/td&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;1.25&lt;/td&gt;
      &lt;td&gt;1.5&lt;/td&gt;
      &lt;td&gt;howl&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:42:13.635911&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_01_19_13_59_02&lt;/td&gt;
      &lt;td&gt;39&lt;/td&gt;
      &lt;td&gt;6&lt;/td&gt;
      &lt;td&gt;1.5&lt;/td&gt;
      &lt;td&gt;1.75&lt;/td&gt;
      &lt;td&gt;howl&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:42:44.608062&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_02_11_16_56_39&lt;/td&gt;
      &lt;td&gt;7&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0.0&lt;/td&gt;
      &lt;td&gt;0.25&lt;/td&gt;
      &lt;td&gt;whine&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:42:55.567570&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;…&lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
      &lt;td&gt;…&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Notice that the actual audio data isn’t in here, it’s merely referenced by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session_id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event_id&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subevent_id&lt;/code&gt;. In theory, I could have added the audio as another column like this:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;audio&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;216,224,222,224,223,209,187,135,82,4…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;-10,-3,13,33,37,16,8,22,31,52,73,8…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;76,136,175,185,156,105,44,-40,-112,-…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;192,196,200,209,213,228,242,244,251,…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;39,41,39,59,59,55,52,67,103,130,137…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;30,49,51,40,18,-27,-2,24,-39,-119,-…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;-222,62,144,-71,-212,-45,152,65,-135,…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;9,45,88,155,88,-47,-78,-37,0,-52,-1…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6,9,4,-20,-31,-34,-30,-27,-25,-18,-…&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;However this is pretty messy and a waste of space. Why duplicate the audio when it’s already stored in the session databases? Furthermore, I’ve written an API that makes accessing the data easy, so why not use it?. To associate these labels to their underlying audio data, I created a dictionary of &lt;a href=&quot;https://github.com/ekiefl/maple/blob/2be825f868942493c2d695c7c7a30de6e2d21300/maple/data.py#L197&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SessionAnalysis&lt;/code&gt;&lt;/a&gt; instances, one for each session:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dbs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;session_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;session_id&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dbs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, I wrote methods to query the underlying audio data of any audio chunk, which I call a &lt;strong&gt;subevent&lt;/strong&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_event_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dbs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_event_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_subevent_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;event_audio&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_event_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;subevent_len&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;subevent_audio&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

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

&lt;p&gt;Now I can easily access the audio for any subevent. For example, the first label data is&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;session_id&lt;/th&gt;
      &lt;th&gt;event_id&lt;/th&gt;
      &lt;th&gt;subevent_id&lt;/th&gt;
      &lt;th&gt;t_start&lt;/th&gt;
      &lt;th&gt;t_end&lt;/th&gt;
      &lt;th&gt;label&lt;/th&gt;
      &lt;th&gt;date_labeled&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;2021_03_05_20_10_07&lt;/td&gt;
      &lt;td&gt;430&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;0.0&lt;/td&gt;
      &lt;td&gt;0.25&lt;/td&gt;
      &lt;td&gt;bark&lt;/td&gt;
      &lt;td&gt;2021-03-15 22:41:29.135011&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The audio can be accessed with&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_subevent_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;2021_03_05_20_10_07&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;430&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p class=&quot;notice&quot;&gt;By the way, if you go down the rabbit hole, this audio data is ultimately obtained by an SQL query using the &lt;a href=&quot;https://docs.python.org/3/library/sqlite3.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sqlite3&lt;/code&gt;&lt;/a&gt; python API.&lt;/p&gt;

&lt;p&gt;Preparing the data is done with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.prep_data&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;prep_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;spectrogram&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Establishes the training and validation datasets

    This method sets the attributes `self.X`, and `self.y`

    Parameters
    ==========
    transformation : str, &apos;spectrogram&apos;
        Pick any of {&apos;spectrogram&apos;, &apos;none&apos;, &apos;fourier&apos;}.
    &quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;spectrogram&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;transformation_fn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_subevent_spectrogram&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_spectrogram_length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;none&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;transformation_fn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_subevent_audio&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_audio_length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;fourier&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;transformation_fn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_subevent_fourier&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_fourier_length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;transformation &apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transformation&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos; not implemented.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;X&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zeros&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zeros&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;astype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iterrows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_dict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;label&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformation_fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;session_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;session_id&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;event_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;event_id&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;subevent_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subevent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;subevent_id&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;X&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This method sets the attributes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.X&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.y&lt;/code&gt;. Each row of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.X&lt;/code&gt; is the data for each audio chunk, and each element of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.y&lt;/code&gt; is a numerical label corresponding to each of the 6 classes. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transformation&lt;/code&gt; will define the contents of each row. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transformation=&apos;spectrogram&apos;&lt;/code&gt;, each row is the flattened spectrogram of an audio chunk. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transformation=&apos;fourier&apos;&lt;/code&gt;, each row is the Fourier amplitude spectrum of an audio chunk. And finally, if &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transformation=&apos;none&apos;&lt;/code&gt;, each row is the raw audio signal. I find the ability to train using any of these transformations very exciting because I can directly compare performance depending on which transformation is used.&lt;/p&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.X&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.y&lt;/code&gt; defined, the model can be trained. This is done with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.fit_data&lt;/code&gt; which is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sklearn.ensemble.RandomForestClassifier&lt;/code&gt; wrapper.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fit_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Trains a random forest classifier and calculates an OOB model score.

    This method trains a model that is stored as `self.model`. `self.model` is a
    `sklearn.ensemble.RandomForestClassifier` object. The model score (fraction of correctly
    predicted validation samples) is stored as `self.model.xval_score_`

    Parameters
    ==========
    *args, **kwargs
        Uses any and all parameters accepted by `sklearn.ensemble.RandomForestClassifier`
        https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html
    &quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RandomForestClassifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The model is trained and stored as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.model&lt;/code&gt; and a quality score using out-of-bag samples (a unique quality of the random forest classifier) is stored as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.model.oob_score_&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once the model has been created, it can be saved for later use with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.save_model&lt;/code&gt;, which makes use of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;joblib&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save_model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filepath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Saves `self.model` as a file with using `joblib.dump`

        Saves `self.model`, which is a `sklearn.ensemble.RandomForestClassifier` object, to
        `filepath`.  Before saving, some the `sample_rate`, `subevent_time`, `subevent_len`, and
        whether the spectrogram was log-transformed (`log`) are stored as additional attributes of
        `self.model`.

        Parameters
        ==========
        filepath : str, Path-like
            Stores the model with `joblib.dump`
        &quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_time&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sample_rate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_len&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sample_rate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;joblib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dump&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filepath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This provides convenient access to the model so that at any time, the model can be loaded with&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;joblib&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;joblib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As a matter of convenience, I also store training and data parameters as attributes of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.model&lt;/code&gt;. For example, the sampling rate of the audio, the size of each data chunk, and the transformation type are all stored directly in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.model&lt;/code&gt; which makes this information available when the model is loaded at a later date.&lt;/p&gt;

&lt;p&gt;The totality of the model-building procedure is glued together with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self.run&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Run the training procedure

    This method glues the procedure together.
    &quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prep_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spectrogram&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fit_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;oob_score&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save_model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_dir&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;model.dat&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disconnect_dbs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;performance&quot;&gt;Performance&lt;/h3&gt;

&lt;p&gt;This is the fun part. How good is the model? I wanted to address this question from several standpoints.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Hyperparameter tuning&lt;/strong&gt;: What are the optimal parameters for the model fit?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Data transformation comparisons&lt;/strong&gt;: Does the spectrogram data outperform the raw audio signal? How about the Fourier signal?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Quantity of training data&lt;/strong&gt;: Would labeling more data significantly increase accuracy or have I reached the point of diminishing returns?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But first, let’s just get a rough idea of how good the model is using out-of-the-box parameters, aka &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sklearn&lt;/code&gt;’s defaults. I extended the command line to include a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;train&lt;/code&gt; mode, so I can train the data like so.&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;./main.py train &lt;span class=&quot;nt&quot;&gt;--label-data&lt;/span&gt; label_data.txt &lt;span class=&quot;nt&quot;&gt;--model-dir&lt;/span&gt; model
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This created a model file under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model/model.dat&lt;/code&gt;. Loading up the model and printing out the out-of-bag validation score yields&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;joblib&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;joblib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;model/model.dat&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oob_score_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.8506375227686703&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;It correctly labelled 85% of the validation dataset&lt;/strong&gt;. In my opinion, that’s really good given such a small training dataset.&lt;/p&gt;

&lt;h4 id=&quot;hyperparameter-tuning&quot;&gt;Hyperparameter tuning&lt;/h4&gt;

&lt;p&gt;The default model parameters did a pretty good job, but let’s see if tuning the hyperparameters can improve performance.&lt;/p&gt;

&lt;p&gt;The plan is to do a hyperparameter grid search to see what kind of model performance improvements can be achieved. However, before doing that there is one parameter of especial interest, since it greatly affects the speed of training: the number of trees.&lt;/p&gt;

&lt;p&gt;Random forest classifiers create a forest of decision trees, and for achieving the highest accuracy, &lt;strong&gt;more trees is better&lt;/strong&gt;. Let’s call the number of trees $n$. Increasing $n$ will statistically speaking increase model accuracy, however there is a law of diminishing returns and this comes at the cost of fit speed.&lt;/p&gt;

&lt;p&gt;Choosing $n=10,000$ would be a bad idea for the hyperparameter grid search, since it would take minutes to generate each model. So to increase my search space, I wanted to pick the lowest number of trees possible so that model generation is fast, yet the results are &lt;strong&gt;good enough&lt;/strong&gt; to make reliable decisions from. Then, the plan is to hold the number of trees constant for the grid search and vary all of the other hyperparameters.&lt;/p&gt;

&lt;p&gt;So how many trees should I pick? I wrote a quick script that prepares the data using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Train&lt;/code&gt; class and then generates models for varying $n$. For each $n$ value I used a 3-fold cross validation to reduce the effect of overfitting to any particular data subset. Then, I plotted the mean and standard error of the cross-validation scores at each $n$ along with the mean time that it took to fit each model.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;argparse&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.classifier&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.ensemble&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RandomForestClassifier&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.model_selection&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GridSearchCV&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argparse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Namespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;label_data.txt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_dir&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;none&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prep_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;param_grid&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;s&quot;&gt;&apos;n_estimators&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logspace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stop&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GridSearchCV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;estimator&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RandomForestClassifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;param_grid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param_grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cv&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verbose&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;n_jobs&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;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Plotting
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model_df&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DataFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cv_results_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;n_trees&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;param_n_estimators&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;mean_scores&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;mean_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;std_err_scores&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;std_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sqrt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_trees&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;fit_times&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;mean_fit_time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;std_err_times&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;std_fit_time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sqrt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_trees&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;fig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ax1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subplots&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;tab:red&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set_title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Tradeoff of fit time vs accuracy&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set_xlabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Number of trees&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set_ylabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Cross-validation score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set_xscale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;log&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errorbar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_trees&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mean_scores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;yerr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;std_err_scores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capsize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;4.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;--&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tick_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;y&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labelcolor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;ax2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ax1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;twinx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;tab:blue&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set_ylabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Fit time [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;errorbar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_trees&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fit_times&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;yerr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;std_err_times&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capsize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;4.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;--&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ax2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tick_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;y&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labelcolor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;fig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tight_layout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Running the script produces a plot that illustrates the increase in accuracy (shown in red) as a function of $n$, the number of trees, at the expense of fit time (shown in blue).&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/score_vs_n.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/score_vs_n.png&quot; alt=&quot;score_vs_n&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So as expected, the more trees the better, but there is definitely a point of diminishing returns past around 20 trees. And this comes at substantial time cost, where in blue we can see the model fit time increases exponentially as a function of $n$.&lt;/p&gt;

&lt;p&gt;Overall, these data are showing me it wouldn’t be a bad idea to use $n=30$ for the hyperparameter grid search.&lt;/p&gt;

&lt;p&gt;To do the grid search, I took a look at the sklearn.RandomForestClassifier &lt;a href=&quot;https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html&quot;&gt;docs&lt;/a&gt; as well as &lt;a href=&quot;https://towardsdatascience.com/hyperparameter-tuning-the-random-forest-in-python-using-scikit-learn-28d2aa77dd74&quot;&gt;this blog&lt;/a&gt; and ultimately decided on the following parameter grid:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;param_grid&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;s&quot;&gt;&apos;n_estimators&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;max_features&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;sqrt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;log2&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;max_depth&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;astype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;min_samples_split&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;min_samples_leaf&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s 640 different parameter combinations, and with a 5-fold cross-validation per parameter set, I needed to generate 3200 different models. That’s why picking the smallest $n$ possible is so important. I ran the grid search with the following script, which took about an hour to run (I estimate it would have taken 3 hours for $n=200$ and 9 hours for $n=500$).&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;argparse&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.classifier&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.ensemble&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RandomForestClassifier&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sklearn.model_selection&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GridSearchCV&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argparse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Namespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;label_data.txt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prep_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;param_grid&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;s&quot;&gt;&apos;n_estimators&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;max_features&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;sqrt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;log2&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;max_depth&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;astype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;min_samples_split&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;min_samples_leaf&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GridSearchCV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;estimator&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RandomForestClassifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;param_grid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param_grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cv&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verbose&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;n_jobs&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;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DataFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cv_results_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort_values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;by&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;rank_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cols&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;s&quot;&gt;&apos;mean_fit_time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;mean_score_time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;mean_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startswith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;param&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;endswith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;param&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cols&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_csv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;hyperparameter_tuning_results.txt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\t&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The output of this script is a dataframe. First, I looked at the model performances by ordering them according to their rank, and noticed there appears to be 3 apparent regimes of model quality, that I’ve colored below.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;rank_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;mean_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xlabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Rank&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ylabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Accuracy&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/score_vs_rank.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/score_vs_rank.png&quot; alt=&quot;score_vs_n&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wondered if there were any keystone parameters that partition the models into these three regimes. So I looked at the most common parameter value chosen for each of the three regimes.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;rank_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;regime&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;top&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;rank_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;rank_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;600&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;regime&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;middle&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;rank_test_score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;600&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;regime&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;bottom&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;groupby&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;regime&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;param_max_features&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;param_max_depth&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;param_min_samples_leaf&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;param_min_samples_split&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_level_values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;count&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;freq&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;top&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;most_common&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_level_values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;top&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;droplevel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transpose&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;most_common&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;bottom&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;middle&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;top&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;param_max_features&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;log2&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;log2&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;sqrt&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;param_max_depth&lt;/span&gt;              &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;     &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;    &lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;param_min_samples_leaf&lt;/span&gt;       &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;      &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;     &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;param_min_samples_split&lt;/span&gt;     &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;     &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;     &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Surprisingly, this partitioning according to rank is 100% interpretable.&lt;/p&gt;

&lt;p&gt;Take a look at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_features&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;most_common&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;param_max_features&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bottom&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;log2&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;middle&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;log2&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;top&lt;/span&gt;       &lt;span class=&quot;n&quot;&gt;sqrt&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param_max_features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;object&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The most commonly seen &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_features&lt;/code&gt; in the top regime is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sqrt&lt;/code&gt;, whereas the most common in the middle and bottom regimes both have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_features&lt;/code&gt; equal to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log2&lt;/code&gt;. In fact, the best ranking model with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_features&lt;/code&gt; equal to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log2&lt;/code&gt; ranks 101st. This indicates that &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sqrt&lt;/code&gt; is strongly favored&lt;/strong&gt; for the spectrogram transformation, and that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_features&lt;/code&gt; is a very important distinguishing hyperparameter.&lt;/p&gt;

&lt;p&gt;Moving on to the other 3 parameters, here I’ve plotted the most common value seen for each regime.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;most_common&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tight_layout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ylabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;value&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/hyperparameters.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/hyperparameters.png&quot; alt=&quot;hyperparameters&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the far left is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_depth&lt;/code&gt;, where we see a clear increase in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_depth&lt;/code&gt; as the models get better and better. In fact, around 75% of models in the bottom regime have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_depth&lt;/code&gt; of 5. Similarly for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;min_samples_leaf&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;min_samples_split&lt;/code&gt;, we see that the middle and bottom regimes consistently have high values, and the top regime has low values.&lt;/p&gt;

&lt;p&gt;The consistent theme in the trends of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_depth&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;min_samples_leaf&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;min_samples_split&lt;/code&gt; is that top-performing models do not want to be constrained in the number of leaves in the decision tree. This makes intuitive sense, but it’s nice to see it in the data.&lt;/p&gt;

&lt;p&gt;Based on these findings, I’ve decided to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Train.fit_data&lt;/code&gt; to perform a small parameter scan with 10-fold cross validation. The new method looks like this:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fit_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Trains a random forest classifier and calculates a model score.

    This method trains a bunch of models over a small subset of hyperparameter space based on an
    ad-hoc analysis described here:
    ekiefl.github.io/2021/03/14/maple-classifier/#-hyperparameter-tuning

    For each model setting, a 5-fold cross validation is used. When the most accurate model is
    determined, it is stored as `self.model` and the 5-fold cross validation accuracy is stored
    as self.model.xval_score_.
    &quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GridSearchCV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;estimator&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RandomForestClassifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;param_grid&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;s&quot;&gt;&apos;n_estimators&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;max_features&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;sqrt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;log2&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;criterion&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;gini&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;entropy&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cv&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;verbose&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;n_jobs&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;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;X&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;best_estimator_&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xval_score_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;best_score_&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;data-transformation-comparisons&quot;&gt;Data transformation comparisons&lt;/h4&gt;

&lt;p&gt;In the &lt;a href=&quot;#data-transformation&quot;&gt;data transformation&lt;/a&gt; section, I kept harping about how spectrograms would outperform time series audio (time-space) and Fourier spectra (frequency-space) because they resolve both time and frequency components simultaneously. &lt;strong&gt;Let’s see if that’s actually true&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Thanks to how general &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Train.prep_data&lt;/code&gt; is, this is really easy to test. To do so, I wrote this little script that calculates models with varying transformations.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;argparse&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;pandas&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;pathlib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Path&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.classifier&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;trans_settings&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;spectrogram&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;spectrogram&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;fourier&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;fourier&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;none&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argparse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Namespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;label_data.txt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;scores&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;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trans&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trans_settings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trans&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trans&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_dir&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;model_&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trans&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&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;s&quot;&gt;&apos;_log&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disconnect_dbs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xval_score_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disconnect_dbs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DataFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;transformation&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trans_settings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;log&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trans_settings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I was also curious whether or not log-transforming the data could increase accuracy, so that’s in the script too.&lt;/p&gt;

&lt;p&gt;The output is a table summarizing the prediction quality for various types of transformations on the audio data:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: right&quot;&gt; &lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;transformation&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;log&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;score&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;spectrogram&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;False&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.852823&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;spectrogram&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;True&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.850638&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;fourier&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;False&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.825865&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;fourier&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;True&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.826958&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;none&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;False&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0.632423&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;It’s great to verify what I’ve been advertising: the spectrogram outperforms the time- and frequency-space transformations. It’s interesting to see how poor the classifier performs when trained on the time series audio signal. As a final observation, log-transforming the data seems to have little effect on performance.&lt;/p&gt;

&lt;h4 id=&quot;quantity-of-training-data&quot;&gt;Quantity of training data&lt;/h4&gt;

&lt;p&gt;So hyperparameter tuning and investigating alternative data transformations did not yield significant increases in classification performance. The only remaining thing to investigate is: &lt;strong&gt;should I label more data&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;A bigger training dataset is definitely better, but there comes a point where the reward does not match the effort. So before committing to labeling more data, which sucks, I wanted to know how much room there is for improvement. Basically, if $N$ is the size of my training dataset, and $S$ is the model score, I wanted to know where I lie on this learning curve:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/stay_or_go.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/stay_or_go.jpg&quot; alt=&quot;stay_or_go&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get an idea of where I am on the learning curve, I trained a series of models with a subset of my 2,745 label data and visualized the results. At each fraction of the full data, I’m doing a 5-fold cross validation for 5 trials, where the training data is resampled for each trial.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;argparse&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;pandas&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.classifier&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argparse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Namespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;label_data.txt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;full_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;param_grid&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;s&quot;&gt;&apos;n_estimators&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;fracs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linspace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zeros&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fracs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;trials&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;frac&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;enumerate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fracs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zeros&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;full_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sample&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frac&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frac&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reset_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;drop&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prep_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transformation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;spectrogram&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fit_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_grid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_grid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cv&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xval_score_&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;#9E70B8&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Should I keep labeling?&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xlabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Size of training dataset (N)&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ylabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Model score (S)&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;full_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fracs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;astype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;yticks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scores&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.01&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.01&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/stay_or_go_real.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/stay_or_go_real.png&quot; alt=&quot;stay_or_go_real&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To my eye, it seems like there isn’t much to gain from labeling more data, but I really wanted to see if I could break 86% accuracy, so I decided that I would label up to $N=5000$.&lt;/p&gt;

&lt;p&gt;To start labeling more data, I ran the command&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;./main.py label &lt;span class=&quot;nt&quot;&gt;--label-data&lt;/span&gt; label_data.txt &lt;span class=&quot;nt&quot;&gt;--session-paths&lt;/span&gt; training_session_paths
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;75 minutes later, there are now 5,031 labelled audio chunks. Repeating the analysis I ended up with this new learning curve:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/stay_or_go_real2.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/stay_or_go_real2.png&quot; alt=&quot;stay_or_go_real2&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The results did not change significantly. Certainly not enough to have justified 75 minutes. It seems like the increase in accuracy was about 0.5-1.0%, bringing the total accuracy just above 85%.&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;These plots were generated using 30 trees to cut down on run-time, so the final model with 200 trees will produce a slightly higher score.&lt;/p&gt;

&lt;h4 id=&quot;final-model-accuracy&quot;&gt;Final model accuracy&lt;/h4&gt;

&lt;p&gt;After considering hyperparameter space, alternative data transformations, and doubling the amount of training data, I’ve exhausted all my options. It is time to run one last model: the one I’ll use moving forward:&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;./main.py train &lt;span class=&quot;nt&quot;&gt;--label-data&lt;/span&gt; label_data.txt &lt;span class=&quot;nt&quot;&gt;--model-dir&lt;/span&gt; model
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This model is produced from a cross-validation grid search of potential models, which has no constraints on tree depth or leaf number, considers 2 alternatives for the max number of features each tree is built from (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sqrt&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log2&lt;/code&gt;), 2 alternatives for splitting criterion (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gini&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entropy&lt;/code&gt;), and a fixed tree number of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;200&lt;/code&gt;. Each model’s score is determined from a 20-fold cross-validation scheme and the top-ranking model becomes the chosen model.&lt;/p&gt;

&lt;p&gt;Loading up the resultant model and printing its score yields a &lt;strong&gt;final accuracy of 86.1%&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;joblib&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;joblib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;model/model.dat&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xval_score_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.8610715866691964&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;section-iv-classifying&quot;&gt;&lt;strong&gt;Section IV&lt;/strong&gt;: Classifying&lt;/h2&gt;

&lt;p&gt;Training the model is one thing. Using it is another. In this section I will use the model to classify and visualize past audio events from the last several months.&lt;/p&gt;

&lt;p&gt;The workhorse for all classification in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maple&lt;/code&gt; is this simple class (&lt;a href=&quot;https://github.com/ekiefl/maple/blob/b87d3d7e2c75a1b95c844ef043fb617de83e7d8d/maple/classifier.py#L596&quot;&gt;source code&lt;/a&gt;):&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Classifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; does not exist&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;joblib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scaler&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;joblib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;scaler.dat&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;predict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;as_label&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Given an arbitrary audio length, predict the class&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;num_chunks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;none&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;as_label&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zeros&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_features_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;audio_chunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;subevent_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;audio_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;chunk_predictions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;predict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# most common
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;prediction&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bincount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk_predictions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;argmax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prediction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;as_label&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prediction&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;audio_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trans&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;spectrogram&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_spectrogram&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;audio_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sample_rate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flatten&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trans&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;fourier&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_fourier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;audio_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sample_rate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trans&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;none&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;audio_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Transformation &apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;selfmodel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trans&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos; not implemented for Classifier&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scaler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reshape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;flatten&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;norm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isnan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# In diabolical cases an audio chunk may have all zeros, which result in nan&apos;s that break
&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;# the predict method.
&lt;/span&gt;            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zeros&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

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

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Classifier&lt;/code&gt; is initialized by providing the path to a model. Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Train&lt;/code&gt; stored all of the transformation metadata as attributes of the model object (such as whether it was trained on spectrograms, Fourier spectra, or raw audio &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trans&lt;/code&gt;; whether it was log-transformed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log&lt;/code&gt;; whether it was feature scaled &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scale&lt;/code&gt;; whether the samples were normalized &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;norm&lt;/code&gt;), upon loading the model, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Classifier&lt;/code&gt; &lt;strong&gt;knows exactly how it must transform incoming target data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This means predicting the class of an audio event is as simple as passing audio data to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Classifier.predict&lt;/code&gt;. When this happens, it slices the audio into chunks, calculates the appropriate transformation, and then classifies each chunk. The overall prediction for the entire event is chosen as the &lt;strong&gt;most frequently observed chunk classification&lt;/strong&gt;. So if 3 chunks are predicted to be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bark&lt;/code&gt; and 1 is predicted to be a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;whine&lt;/code&gt;, the prediction for the event is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bark&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To classify events from past sessions, I extended the command line utility to include a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;classify&lt;/code&gt; mode which makes use of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Classifier&lt;/code&gt;. It accepts a model directory as input, and optionally a list of session dbs to run the classifier on. It goes through each event of each session db, classifies the event, and stores the results in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;class&lt;/code&gt; column of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;events&lt;/code&gt; table. I ran it on all the session DBs with&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;./main.py classify &lt;span class=&quot;nt&quot;&gt;--model-dir&lt;/span&gt; model
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command provides convenient means to classify or re-classify events with a single command, which is super convenient if one day I train a superior classifier and want to apply it to past datasets.&lt;/p&gt;

&lt;h3 id=&quot;verdict-pretty-accurate&quot;&gt;Verdict: pretty accurate&lt;/h3&gt;

&lt;p&gt;From the training validation I know the accuracy is around 86%, but I wanted to see the classifier in action.&lt;/p&gt;

&lt;p&gt;So I loaded up a session database, which now houses fresh labels for each event&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;./main.py analyze &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; 2021_02_13_19_19_33
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and used &lt;a href=&quot;https://github.com/ekiefl/maple/blob/f54d85b588ff0e89192819460f77fef5ac0ae64f/maple/data.py#L294&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SessionAnalysis.play_many&lt;/code&gt;&lt;/a&gt; to start comparing the predictions to the audio. Here is a spattering of what I found that included a diverse range of classes.&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/NRtRwQp-sr0&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;It’s clearly working pretty well.&lt;/p&gt;

&lt;h3 id=&quot;real-time-classification&quot;&gt;Real-time classification&lt;/h3&gt;

&lt;p&gt;Now I have a way to classify events from past sessions. But moving forward, I would like to &lt;strong&gt;classify events as they occur&lt;/strong&gt; so that the dog-sitter can make real-time decisions on whether to respond to the dog that are based on the class predictions. With the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Classifier&lt;/code&gt; class in place, this is all I had to change.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ekiefl/maple/commit/e6ec84b59c5b9837901ffab54a4cd7bedc59e9c5&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/realtime_diff.png&quot; alt=&quot;realtime_diff&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upon this change, all future audio events will be classified on-the-fly.&lt;/p&gt;

&lt;h4 id=&quot;speed&quot;&gt;Speed&lt;/h4&gt;

&lt;p&gt;One concern about on-the-fly classification is speed. If classification is too slow, I may have to make some sacrifices. For example, tree traversal would be much faster if I decrease the number of trees in the random forest from 200 down to something more light-weight–say 30.&lt;/p&gt;

&lt;p&gt;To measure the classification speed, I borrowed a timing class I had written for &lt;a href=&quot;https://merenlab.org/software/anvio/&quot;&gt;anvi’o&lt;/a&gt; a few years back and put it in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maple.utils&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TimeCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Time a block of code.

    This context manager times blocks of code.

    Examples
    ========
    &amp;gt;&amp;gt;&amp;gt; import time
    &amp;gt;&amp;gt;&amp;gt; import maple.utils as utils
    &amp;gt;&amp;gt;&amp;gt; # EXAMPLE 1
    &amp;gt;&amp;gt;&amp;gt; with utils.TimeCode() as t:
    &amp;gt;&amp;gt;&amp;gt;     time.sleep(5)
    ✓ Code finished successfully after 05s

    &amp;gt;&amp;gt;&amp;gt; # EXAMPLE 2
    &amp;gt;&amp;gt;&amp;gt; with terminal.TimeCode() as t:
    &amp;gt;&amp;gt;&amp;gt;     time.sleep(5)
    &amp;gt;&amp;gt;&amp;gt;     print(asdf) # undefined variable
    ✖ Code encountered error after 05s

    &amp;gt;&amp;gt;&amp;gt; # EXAMPLE 3
    &amp;gt;&amp;gt;&amp;gt; with terminal.TimeCode(quiet=True) as t:
    &amp;gt;&amp;gt;&amp;gt;     time.sleep(5)
    &amp;gt;&amp;gt;&amp;gt; print(t.time)
    0:00:05.000477
    &quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s_msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;✓ Code finished after&apos;&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f_msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;✖ Code encountered error after&apos;&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__enter__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__exit__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exception_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exception_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;traceback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timedelta_to_checkpoint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;return_code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exception_type&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;return_code&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s_msg&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f_msg&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, I wrote a script that loads up the audio events of an example session database, and measures the time it takes to classify each event for the classifier. Since I suspected that the number of trees would have a major influence on speed, I went ahead and did these measurements for a spattering of models generated with different tree counts.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;argparse&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.data&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.utils&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TimeCode&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple.classifier&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Classifier&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;tree_nums&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;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;80&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;160&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;320&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;640&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;timing_df&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tree_nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;argparse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Namespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label_data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;label_data.txt&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;prep_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SessionAnalysis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;data/sessions/2021_02_13_19_19_33/events.db&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tree_nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Train a model
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_dir&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;model_n_&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup_dir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fit_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_grid&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;s&quot;&gt;&apos;n_estimators&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cv&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Load the model
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;classifier&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Classifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;model_n_&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/model.dat&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iterrows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;event_audio&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;audio&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TimeCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;classifier&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;predict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;timing_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disconnect_dbs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;seaborn&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sns&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;timing_df&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DataFrame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timing_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;melt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;var_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;num trees&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;timing_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timing_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;time [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;timing_df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;\
    &lt;span class=&quot;n&quot;&gt;groupby&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;num trees&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;\
    &lt;span class=&quot;n&quot;&gt;describe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;\
    &lt;span class=&quot;n&quot;&gt;droplevel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;\
    &lt;span class=&quot;n&quot;&gt;assign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stderr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;std&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;count&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;\
    &lt;span class=&quot;n&quot;&gt;plot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bar&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mean&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;yerr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;stderr&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Time-to-classify dependence on tree number&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;xlabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Number of trees [n]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ylabel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Time [s]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tight_layout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;plt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/classify_speed.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/classify_speed.png&quot; alt=&quot;classify_speed&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a model with 200 trees, the cost of classifying during runtime is about &lt;strong&gt;25ms&lt;/strong&gt; of downtime following each event. Because my application isn’t multi-threaded, this time eats into time that should be dedicated to listening for the next event. Fortunately for me, 25ms is barely anything for my application. On the other hand, a tree count of 640 yields a 60ms downtime, which is starting to become problematic.&lt;/p&gt;

&lt;p&gt;Based on these results, I am very happy to keep my model at a tree count of 200.&lt;/p&gt;

&lt;h2 id=&quot;section-v-visualizing&quot;&gt;&lt;strong&gt;Section V&lt;/strong&gt;: Visualizing&lt;/h2&gt;

&lt;p&gt;One utility of creating an audio classifier is being able to distill the entirety of a session into an intuitive visualization that illustrates how the dogs behaved. &lt;a href=&quot;https://ekiefl.github.io/2020/07/20/maple-intro/&quot;&gt;Last time&lt;/a&gt; I created an interactive interface that can be opened for any session from the command line.&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;./main.py analyze &lt;span class=&quot;nt&quot;&gt;--session&lt;/span&gt; 2021_02_13_19_19_33
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Before, it looked like this.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/old_viz.html&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/old_viz.png&quot; alt=&quot;old_viz&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
[&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/old_viz.html&quot;&gt;&lt;strong&gt;Click for interactive plot&lt;/strong&gt;&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Pretty cool. It shows every event and how loud the dogs were over time. It also shows owner responses, where green is praise and red is scold. But further decomposing the events into their classes would really paint a much more illustrative picture of what’s going on.&lt;/p&gt;

&lt;p&gt;I decided that a good way to visualize the data would be to bin the events into minute timeframes and show the proportion of events belonging to each class in a stacked line chart. Ultimately, I decided on the following visual aesthetic:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/new_viz.html&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/new_viz.png&quot; alt=&quot;new_viz&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
[&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/new_viz.html&quot;&gt;&lt;strong&gt;Click for interactive plot&lt;/strong&gt;&lt;/a&gt;]&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;I create these plots with &lt;a href=&quot;https://plotly.com/&quot;&gt;plotly&lt;/a&gt; and the source code can be found &lt;a href=&quot;https://github.com/ekiefl/maple/blob/7fd913f3948962b1bbade0e2d8dc36200ba332d2/maple/routines.py#L224&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With this visualization you can see at a glance how the dog’s behaved. For example, destroying my door seemed to be a passionate side project for Maple, where her progress is apparently driven by repeated bursts of inspiration.&lt;/p&gt;

&lt;h2 id=&quot;section-vi-updating-response-logic&quot;&gt;&lt;strong&gt;Section VI&lt;/strong&gt;: Updating response logic&lt;/h2&gt;

&lt;p&gt;Door scratching is the most undesirable behavior exhibited by the dogs. But in the above session, it went under the radar because it’s not very loud. This is the fundamental problem with the old decision-logic. Yet with real-time classification, now I can buff the decision logic with a more sentiment-based understanding of the events.&lt;/p&gt;

&lt;p&gt;The possibilities for buffing the response logic are now endless. I could create different pools of owner responses like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;praise&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;warn&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;console&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;encourage&lt;/code&gt;, etc. But in the interest of me graduating in a timely manner, I decided to keep things pragmatic: I am going to stick to just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;praise&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold&lt;/code&gt;, but I am going to update the decision logic that will take note of the event classes.&lt;/p&gt;

&lt;h3 id=&quot;praise&quot;&gt;Praise&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/praise_flowchart.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/praise_flowchart.jpg&quot; alt=&quot;praise_flowchart&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The old logic (above) took note of how many events were in the considered praise window, and enforced (1) that the total number of events should be below some threshold and (2) that none of the events should pass a certain pressure threshold.&lt;/p&gt;

&lt;p&gt;This is pretty &lt;em&gt;sound&lt;/em&gt; logic, but it fails under circumstances when the dogs produce noise that isn’t inherently bad. Currently, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;play&lt;/code&gt; is the only class that would fall under this category. To accommodate for the fact that noise is not always bad, the new logic is now the same, except all &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;play&lt;/code&gt; events in the praise window are removed prior to the logic shown in the above flowchart. This way, the dogs can play and still be praised.&lt;/p&gt;

&lt;h3 id=&quot;scold&quot;&gt;Scold&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/scold_flowchart.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/scold_flowchart.jpg&quot; alt=&quot;scold_flowchart&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The old logic (above) took note of the total level of noise, whether the most recent event was particularly loud, and if both of these values pass their respective thresholds, the dog-sitter scolds. Again, this is an oversimplification because noise is not always bad.&lt;/p&gt;

&lt;p&gt;After re-analyzing sessions, I came to the following new workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/scold_workflow_1.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/scold_workflow_1.jpg&quot; alt=&quot;scold_workflow_1&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of measuring the total amount of sound pressure, I now count the number of barks. This is first thing that must be triggered to consider a scold. If the number of barks in the scold window is met, then I further demand that that the last $n$ events were &lt;em&gt;all&lt;/em&gt; barks. This is just giving them the benefit of the doubt. And then, as a final gift of generosity, I demand that the last event passes some noise threshold. If all these conditions are met, the dog-sitter scolds.&lt;/p&gt;

&lt;p&gt;So that’s the workflow for barking. I also wanted to handle door scratching since it is so destructive, so there is another flowchart for scratching.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/scold_workflow_2.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/scold_workflow_2.jpg&quot; alt=&quot;scold_workflow_2&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More simply, the dog-sitter fires off a scold if a certain number of door scratching events are found within the scold window.&lt;/p&gt;

&lt;p&gt;Today we’re leaving the house, and these poor doggies are going to be left alone for about 5 hours. A perfect opportunity to test the new decision logic.&lt;/p&gt;

&lt;h3 id=&quot;trial-run&quot;&gt;Trial run&lt;/h3&gt;

&lt;p&gt;I nervously set up the system, locked the dogs in the bedroom, and quietly shut the door.&lt;/p&gt;

&lt;p&gt;And 5 hours later the results are in. This is a big milestone: &lt;strong&gt;first session that implemented real-time audio classification and sentiment-based decision logic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/trial_session.html&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/trial_session.png&quot; alt=&quot;trial_session&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;
[&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/new_viz.html&quot;&gt;&lt;strong&gt;Click for interactive plot&lt;/strong&gt;&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Considering it was the longest we had left them alone in months, they behaved &lt;em&gt;so&lt;/em&gt; well. I’m proud of them. And to be honest, I’m proud of the virtual dog-sitter too, because I think it acted quite rationally.&lt;/p&gt;

&lt;p&gt;So I see some door scratching. Were any of the 4 scold responses triggered by door scratching? Here is a zoom-in of the second scold that was properly triggered in response to a series of door scratching events.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/zoom_2.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/zoom_2.png&quot; alt=&quot;zoom_2&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Great! And as cherry on top, the scold seems to have deterred the behavior, at least for a brief minute.&lt;/p&gt;

&lt;p&gt;As another example, here is a zoom-in of the first 10 minutes that we were gone, where things got crazy.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-classifier/zoom_10.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-classifier/zoom_10.png&quot; alt=&quot;zoom_10&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upon being alone, they are initially struck with worry. Worry that they can’t see their mommy, worry that they heard the front door close, worry that they are no longer in control. This manifests in their vocalizations as whining, and I think it corresponds to them fearing the worst, but trying to keep it together because they want to be good dogs.&lt;/p&gt;

&lt;p&gt;Yet as their reality sets in, worry and disbelief turn to panic and desperation. This spiral in mindset is clearly seen as the sound landscape transitions from whining-dominant to barking-dominant. Now unhinged, they continue barking until the dog-sitter decides that enough is enough and &lt;strong&gt;scolds them&lt;/strong&gt;. This seems to snap the dogs back to reality, as evidenced by the abrupt halt in barking.&lt;/p&gt;

&lt;p&gt;So the new decision logic for scolding is triggering at the right time, and appears to have a real influence on their behavior.&lt;/p&gt;

&lt;p&gt;While the last example shows some very interesting data from a technical standpoint, this is a rather depressing illustration of &lt;a href=&quot;https://www.aspca.org/pet-care/dog-care/common-dog-behavior-issues/separation-anxiety&quot;&gt;separation anxiety&lt;/a&gt;, something very common in dogs that’s driven by an unhealthy emotional dependence on their owners.&lt;/p&gt;

&lt;p&gt;Separation anxiety is essentially the fear of being alone. Currently the dog-sitter addresses separation anxiety by creating the illusion that they are &lt;em&gt;not&lt;/em&gt; actually alone. Instead, auditory responses of Kourtney and I give a convincing facade that big brother is watching. This lets them hear our voices and promotes the concept that the same behavorial rules apply even when we are not physically present–which provides them with a sense of normalcy.&lt;/p&gt;

&lt;p&gt;So currently separation anxiety is being addressed mostly in an illusory manner. Yet moving forward, I would like the dog-sitter to countercondition the fear associated with being alone, with a newfound excitement of being alone. Currently, all they get for being good is an auditory praise, but what if I created something that dispenses treats instead? A surplus of treats (contingent on good behavior) is certainly a silver lining for being left alone, and would help countercondition a lot of the separation anxiety. If I take this project further, this is the direction I will head.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;In summary, this work builds upon the maple software by implementing a random forest classifier that can identify common sounds produced by Maple and Dexter (whine, bark, etc). By classifying events in real time, I was able to develop more robust decisions on how the dog-sitter should respond to the dogs.&lt;/p&gt;

&lt;p&gt;All of this new functionality has been implemented in the &lt;a href=&quot;https://github.com/ekiefl/maple&quot;&gt;code base&lt;/a&gt;, and the totality of changes made can be viewed in this &lt;a href=&quot;https://github.com/ekiefl/maple/compare/a0689457ee304d41ffff2ae79f9669e824a4c9f7...341da041459c8222c238866403973c71b22b31eb&quot;&gt;git compare&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;See you next time.&lt;/p&gt;

    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2021/03/14/maple-classifier/&quot;&gt;Classifying dog barks for my dog-sitting software&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on March 14, 2021.&lt;/p&gt;

  </content>
</entry>


<entry>
  <title type="html"><![CDATA[The algorithmic theory behind pool/billiards simulation]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2020/12/20/pooltool-alg/" />
  <id>https://ekiefl.github.io/2020/12/20/pooltool-alg</id>
  <published>2020-12-20T00:00:00+00:00</published>
  <updated>2020-12-20T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    
&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;

&lt;h3 id=&quot;what-is-a-pool-simulator&quot;&gt;What is a pool simulator?&lt;/h3&gt;

&lt;p&gt;In the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/&quot;&gt;last post&lt;/a&gt; I discussed the physics for all the different phenomena in pool and
outlined equations of motion for each scenario. Yet there is still a critical missing piece: &lt;strong&gt;how and
when should these equations be applied?&lt;/strong&gt; For instance, I have equations to resolve the collision
between two balls, but how do I know which two balls collided and when?&lt;/p&gt;

&lt;p&gt;A pool simulator is more than just a sum of physics equations. Critically, a pool simulator requires
an algorithm that coordinates the proper usage of these equations, and glues them together to evolve
a shot from the moment the cue ball is struck to the moment the last ball stops moving. This
algorithm is what I call the &lt;strong&gt;evolution algorithm&lt;/strong&gt;. The evolution algorithm advances the &lt;strong&gt;system
state&lt;/strong&gt; from some initial time $t_i$, to some final time $t_f$.&lt;/p&gt;

&lt;p&gt;In this post I define the system state, and then discuss two evolution algorithms: (1) discrete time
evolution, and (2) continuous event-based evolution.&lt;/p&gt;

&lt;h3 id=&quot;what-is-the-system-state&quot;&gt;What is the system state?&lt;/h3&gt;

&lt;p&gt;In the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/&quot;&gt;last post&lt;/a&gt; I defined the ball state by 3 vectors:
the ball’s displacement $\vec{r}(t)$, velocity $\vec{v}(t)$, and angular velocity $\vec{\omega}(t)$.
Together, these three vectors fully characterize the state of a ball at any given time $t$. The system
state for a given time $t$ is just the collection of individual ball states at that time.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Definition: system state&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Mathematically, the ball state for the $i\text{th}$ ball is&lt;/p&gt;

\[s_i(t) = \{ \vec{r}_i(t) \, , \vec{v}_i(t), \, \vec{\omega}_i(t) \}
\notag\]

  &lt;p&gt;where $\vec{r}_i(t)$ is the ball’s position, $\vec{v}_i(t)$ is the ball’s velocity, and
$\vec{\omega}_i(t)$ is the ball’s angular velocity. The system state for a table with $N$ balls at an arbitrary time $t$ is
then&lt;/p&gt;

\[S(t) = \{ s_i(t) \, \, \forall i \in \{1, ..., N\} \}
\notag\]

&lt;/div&gt;

&lt;p&gt;The evolution algorithm calculates the system for a desired $t$ based on the system state at
an earlier time. With this definition formalized, let’s move on to discuss options for a shot evolution algorithm.&lt;/p&gt;

&lt;h2 id=&quot;two-options-discrete-or-continuous&quot;&gt;Two options: discrete or continuous&lt;/h2&gt;

&lt;h3 id=&quot;discrete-time-evolution&quot;&gt;Discrete time evolution&lt;/h3&gt;

&lt;p&gt;The premise of discrete time evolution is to &lt;strong&gt;advance the system state in very small
discrete steps of time&lt;/strong&gt;. After each timestep events can be detected and resolved. Did a ball collide
with another ball, or with a cushion? If so, resolve the ball states, then advance to the next
system state.&lt;/p&gt;

&lt;p&gt;This is a really straight-forward evolution algorithm that is very convenient, and
simple to understand. Unfortunately, discrete time evolution introduces intrinsic error that reduces accuracy of when
events occur. An example is outlined in Figure 1.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/discrete_error.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/discrete_error.jpg&quot; alt=&quot;discrete_error&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 1&lt;/strong&gt;. Collision detection using discrete time evolution. The collision is detected at the
timestep when the 2 balls overlap slightly (marked as x).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the above scenario, the blue ball gets incrementally closer to the red ball with each
timestep. Upon the third time step, the blue and red balls overlap slightly, triggering the detection
of a collision. Herein lies the fundamental problem with discrete time evolution: &lt;strong&gt;collision events
are never predicted, but rather detected after they were already supposed to have happened&lt;/strong&gt;.
So in the above example, the calculated collision time is $3 \Delta t$, even though the actual
collision time is less than that–more like $2.8 \Delta t$.&lt;/p&gt;

&lt;p&gt;The inaccuracy introduced by discrete time evolution can always be ameliorated by making $\Delta
t$ smaller and smaller, yet there will always exist a margin of error on the order of $\Delta t$.&lt;/p&gt;

&lt;p&gt;Furthermore, decreasing $\Delta t$ comes at massive computational cost. For example if you decrease
the timestep 100-fold—sure, you increase the accuracy 100-fold—but you also increase the
computational complexity 100-fold, since now you’ve gotta step through 100 times more time steps.&lt;/p&gt;

&lt;p&gt;This can become brutal when dealing with pool simulations. Ball speeds commonly
reach up to $10$ m/s, and a reasonable requirement for realism is that 2 balls should never intersect more
than $1/100$th of a ball radius ($0.3$ mm). The required timestep for this level of realism is then
$30$ microseconds. If the time from the cue strike to the last ball rolling is 10 seconds, that is
$30,000$ time steps in total for one shot. If the level of realism is $1/1000$th a ball radius, that
is $300,000$ time steps. Yikes.&lt;/p&gt;

&lt;p&gt;The problem with this is all of the wasted computation… I don’t need $30$ microsecond time steps
when all of the balls are far apart and barely moving. It is only really in select scenarios, such
as a pool break, that realism demands such miniscule time steps.&lt;/p&gt;

&lt;p&gt;To save computation, a smart discrete time
evolution scheme would cut down on the number of time steps by making them
&lt;a href=&quot;https://en.wikipedia.org/wiki/Adaptive_step_size&quot;&gt;adaptive&lt;/a&gt; depending on the state of the system.
There are an infinite number of ways you could develop heuristics for an adaptive time stepper, that
may be based on the distances between balls (if they are far apart, increase the time step), or
based on velocities (if they are moving fast, decrease the time step). I’m not even going to go
there because the possibilities are endless.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Aside: sometimes, you have to use discrete time evolution&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Discrete time evolution is sometimes a necessary evil in many-body systems when equations of
motion cannot be solved analytically. For example, the &lt;a href=&quot;https://en.wikipedia.org/wiki/Three-body_problem&quot;&gt;three-body
problem&lt;/a&gt; (3 planets exhibiting gravitational
forces on one another) has no analytical formulae that can express the positions of the planets as a
function of time. That’s because the system is just so complex:&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/3_body_problem.gif&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/3_body_problem.gif&quot; alt=&quot;3_body_problem&quot; /&gt;&lt;/a&gt;
&lt;em&gt;Trajectories for three planetary bodies exhibiting gravitational forces on one another must be found through
discrete time evolution, since no analytical solutions exist. &lt;a href=&quot;https://en.wikipedia.org/wiki/Three-body_problem&quot;&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

  &lt;p&gt;Fortunately, for any given state of the system, the &lt;em&gt;forces&lt;/em&gt; governing the equations are calculable
(inverse square law), and this is &lt;em&gt;all&lt;/em&gt; that is needed for discrete time evolution. If $\Delta t$
is chosen to be small enough, one can consider the force between the $i\text{th}$ and $(i+1)$th time step
to be constant and since $\vec{F}=m\vec{a}$, that means acceleration is constant. We can discretely integrate
this equation once and then again to yield how the velocity and position should be updated for the $(i+1)$th timestep:&lt;/p&gt;

\[\vec{a}_{i+1} = \vec{F}/m
\notag\]

\[\vec{v}_{i+1} = (\vec{F}/m) t + \vec{v}_i
\notag\]

\[\vec{r}_{i+1} = \frac{(\vec{F}/m)}{2}t^2 + \vec{v}_{i+1} t + \vec{r}_i
\notag\]

  &lt;p&gt;Voila, we have a discrete time evolution algorithm for advancing the system state.&lt;/p&gt;

&lt;/div&gt;

&lt;h3 id=&quot;continuous-event-based-evolution&quot;&gt;Continuous event-based evolution&lt;/h3&gt;

&lt;p class=&quot;notice&quot;&gt;The continuous event-based evolution algorithm was first developed by Leckie and Greenspan in a
seminal paper entitled &lt;a href=&quot;https://link.springer.com/chapter/10.1007/11922155_19&quot;&gt;An Event-Based Pool Physics
Simulator&lt;/a&gt;. If you would like to hear it
straight from the horse’s mouth, a free pre-print of this publication is available
&lt;a href=&quot;http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.89.4627&amp;amp;rep=rep1&amp;amp;type=pdf&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/&quot;&gt;last post&lt;/a&gt; I presented a bunch of analytical
equations of motion for ball trajectories, depending on whether the ball is &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-1-stationary&quot;&gt;stationary&lt;/a&gt;, &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-2-spinning&quot;&gt;spinning&lt;/a&gt;, &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-3-rolling&quot;&gt;rolling&lt;/a&gt;, &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-4-sliding&quot;&gt;sliding&lt;/a&gt;, or &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-iv-ball-air-interactions&quot;&gt;airborne&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These equations are perfect, because we can use them to evolve the ball states to any arbitrary
time. The only (vital) problem is that &lt;strong&gt;events&lt;/strong&gt; such as the collision in Figure 1 disrupt their
validity, since they are developed assuming the ball acts in isolation.&lt;/p&gt;

&lt;p&gt;For example, a ball
rolling with a very high velocity may be destined to travel 50m in an isolated environment, yet
collides with a cushion after just a few meters. Thus, the ball can safely be evolved via &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-3-rolling&quot;&gt;the
rolling equations of motion&lt;/a&gt; &lt;strong&gt;up until the
moment of collision&lt;/strong&gt;, at which point the event must be resolved via &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-iii-ball-cushion-interactions&quot;&gt;the ball-cushion interaction
equations&lt;/a&gt;. After the
collision is resolved, the ball can be safely evolved up until the next event.&lt;/p&gt;

&lt;p&gt;At this point, my mind was made up. &lt;strong&gt;I was going to use the continuous event-based algorithm for its speed
and accuracy&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-algorithm-in-its-entirety&quot;&gt;The algorithm, in its entirety&lt;/h2&gt;

&lt;p&gt;The rest of this post is concerned with detailing each and every aspect of this algorithm.&lt;/p&gt;

&lt;h3 id=&quot;1-birds-eye-view&quot;&gt;(1) Bird’s eye view&lt;/h3&gt;

&lt;p&gt;The prescription of evolving the equations of motion up until the next event forms the basis of
the continuous event-based evolution algorithm. Simply stated, the algorithm goes like this:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Continuous event-based evolution algorithm&lt;/span&gt;&lt;/p&gt;

  &lt;ol&gt;
    &lt;li&gt;Set $t \rightarrow 0$.&lt;/li&gt;
    &lt;li&gt;Calculate the time $\Delta t_E$ until the next event. If $\Delta t_E = \infty$, stop.&lt;/li&gt;
    &lt;li&gt;Evolve the system state $S(t) \rightarrow S(t + \Delta t_E)$.&lt;/li&gt;
    &lt;li&gt;Resolve the event.&lt;/li&gt;
    &lt;li&gt;Set $t \rightarrow t + \Delta t_E$.&lt;/li&gt;
    &lt;li&gt;Repeat steps 2-5.&lt;/li&gt;
  &lt;/ol&gt;

&lt;/div&gt;

&lt;p&gt;Let’s walk through a loop of the algorithm. Assume the time is currently $t$, such that the system
state is $S(t)$.&lt;/p&gt;

&lt;p&gt;The first step is determining when the next event occurs. By next event, I mean that &lt;strong&gt;no other
event precedes it&lt;/strong&gt;. Assume this occurs an amount of time $\Delta t_E$ from the current time.
Calculating $\Delta t_E$ is non-trivial, and my guess is that around 95% of the algorithm’s
computational complexity is allocated to this specific task. The intricacies of this process will be
described &lt;em&gt;ad nauseam&lt;/em&gt; in the next section, but until then, assume $\Delta t_E$ is calculable.&lt;/p&gt;

&lt;p&gt;Once calculated, the next step is to advance the system state from $S(t)$ to $S(t + \Delta t_E)$.
This means updating each ball’s state to $s_i(t + \Delta t_E)$, where the equations of motion are
determined depending on whether the ball is stationary, spinning, rolling, sliding, or airborne. Since we
know that no other event occurs before $\Delta t_E$, we can safely evolve their equations to $t + \Delta t_E$,
but &lt;strong&gt;no further&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then, the states of any balls involved in &lt;strong&gt;the event must be resolved&lt;/strong&gt;. Some examples: If the event is
two balls colliding, their states are resolved via &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-ii-ball-ball-interactions&quot;&gt;the ball-ball interaction equations&lt;/a&gt;. If the event is a ball
contacting a cushion, its state is resolved via &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-iii-ball-cushion-interactions&quot;&gt;the ball-cushion interaction equations&lt;/a&gt;. If the event is a ball transitioning
from sliding to rolling, its state is resolved by updating its equations of motions from
&lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-4-sliding&quot;&gt;sliding&lt;/a&gt; to &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-3-rolling&quot;&gt;rolling&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once the states of the balls involved in the event have been solved, every ball now has equations of
motion that will be valid until the next event occurs. Thus, the process repeats itself.&lt;/p&gt;

&lt;p&gt;So that’s how the algorithm works, and it is superior to the discrete time evolution algorithm
because one can advance directly to the next event. A typical shot may only have as little as 5
events, and calculating when they occur is orders of magnitude less computation than the hundreds of
thousands of time steps used in discrete time evolution for a typical shot. Not to mention the only
error in the continuous event-based evolution algorithm is due to floating point precision.&lt;/p&gt;

&lt;p&gt;This is a beautifully simple algorithm, but its &lt;strong&gt;utterly useless if we can’t calculate $\Delta t_E$&lt;/strong&gt;,
the time until the next event–and that’s no easy task. That’s what I tackle in the next
sections. First, I formally define what an event is and outline all possible events. Then, I define the strategy
and in excruciating detail outline how to calculate the time until the next event.&lt;/p&gt;

&lt;h3 id=&quot;2-what-are-events&quot;&gt;(2) What are events?&lt;/h3&gt;

&lt;p&gt;In the context of the continuous event-based evolution algorithm, an event is defined as anything
that invalidates the equations of motion for 1 or more balls, due to something that is unaccounted
for in the equations of motion, such as a collision with another ball. In total, there are 8 events, 4 of
which are collisions, 4 of which are transitions. I discuss each below.&lt;/p&gt;

&lt;h4 id=&quot;ball-ball-collision&quot;&gt;ball-ball collision&lt;/h4&gt;

&lt;p&gt;Unlike every other event, the ball-ball collision involves 2 balls, one of which must be in a
translating motion state: rolling, sliding, or airborne. The other ball may be in any motion state:
stationary, spinning, rolling, sliding or airborne. The collision is governed by &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-ii-ball-ball-interactions&quot;&gt;the ball-ball
interaction equations&lt;/a&gt;.&lt;/p&gt;

&lt;!-- modified from http://bl.ocks.org/eyaler/10586116 --&gt;
&lt;div id=&quot;ball_ball_network&quot; class=&quot;subnetwork&quot;&gt;&lt;/div&gt;
&lt;script id=&quot;network&quot; path=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_ball_network.json&quot; svg_id=&quot;ball_ball_network&quot; height=&quot;325&quot; src=&quot;/assets/js/graph.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;&lt;em&gt;Figure 2. The possible inputs and outputs of the ball-ball collision event.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the incoming motion state isn’t airborne, then neither will be the outgoing state. Conversely,
if the incoming motion state is airborne, so will be the outgoing motion state. In other words, &lt;strong&gt;a
table-bound ball cannot become airborne due to a ball-ball collision&lt;/strong&gt;. I admit this might be
confusing if you consider what happens when an airborne ball collides with a ball on the table:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/iT8k4W7tvM8&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;They both end up airborne. Since the line of centers between the two balls is directed &lt;em&gt;into&lt;/em&gt; the
table, the outgoing velocity of the table-bound ball has a component into the table, which initiates
a ball-slate collision.  Consequently, the table-bound ball (the 3-ball) becomes airborne. In the
continuous event-based algorithm I’ve developed, the ball becomes airborne due to the ball-slate
collision, not the ball-ball collision, which are separated by an infinitesimally small amount of
time. As a network, the totality of this process looks like this:&lt;/p&gt;

&lt;!-- modified from http://bl.ocks.org/eyaler/10586116 --&gt;
&lt;div id=&quot;ball_ball_event_path&quot; class=&quot;subnetwork&quot;&gt;&lt;/div&gt;
&lt;script id=&quot;network&quot; path=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_ball_event_path.json&quot; svg_id=&quot;ball_ball_event_path&quot; height=&quot;400&quot; src=&quot;/assets/js/graph.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;&lt;em&gt;Figure 3. Network representation of events and ball motion states for the ball-ball collision slow-mo video.&lt;/em&gt;&lt;/p&gt;

&lt;h4 id=&quot;ball-cushion-collision&quot;&gt;ball-cushion collision&lt;/h4&gt;

&lt;p&gt;The ball-cushion collision involves just one ball, which must be in a translating motion state:
rolling, sliding, or airborne. The output state is either sliding or airborne. The collision is
governed by &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-iii-ball-cushion-interactions&quot;&gt;the ball-cushion interaction equations&lt;/a&gt;.&lt;/p&gt;

&lt;!-- modified from http://bl.ocks.org/eyaler/10586116 --&gt;
&lt;div id=&quot;ball_cushion_network&quot; class=&quot;subnetwork&quot;&gt;&lt;/div&gt;
&lt;script id=&quot;network&quot; path=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_cushion_network.json&quot; svg_id=&quot;ball_cushion_network&quot; height=&quot;325&quot; src=&quot;/assets/js/graph.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;&lt;em&gt;Figure 4. The possible inputs and outputs of the ball-cushion collision event.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The only way to have an outgoing airborne motion state is if the incoming motion state is also
airborne&lt;/strong&gt;. Just like in the ball-ball collision, you may find this confusing, since after a collision
with a cushion, you often see it airborne:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/bMdHxdEzSxM&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;Yet just like as described in the ball-ball collision, the ball becomes airborne due to the
ball-slate collision, which occurs an infinitesimally small amount of time after the ball-cushion
collision. You can see the above shot represented as an interaction:&lt;/p&gt;

&lt;!-- modified from http://bl.ocks.org/eyaler/10586116 --&gt;
&lt;div id=&quot;ball_cushion_event_path&quot; class=&quot;subnetwork&quot;&gt;&lt;/div&gt;
&lt;script id=&quot;network&quot; path=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_cushion_event_path.json&quot; svg_id=&quot;ball_cushion_event_path&quot; height=&quot;400&quot; src=&quot;/assets/js/graph.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;&lt;em&gt;Figure 5. Network representation of events and ball motion states for the ball-cushion slow-mo
video&lt;/em&gt;&lt;/p&gt;

&lt;h4 id=&quot;ball-slate-collision&quot;&gt;ball-slate collision&lt;/h4&gt;

&lt;p&gt;The ball-slate collision occurs when a ball contacts the table with a velocity component &lt;em&gt;into&lt;/em&gt; the
table ($-z-$direction). The collision is governed by &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-iv-ball-slate-interactions&quot;&gt;the ball-slate interaction equations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stationary, spinning, and
rolling balls necessarily have 0 speed along the $z-$axis, so the only available input motion
states are sliding and airborne.&lt;/p&gt;

&lt;p&gt;So when do we see ball-slate collisions? An airborne ball undergoes a ball-slate collision whenever
it contacts the table. Additionally, we’ve seen in Figures 3 and 5 that sliding balls can also have
non-zero velocities in the $z-$direction (caused by ball-ball and ball-cushion collisions that
immediately precede the ball-slate collision), and also undergo ball-slate collisions.&lt;/p&gt;

&lt;p&gt;The first
possible outgoing motion state of the ball-slate interaction is being airborne. The second possible
outgoing motion state is sliding, which occurs when the ball doesn’t have enough velocity to become
airborne.&lt;/p&gt;

&lt;!-- modified from http://bl.ocks.org/eyaler/10586116 --&gt;
&lt;div id=&quot;ball_slate_network&quot; class=&quot;subnetwork&quot;&gt;&lt;/div&gt;
&lt;script id=&quot;network&quot; path=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_slate_network.json&quot; svg_id=&quot;ball_slate_network&quot; height=&quot;275&quot; src=&quot;/assets/js/graph.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;&lt;em&gt;Figure 6. The possible inputs and outputs of the ball-slate collision event.&lt;/em&gt;&lt;/p&gt;

&lt;h4 id=&quot;ball-pocket-collision&quot;&gt;ball-pocket collision&lt;/h4&gt;

&lt;p&gt;Of all the events, this is definitely the most fabricated. You may hear
ball-pocket collision and think about all the complexities of the ball leaving
the slate and falling into the pocket, bouncing off the sides of the pocket,
etc. but that’s not what I mean by ball-pocket collision. In fact, I don’t
really plan to model that.&lt;/p&gt;

&lt;p&gt;Instead, the ball-pocket collision is merely a way
to determine if a ball is pocketed and if it is, removing it from the
simulation. Basically, &lt;strong&gt;if a ball undergoes a ball-pocket collision, the ball
is considered pocketed&lt;/strong&gt;. It went in the hole. Good job.&lt;/p&gt;

&lt;p&gt;To resolve the collision,
the ball is simply removed from the simulation.&lt;/p&gt;

&lt;h4 id=&quot;transition-events&quot;&gt;transition events&lt;/h4&gt;

&lt;p&gt;In Figures 2-6, I’ve shown that &lt;strong&gt;collisions often induce motion state transitions&lt;/strong&gt;, however in each of
those cases the transition is caused by the collision. Yet motion state transitions also occur
&lt;strong&gt;naturally&lt;/strong&gt;:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/A9mweRTxGiw&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;In the above short clip, the ball transitions from spinning to stationary. This is considered an event,
because upon becoming stationary, the ball’s equations of motion (spinning) become
invalid and need to be updated to the stationary equations of motion.&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;As a matter of interest, if the
equation was not updated, it dictates that the ball would begin spinning in the reverse direction, gaining
more and more rotational kinetic energy indefinitely. This is obviously non-physical, and illustrates the need
to properly handle motion state transition events.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Definition: transition event&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;A &lt;em&gt;transition event&lt;/em&gt; is when a ball’s motion state naturally transitions to another motion state,
&lt;em&gt;i.e.&lt;/em&gt; without being catalyzed by a collision.&lt;/p&gt;

  &lt;!-- modified from http://bl.ocks.org/eyaler/10586116 --&gt;
  &lt;div id=&quot;x_y_transition_network&quot; class=&quot;subnetwork&quot;&gt;&lt;/div&gt;
  &lt;script id=&quot;network&quot; path=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/x_y_transition_network.json&quot; svg_id=&quot;x_y_transition_network&quot; height=&quot;275&quot; src=&quot;/assets/js/graph.js&quot;&gt;&lt;/script&gt;

  &lt;p&gt;&lt;em&gt;Figure 7. A transition event network is very simple. X goes in and Y goes out.&lt;/em&gt;&lt;/p&gt;

&lt;/div&gt;

&lt;p&gt;Mathematically, there are $5^4 = 1024$ transitions, but only 4 occur naturally. Thus there are only 4
transition &lt;em&gt;events&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;spinning-stationary&lt;/li&gt;
  &lt;li&gt;rolling-stationary&lt;/li&gt;
  &lt;li&gt;rolling-spinning&lt;/li&gt;
  &lt;li&gt;sliding-rolling&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fundamentally, these transitions happen due to a loss of energy due to friction. So these
transitions are always from high energy to low energy.&lt;/p&gt;

&lt;p&gt;I’ve actually omitted 2 very rare transition events. In theory, a ball can transition
from sliding to stationary if the perfect amount of backspin is applied. Same goes for sliding to
spinning. So technically, there are 2 more transition events:&lt;/p&gt;

&lt;p&gt;5. sliding-stationary (&lt;em&gt;very rare&lt;/em&gt;)&lt;/p&gt;

&lt;p&gt;6. sliding-spinning (&lt;em&gt;very rare&lt;/em&gt;)&lt;/p&gt;

&lt;p&gt;Yet, there is 0 margin for error for these scenarios. In virtually all simulated cases, a ball will
exit the sliding state with non-zero speed (using 64-bit floats with SI units yields femtometer
$(10^{-15})$ per second precisions). For this reason, the sliding-stationary and sliding-spinning
transition events can in all practical senses be decomposed into 2 transition events separated by
some very small amount of time. For example, the sliding-stationary transition can be decomposed
into the sliding-rolling transition followed by a rolling-stationary transition.&lt;/p&gt;

&lt;!-- modified from http://bl.ocks.org/eyaler/10586116 --&gt;
&lt;div id=&quot;spinning_rolling_stationary_transition_network&quot; class=&quot;subnetwork&quot;&gt;&lt;/div&gt;
&lt;script id=&quot;network&quot; path=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/spinning_rolling_stationary_transition_network.json&quot; svg_id=&quot;spinning_rolling_stationary_transition_network&quot; height=&quot;275&quot; src=&quot;/assets/js/graph.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;&lt;em&gt;Figure 8. The sliding-stationary transition event can be decomposed into the sliding-rolling
followed by the rolling-stationary transition events.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This wraps up my description of the possible event types.&lt;/p&gt;

&lt;h3 id=&quot;3-the-strategy&quot;&gt;(3) The strategy&lt;/h3&gt;

&lt;p&gt;We now know all the possible types of events. So what’s the strategy for calculating when the next
event occurs? As it turns out, the most feasible strategy is to explicitly calculate the time until &lt;strong&gt;every
possible next event&lt;/strong&gt;. If you calculate the time until &lt;strong&gt;literally every possible next event&lt;/strong&gt;,
then by definition the one that occurs in the shortest amount of time is the one that physically occurs next.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;The strategy for calculating $\Delta t_E$&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;To calculate $\Delta t_E$, we calculate all possible events:&lt;/p&gt;

  &lt;ol&gt;
    &lt;li&gt;Calculate all ball-ball collision event times&lt;/li&gt;
    &lt;li&gt;Calculate all ball-cushion collision event times&lt;/li&gt;
    &lt;li&gt;Calculate all ball-slate collision event times&lt;/li&gt;
    &lt;li&gt;Calculate all ball-pocket collision event times&lt;/li&gt;
    &lt;li&gt;Calculate all ball transition event times&lt;/li&gt;
    &lt;li&gt;$\Delta t_E$ is the smallest of all calculated event times&lt;/li&gt;
  &lt;/ol&gt;

  &lt;p&gt;If $\Delta t_E$ is infinite, there is no next event, which happens in the unique scenario that the system is in its lowest energy state (all balls are stationary).&lt;/p&gt;

&lt;/div&gt;

&lt;h4 id=&quot;toy-example&quot;&gt;Toy example&lt;/h4&gt;

&lt;p&gt;Consider the example pictured in Figure 9. Ball A is sliding, balls B &amp;amp; C are stationary, and there
are no other balls on the table. For simplicity, assume there are no cushions or pockets and that
the table surface extends indefinitely. &lt;strong&gt;To calculate the next event, we need to consider all
possible events&lt;/strong&gt;. So let’s do that.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/all_events.jpg&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/all_events.jpg&quot; alt=&quot;all_events&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 9&lt;/strong&gt;. A system with 1 sliding ball (A) and 2 stationary balls (B &amp;amp; C). The solid-filled
balls indicate where the balls currently are. There are 3  next events: (1) Ball A can
transition from sliding to rolling $(\Delta t^{(1)})$, (2) ball A can collide with ball B $(\Delta
t^{(2)})$, or (3) ball A can collide with ball C $(\Delta t^{(3)})$.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;ball-cushion collision events&lt;/strong&gt;. Since there are no cushions, there are no ball-cushion
collision events to consider.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ball-pocket collision events&lt;/strong&gt;. Let’s also assume there are no pockets, so no ball-pocket
collision events to consider either.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ball-slate collision events&lt;/strong&gt;. Balls B and C are stationary, so cannot undergo a ball-slate
collision event. If ball A has a velocity component into the table, then the next event would be a
ball-slate collision, since it would occur at $t=0$. But for the sake of example, let’s assume
ball A has no velocity component into the table. Then there are no ball-slate collision events to
consider.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ball-ball collision events&lt;/strong&gt;. There are 3 potential ball-ball collision events:
between balls A &amp;amp; B, A &amp;amp; C, and B &amp;amp; C. However, since 2 stationary balls cannot collide, the B &amp;amp; C
collision is an impossible event, and only A &amp;amp; B, and A &amp;amp; C collisions need to be considered.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;transition events&lt;/strong&gt;. Since balls B and C are stationary, there are no available transition
events–they are already in their lowest energy states. On the other hand, Ball A is sliding, so a
sliding-rolling transition event is a possibility for Ball A.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In summary, there are 3 events to consider: (1) a sliding-rolling transition for ball A, (2) a
ball-ball collision with balls A and B, and (3) a ball-ball collision with ball A and C.&lt;/p&gt;

&lt;p&gt;Based on
how I’ve drawn Figure 9, it’s obvious that the next event is the sliding-rolling transition, yet in
general the time for each of these events must be calculated explicitly. If ball A was spinning a
lot, ball A could collide with ball B whilst spinning, which would make the collision with ball B
the next event. In fact, we can’t even rule out that a collision with C is not the next event, which
could happen if ball A has a curved trajectory that goes around ball B.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So the point is this&lt;/strong&gt;: given a system state $S(t)$, the only way to determine the next event $\Delta
t_E$, is to explicitly calculate the time until all possible next events. The next event is the one
that occurs in the smallest amount of time.&lt;/p&gt;

&lt;p&gt;This means we must develop means to calculate the time until every possible next event, which is the subject of the next section. Buckle your seatbelts.&lt;/p&gt;

&lt;h3 id=&quot;4-calculating-possible-event-times&quot;&gt;(4) Calculating possible event times&lt;/h3&gt;

&lt;h4 id=&quot;transition-times&quot;&gt;Transition times&lt;/h4&gt;

&lt;p&gt;Transition times are the easiest to calculate, so I’ll start with them.&lt;/p&gt;

&lt;p&gt;The spinning-stationary transition is defined by the moment at which a spinning ball reaches 0
angular velocity in the $z-$direction. From the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-2-spinning&quot;&gt;spinning equations of motion&lt;/a&gt;, the angular velocity as a function of time is given
by&lt;/p&gt;

\[\omega_z(t) = \omega_{0z} - \frac{5\mu_{sp}g}{2R}t \notag\]

&lt;p&gt;Setting this to 0 and solving for time yields the spinning-stationary transition time:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Time until spinning-stationary transition event&lt;/span&gt;&lt;/p&gt;

\[\Delta t_E = \frac{2R}{5 \mu_{sp} g} \omega_{0z}
\label{spinning_stationary_time}\]

  &lt;p&gt;where $\omega _{0z}$ is the current angular velocity in the $z-$direction.&lt;/p&gt;

&lt;/div&gt;

&lt;p class=&quot;notice&quot;&gt;Reminder, this equation is significant because for a given time $t$, the
spinning-stationary event for &lt;strong&gt;every spinning ball&lt;/strong&gt; must be considered as a
potential next event.&lt;/p&gt;

&lt;p&gt;On to the next. Both the rolling-stationary events &lt;em&gt;and&lt;/em&gt; the rolling-spinning events are defined by the moment at
which a ball’s center of mass velocity reaches 0. From the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-3-rolling&quot;&gt;rolling equations of motion&lt;/a&gt;, the velocity of a rolling ball as a function of time
is given by&lt;/p&gt;

\[\vec{v}(t) = \vec{v}_0 - \mu_r g t \hat{v}_0 \label{rolling_velocity}
\notag\]

&lt;p&gt;Taking the magnitude of both sides, setting the LHS to 0, and solving for time yields the
rolling-stationary and rolling-spinning transition times:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Time until rolling-(stationary/spinning) transition events&lt;/span&gt;&lt;/p&gt;

\[\Delta t_E = \frac{v_0}{\mu_{r} g}
\label{rolling_spinning_stationary_time}\]

  &lt;p&gt;where $v_0$ is the current ball speed.&lt;/p&gt;

  &lt;p&gt;If $\omega _{z}(\Delta t_E) = 0$, the event is a rolling-stationary transition event. Otherwise, it
is a rolling-spinning transition event.&lt;/p&gt;

&lt;/div&gt;

&lt;p class=&quot;notice&quot;&gt;Reminder, this equation is significant because for a given time $t$, the
rolling-spinning and rolling-stationary transition events for &lt;strong&gt;every rolling ball&lt;/strong&gt; must be considered as a
potential next event.&lt;/p&gt;

&lt;p&gt;Finally, we got the sliding-rolling transition, which is defined by the moment at which the relative velocity $\vec{u}$ becomes
$\vec{0}$. From the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-4-sliding&quot;&gt;sliding equations of motion&lt;/a&gt;, the relative velocity as a function of time is given
by&lt;/p&gt;

\[\vec{u}(t) = (u_0 - \frac{7}{2} \mu_s g t ) \, \hat{u}_0
\notag\]

&lt;p&gt;Taking the magnitude of both sides, setting the LHS to 0, and solving for time yields the
sliding-rolling transition time:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Time until sliding-rolling transition event&lt;/span&gt;&lt;/p&gt;

\[\Delta t_E = \frac{2}{7} \frac{u_0}{\mu_s g}
\label{sliding_rolling_time}\]

  &lt;p&gt;where $u_0$ is the current magnitude of the relative velocity.&lt;/p&gt;

&lt;/div&gt;

&lt;p class=&quot;notice&quot;&gt;Reminder, this equation is significant because for a given time $t$, the
sliding-rolling transition event for &lt;strong&gt;every sliding ball&lt;/strong&gt; must be considered
as a potential next event.&lt;/p&gt;

&lt;p&gt;That’s all of the event transition times. Short and sweet.&lt;/p&gt;

&lt;h4 id=&quot;ball-ball-collision-times&quot;&gt;Ball-ball collision times&lt;/h4&gt;

&lt;p&gt;We are on the search for calculating all possible next events.&lt;/p&gt;

&lt;p&gt;A very large subcategory of next possible events is all of the ball-ball
collisions that may occur. In fact, if there are 15 balls on the table, there
are $_ {15}\text{C} _ {2} = 105$ different ball-ball collision events that may be the
next event, and the time until each of them must be explicitly calculated. As we will see,
many may take an infinite or non-real amount of time.&lt;/p&gt;

&lt;p&gt;First, an important assumption needs to be discussed. Suppose the current time is $t$, and
you want to calculate the time until collision, $\Delta t_E$, between two balls
with states $s^{(i)}(t)$ and $s^{(j)}(t)$. I’ll call this the $i-j$ collision
event. Within the time range $[t, t + \Delta t_E]$, &lt;strong&gt;I will assume neither of
the balls are involved in any other event&lt;/strong&gt;. This means neither of the 2 balls
engage in any transitions or collisions. If either did, then the intervening
event necessarily precedes the $i-j$ collision event, which means it doesn’t
need to be considered as a candidate for the next event.&lt;/p&gt;

&lt;p&gt;To predict when two balls colide, &lt;strong&gt;we need a collision condition&lt;/strong&gt;. The collision occurs when the distance between the center of
masses of the two balls is $2R$, where $R$ is the radii of the balls.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_ball_time.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_ball_time.jpg&quot; alt=&quot;ball_ball_time&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 10&lt;/strong&gt;. On overhead view of a collision trajectory of the $i\text{th}$ and $j\text{th}$ balls. The magnitude of the distance
vector is $d _{ij}(t)$, and the collision occurs when $d _{ij}$ is $2R$.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mathematically, we can
track the distance between the two balls as a function of time by defining a distance vector
$\vec{d}_{ij}(t)$:&lt;/p&gt;

\[\vec{d}_{ij}(t) = \vec{r}^{(j)}(t) - \vec{r}^{(i)}(t)
\label{dist_vec_defn}\]

&lt;p&gt;where $\vec{r}^{(i)}(t)$ is the position of the $i\text{th}$ ball and $\vec{r}^{(j)}(t)$ is the
position of the $j\text{th}$ ball. The collision condition is thus&lt;/p&gt;

\[\lvert \vec{d}_{ij}(\Delta t_E) \rvert = 2R
\label{collision}\]

&lt;p&gt;where $\Delta t_E$ is the collision time. The forms of $\vec{r}^{(i)}(t)$ and $\vec{r}^{(j)}(t)$ will depend on
the whether the balls are stationary, spinning, rolling, spinning, or airborne. Fortunately, we have
equations of motion for each of these scenarios, each of which can be cast in the following form:&lt;/p&gt;

\[\vec{r}^{(i)}(t) = 
\begin{bmatrix}
    a_x^{(i)} t^2 + b_x^{(i)} t + c_x^{(i)} \\
    a_y^{(i)} t^2 + b_y^{(i)} t + c_y^{(i)} \\
    a_z^{(i)} t^2 + b_z^{(i)} t + c_z^{(i)}
\end{bmatrix}
\label{quad_r}\]

&lt;p&gt;As you can see, each component can be expressed as a quadratic equation with respect to time (though
in many cases the $a$, $b$, and $c$ coefficients are 0). The coefficients depend on which motion state the
ball is in. For example, Eq. $\eqref{quad_r}$ for a rolling ball has the following coefficients:&lt;/p&gt;

\[a_x^{(i)} = - \frac{1}{2} \mu_r g \cos(\phi^{(i)})
\notag\]

\[a_y^{(i)} = - \frac{1}{2} \mu_r g \sin(\phi^{(i)})
\notag\]

\[a_z^{(i)} = 0
\notag\]

\[b_x^{(i)} = v_0^{(i)} \cos(\phi^{(i)})
\notag\]

\[b_y^{(i)} = v_0^{(i)} \sin(\phi^{(i)})
\notag\]

\[b_z^{(i)} = 0
\notag\]

\[c_x^{(i)} = r_{0x}^{(i)}
\notag\]

\[c_y^{(i)} = r_{0y}^{(i)}
\notag\]

\[c_z^{(i)} = 0
\notag\]

&lt;p&gt;which was determined by looking at the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-3-rolling&quot;&gt;the rolling equations of motion&lt;/a&gt;. As another example, looking at
&lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#--case-1-stationary&quot;&gt;the stationary equations of motion&lt;/a&gt;
reveals that a stationary ball has the following coefficients in Eq. $\eqref{quad_r}$:&lt;/p&gt;

\[a_x^{(j)} = a_y^{(j)} = a_z^{(j)} = 0
\notag\]

\[b_x^{(j)} = b_y^{(j)} = b_z^{(j)} = 0
\notag\]

\[c_x^{(j)} = r_{0x}^{(j)}
\notag\]

\[c_y^{(j)} = r_{0y}^{(j)}
\notag\]

\[c_z^{(j)} = 0
\notag\]

&lt;p&gt;Regardless of the particulars, the critical point is that &lt;strong&gt;a ball’s trajectory is defined
by these 9 coefficients&lt;/strong&gt;. To determine if two balls collide, we can formulate the distance vector
$\vec{d}(t)$ from Eq. $\eqref{dist_vec_defn}$ in terms of these 18 coefficients (2 balls, 9
coefficients each):&lt;/p&gt;

\[\vec{d}_{ij}(t) = 
\begin{bmatrix}
    A_x t^2 + B_x t + C_x \\
    A_y t^2 + B_y t + C_y \\
    A_z t^2 + B_z t + C_z \\
\end{bmatrix}
\label{diff_vec}\]

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

\[A_x = a_x^{(j)} - a_x^{(i)}
\label{A_x_ball}\]

\[A_y = a_y^{(j)} - a_y^{(i)}
\label{A_y_ball}\]

\[A_z = a_z^{(j)} - a_z^{(i)}
\label{A_z_ball}\]

\[B_x = b_x^{(j)} - b_x^{(i)}
\label{B_x_ball}\]

\[B_y = b_y^{(j)} - b_y^{(i)}
\label{B_y_ball}\]

\[B_z = b_z^{(j)} - b_z^{(i)}
\label{B_z_ball}\]

\[C_x = c_x^{(j)} - c_x^{(i)}
\label{C_x_ball}\]

\[C_y = c_y^{(j)} - c_y^{(i)}
\label{C_y_ball}\]

\[C_z = c_z^{(j)} - c_z^{(i)}
\label{C_z_ball}\]

&lt;p&gt;Plugging Eq. $\eqref{diff_vec}$ into the collision condition Eq. $\eqref{collision}$ yields an
equation whose roots describe the time until two arbitrarily balls collide:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Time until ball-ball collision event&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Let $\Delta t_E$ be the time until collision between the $i\text{th}$ and $j\text{th}$ balls.
$\Delta t_E$ is the smallest real and positive root to this polynomial equation:&lt;/p&gt;

\[(A_x^2 + A_y^2 + A_z^2) \, \Delta t_E^4 + \\
(2 A_x B_x + 2 A_y B_y + 2 A_z B_z) \, \Delta t_E^3 + \\
(B_x^2 + B_y^2 + B_z^2 + 2 A_x C_x + 2 A_y C_y + 2 A_z C_z) \, \Delta t_E^2 + \\
(2 B_x C_x + 2 B_y C_y + 2 B_z C_z) \, \Delta t_E + \\
C_x^2 + C_y^2 + C_z^2 - 4 R^2 = 0
\label{ball_poly}\]

  &lt;p&gt;where the coefficients are defined by Eqs. $\eqref{A_x_ball}$ - $\eqref{C_z_ball}$.&lt;/p&gt;

  &lt;p&gt;If there exists no real and positive root, then the balls do not collide.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;Eq. $\eqref{ball_poly}$ can be solved analytically or numerically to reveal if/when the $i-j$
collision event occurs. This prescription can be applied for each ball-ball pair.&lt;/p&gt;

&lt;h4 id=&quot;ball-cushion-collision-times&quot;&gt;Ball-cushion collision times&lt;/h4&gt;

&lt;p&gt;Another subset of possible events are all of the ball-cushion collisions—and
there are a lot of collisions to account for.&lt;/p&gt;

&lt;p&gt;In my model, I deconstruct the
entire cushion contact surface into a collection of straight-line cushion segments,
and potential collisions for each ball must be assessed for every cushion segment.
On a standard table with 6 pockets, there are 18 cushion segments (Figure 11). With
15 balls on the table, that is &lt;strong&gt;270&lt;/strong&gt; potential ball-cushion collision events that must
be explicitly considered as the next event.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/cushion_count.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/cushion_count.jpg&quot; alt=&quot;cushion_count&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 11&lt;/strong&gt;. On a standard table with 6 pockets, there are 18 straight-line segments that define the
cushion contact surfaces.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Overall, the treatment is very similar to the ball-ball collision time calculation: develop an expression
for the distance between the ball and the cushion as a function of time–based off the quadratic
form of the ball’s trajectory–and solve for the collision condition. I do this two ways, one which is simple, and one which is very, very, very ugly but more accurate.&lt;/p&gt;

&lt;p&gt;The first is simpler, and defines the ball-cushion collision as the moment the ball contacts a
plane that extends vertically from the cushion segment’s edge. I call this the &lt;strong&gt;collision plane&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/arena.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/arena.jpg&quot; alt=&quot;arena&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 12&lt;/strong&gt;. Visualization of collision planes (orange) that extend
vertically from the cushion edges. The planes extend infinitely in the vertical
direction (despite how they are pictured). A ball-cushion collision is assumed
to occur whenever a ball contacts a collision plane.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This assumption is convenient and mostly accurate when balls remain on or nearly on the table, but
creates false-positive collision events when balls in airborne motion states intersect the
collision plane. Even if balls remain on or nearly on the table, there is a slight inaccuracy
introduced that depends on how airborne the ball is, which is depicted in Figure 13. Nevertheless,
convenience often outweighs accuracy.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/collision_plane_error.jpg&quot; class=&quot;center-img width-50&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/collision_plane_error.jpg&quot; alt=&quot;collision_plane_error&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 13&lt;/strong&gt;. A side view of the collision plane (orange). Two mock balls (red &amp;amp; blue) are
pictured at the moment they contact the cushion edge. As seen, there is a discrepancy between when
balls contact the collision plane versus the cushion edge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The collision condition&lt;/strong&gt; for a collision of the $i\text{th}$ ball with the $j\text{th}$
cushion segment occurs when the $i\text{th}$ ball contacts the $j\text{th}$ cushion segment’s
collision plane. The most convenient way to define the collision plane is with two
points on the $xy-$plane, which can be expressed as these two vectors:&lt;/p&gt;

\[\vec{p}_1^{(j)} = \langle p_{1x}^{(j)}, \, p_{1y}^{(j)}, \, 0 \rangle
\label{p1_plane}\]

\[\vec{p}_2^{(j)} = \langle p_{2x}^{(j)}, \, p_{2y}^{(j)}, \, 0 \rangle
\label{p2_plane}\]

&lt;p&gt;The collision plane is the plane that extends vertically from the line
connecting $\vec{p}_1^{(j)}$ and $\vec{p}_2^{(j)}$. We can also define the collision plane as
a classic $y = m x + b$ scenario by using these points:&lt;/p&gt;

\[y = m x + b
\notag\]

&lt;p&gt;Channel your high school math education to express $m$ and $b$ in terms of the points:&lt;/p&gt;

\[y = \frac{p_{2y}^{(j)} - p_{1y}^{(j)}}{p_{2x}^{(j)} - p_{1x}^{(j)}} x + p_{1y}^{(j)} - \frac{p_{2y}^{(j)} - p_{1y}^{(j)}}{p_{2x}^{(j)} - p_{1x}^{(j)}} p_{1x}^{(j)}
\notag\]

&lt;p&gt;Finally, cast everything onto the left-hand side:&lt;/p&gt;

\[l_x^{(j)} x + l_y^{(j)} y + l_0^{(j)} = 0
\notag\]

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

\[l_x^{(j)} = -\frac{p_{2y}^{(j)} - p_{1y}^{(j)}}{p_{2x}^{(j)} - p_{1x}^{(j)}} \\
l_y^{(j)} = 1 \\
l_0^{(j)} = \frac{p_{2y}^{(j)} - p_{1y}^{(j)}}{p_{2x}^{(j)} - p_{1x}^{(j)}} p_{1x}^{(j)} - p_{1y}^{(j)}
\label{ls}\]

&lt;p&gt;Alright, the shortest distance between a point $(p_x, p_y, p_z)$ and the collision plane is defined by&lt;/p&gt;

\[d = \frac{\lvert l_x^{(j)} p_x + l_y^{(j)} p_y + l_0^{(j)} \rvert}{\sqrt{l_x^{(j)\, 2} + l_y^{(j)\, 2}}}
\label{dist_to_plane}\]

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line&quot;&gt;(Source)&lt;/a&gt;&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;This describes the distance between the point $(p_x, p_y, p_z)$ and a plane that extends
infinitely in the $xy-$plane, rather than a plane that terminates at the points $\vec{p}_1^{(j)}$
and $\vec{p}_2^{(j)}$. We will have to deal with this oversight in a minute.&lt;/p&gt;

&lt;p&gt;Rather than use an arbitrary point $(p_x, p_y, p_z)$, here I throw in
the trajectory of the $i\text{th}$ ball from Eq. $\eqref{quad_r}$:&lt;/p&gt;

\[d_{ij}(t) = \frac{\lvert l_x^{(j)} r_x^{(i)}(t) + l_y^{(j)} r_y^{(i)}(t) + l_0^{(j)} \rvert}{\sqrt{l_x^{(j)\, 2} + l_y^{(j)\, 2}}}
\notag\]

\[d_{ij}(t) = \frac{\lvert l_x^{(j)} (a_x^{(i)} t^2 + b_x^{(i)} t + c_x^{(i)}) + l_y^{(j)} (a_y^{(i)} t^2 + b_y^{(i)} t + c_y^{(i)}) + l_0^{(j)} \rvert}{\sqrt{l_x^{(j)\, 2} + l_y^{(j)\, 2}}}
\notag\]

&lt;p&gt;where $d_{ij}$ is the distance between the $i\text{th}$ ball and the $j\text{th}$ cushion segment.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_cushion_time.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_cushion_time.jpg&quot; alt=&quot;ball_cushion_time&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 14&lt;/strong&gt;. A top down view of a collision trajectory of the $i\text{th}$ ball with the $j\text{th}$ cushion segment.
The magnitude of the distance vector is $d _{ij}(t)$, and the collision occurs when $d _{ij}$ is
$R$–in other words when the ball contacts the collision plane (green line).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With an expression that describes the distance between the $i\text{th}$ ball and the $j\text{th}$ cushion segment, I can
mathematically express the collision condition by setting $d_{ij}$ to $R$, which by definition
happens at a time $\Delta t_E$:&lt;/p&gt;

\[\lvert l_x^{(j)} (a_x^{(i)} \Delta t_E^2 + b_x^{(i)} \Delta t_E + c_x^{(i)}) + l_y^{(j)} (a_y^{(i)} \Delta t_E^2 + b_y^{(i)} \Delta t_E + c_y^{(i)}) + l_0^{(j)} \rvert - \\
R\sqrt{l_x^{(j)\, 2} + l_y^{(j)\, 2}} = 0
\notag\]

&lt;p&gt;So this is the collision condition, which is quadratic with respect to time. &lt;strong&gt;Calculating
the roots of this quadratic will reveal if and when a collision occurs&lt;/strong&gt;. Specifically, if there are any
real, positive roots to the equation, a collision between the $i\text{th}$ ball and $j\text{th}$ cushion is a potential next event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is just one critical problem&lt;/strong&gt;. This collision condition assumes the collision plane extends infinitely in the $xy-$plane, rather than terminating at the points $\vec{p}_1^{(j)}$ and $\vec{p}_2^{(j)}$, each which define the cushion segment’s physical boundaries. This is bad news, because there will be many false-positive collision events for a given cushion segment if this is not dealt with. Check out Figure 15.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/plane_extension.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/plane_extension.jpg&quot; alt=&quot;plane_extension&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 15&lt;/strong&gt;. A ball transits the playing surface and encounters two potential cushion segments. The first cushion segment is defined by the points $(p _{1x}^{(1)}, p _{1y}^{(1)})$ and $(p _{2x}^{(1)}, p _{2y}^{(1)})$ and represents the long rail. The second cushion segment is defined by the points $(p _{1x}^{(2)}, p _{1y}^{(2)})$ and $(p _{2x}^{(2)}, p _{2y}^{(2)})$ and represents the left jaw of the side pocket. The ball’s collision with the first cushion segment is legitimate because $0 \le s^{(1)} \le 1$, whereas the collision with the second cushion segment is illegitimate because $s^{(2)} &amp;gt; 1$.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The collision plane of cushion segment 2 defines the left jaw of the side pocket. It begins at $(p _{1x}^{(2)}, p _{1y}^{(2)})$ and ends at $(p _{2x}^{(2)}, p _{2y}^{(2)})$. But because of Eq. $\eqref{dist_to_plane}$’s flawed assumption, the collision plane mathematically extends into the playing surface, and triggers false collision events with unsuspecting passersby, &lt;em&gt;e.g.&lt;/em&gt; the 11-ball.&lt;/p&gt;

&lt;p&gt;Obviously this won’t do. Fortunately, the solution is self-apparent: if the alleged collision occurs outside the boundary defined by the physical portion of the collision plane, disregard it. I needed to express this mathematically, and I did it by parameterizing a cushion segment’s collision plane with the following representation:&lt;/p&gt;

\[\vec{p}^{(j)} = \begin{bmatrix}
    p_{1x}^{(j)} + (p_{2x}^{(j)} - p_{1x}^{(j)}) \, s \\
    p_{1y}^{(j)} + (p_{2y}^{(j)} - p_{1y}^{(j)}) \, s \\
    z
\end{bmatrix}
\label{parameterized_plane}\]

&lt;p class=&quot;notice&quot;&gt;Since it is a vertical plane, $z$ is a free parameter&lt;/p&gt;

&lt;p&gt;$s$ is the parameter that defines where on the plane you lie. It’s basically a slider scale that goes from $-\infty$ to $+\infty$. But very importantly, this parameterization is defined such that $s=0$ corresponds to the coordinate $\langle p_{1x}^{(j)}, p_{1y}^{(j)}, z \rangle$ and $s=1$ corresponds to the coordinate $\langle p_{2x}^{(j)}, p_{2y}^{(j)}, z \rangle$. In other words, the range $s \in [0, 1]$ defines the physical portion of the collision plane!&lt;/p&gt;

&lt;p&gt;So to see whether or not any collision predicted by the collision condition is legitimate, we need only find the $s$ value at the time of collision. &lt;strong&gt;If $0 \le s \le 1$, the collision is legitimate&lt;/strong&gt;, and otherwise it is not and can be ignored.&lt;/p&gt;

&lt;p&gt;According to Eq. 3 of &lt;a href=&quot;https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html&quot;&gt;this source&lt;/a&gt;, the value of $s$ at the collision time can be calculated via the following equation:&lt;/p&gt;

\[s = - \frac{
    (\vec{p}_1^{(j)} - \vec{r}^{(i)}(t + \Delta t_E)) \bullet (\vec{p}_2^{(j)} - \vec{p}_1^{(j)})
}{
    (\vec{p}_2^{(j)} - \vec{p}_1^{(j)}) \bullet (\vec{p}_2^{(j)} - \vec{p}_1^{(j)})
}
\label{s_at_plane}\]

&lt;p&gt;Now we are fully equipped to detect ball-cushion collisions.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Time until ball-cushion collision event (I)&lt;/span&gt;
Let $\Delta t_E$ be the time until collision between the $i\text{th}$ ball and $j\text{th}$
cushion segment, where the collision is detected via the collision plane. $\Delta t_E$ can
be obtained by solving the roots of the following quadratic polynomial&lt;/p&gt;

\[A \Delta t_E^2 + B \Delta t_E + C = 0
\label{cushion_poly_1}\]

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

\[A = l_x^{(j)} a_x^{(i)} + l_y^{(j)} a_y^{(i)}
\notag\]

\[B = l_x^{(j)} b_x^{(i)} + l_y^{(j)} b_y^{(i)}
\notag\]

\[C = l_0^{(j)} + l_x^{(j)} c_x^{(i)} + l_y^{(j)} c_y^{(i)} \pm R \sqrt{l_x^{(j)\, 2} + l_y^{(j)\, 2}}
\notag\]

  &lt;p&gt;where $l_x^{(j)}$, $l_y^{(j)}$, and $l_0^{(j)}$ are defined by Eq. $\eqref{ls}$. For each real, positive root, it should be determined if $0 \le s \le 1$, where $s$ is given by Eq. $\eqref{s_at_plane}$. In Eq. $\eqref{s_at_plane}$, $\vec{p} _{1}^{(j)}$ and $\vec{p} _{2}^{(j)}$ are given by Eqs. $\eqref{p1_plane}$ and $\eqref{p2_plane}$ and each of their $z$ components should be set to $\vec{r} _z^{(i)}(t + \Delta t_E)$
so that there is no $z$ contribution to $s$. When calculating Eq. $\eqref{s_at_plane}$, the candidate root should be substituted in for $\Delta t_E$.&lt;/p&gt;

  &lt;p&gt;The smallest, real,
positive root where $0 \le s \le 1$ is the time until collision. If no real,
positive root satisfies this requirement, the $i\text{th}$ ball and $j\text{th}$ cushion
segment do not collide.&lt;/p&gt;
&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;As a more advanced treatment, I now consider a more physically accurate case in which the cushion segment is
treated as a line that coincides with the cushion segment’s edge, depicted in orange in Figure 16.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/cushion_line.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/cushion_line.jpg&quot; alt=&quot;cushion_line&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 16&lt;/strong&gt;. A ball in contact with a cushion. Mathematically, the ball-cushion collision is
detected by defining the cushion’s edge, highlighted in orange.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Defining a collision line rather than a collision plane allows complete
accuracy in determining collision times and permits balls to fly off the table,
thereby avoiding all false-positive ball-cushion collision events that occur
with a collision plane.&lt;/p&gt;

&lt;p&gt;The most convenient way to define the collision line of a cushion segment is to specify two points, where
the line connecting them is the collision line. The points for the collision line of the $j\text{th}$
cushion segment can be defined with the following vectors&lt;/p&gt;

\[\vec{p}_1^{(j)} = \langle p_{1x}^{(j)}, \, p_{1y}^{(j)}, \, h \rangle
\label{p1_line}\]

\[\vec{p}_2^{(j)} = \langle p_{2x}^{(j)}, \, p_{2y}^{(j)}, \, h \rangle
\label{p2_line}\]

&lt;p&gt;This time the $z-$component is not free, but rather fixed at the height of the
cushion, $h$. Here I assume $h$ is constant, but it may be fun
exploring a relaxation of this assumption later. With these 2 vectors defined,
the line can be parameterized via the following vector:&lt;/p&gt;

\[\vec{p}^{(j)} = 
\begin{bmatrix}
    p_{1x}^{(j)} + (p_{2x}^{(j)} - p_{1x}^{(j)}) \, s\\
    p_{1y}^{(j)} + (p_{2y}^{(j)} - p_{1y}^{(j)}) \, s\\
    h
\end{bmatrix}
\label{p1_vec}\]

&lt;p&gt;where $s$ determines where on the line you are.&lt;/p&gt;

&lt;p&gt;Just as the distance of a point to the
collision &lt;strong&gt;plane&lt;/strong&gt; is given by Eq. $\eqref{dist_to_plane}$, we need an equation for the
distance of a point to the collision &lt;strong&gt;line&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If such a point is described by the vector $\vec{p}_0 =
\langle p _{0x}, p _{0y}, p _{0z} \rangle$, then the minimum distance to the collision line is&lt;/p&gt;

\[d = \frac{\lvert (\vec{p}_0 - \vec{p}_1^{(j)}) \times (\vec{p}_0 - \vec{p}_2^{(j)}) \rvert}
         {\lvert \vec{p}_2^{(j)} - \vec{p}_1^{(j)} \vert}
\notag\]

&lt;p&gt;&lt;a href=&quot;https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html&quot;&gt;(Source)&lt;/a&gt; where $\times$
denotes the cross product. By replacing $\vec{p}_0$ with the trajectory of the $i\text{th}$ ball, we get
the distance of the $i\text{th}$ ball to the $j\text{th}$ cushion segment as a function of time.&lt;/p&gt;

\[d_{ij}(t) = \frac{\lvert (\vec{r}^{(i)}(t) - \vec{p}_1^{(j)}) \times (\vec{r}^{(i)}(t) - \vec{p}_2^{(j)}) \rvert}
         {\lvert \vec{p}_2^{(j)} - \vec{p}_1^{(j)} \vert}
\label{dist_to_line}\]

&lt;p&gt;Like before, &lt;strong&gt;the collision condition&lt;/strong&gt; is defined by setting $d_{ij}$ to $R$, which by definition
happens at time $\Delta t_E$. After substituting Eq. $\eqref{quad_r}$ into Eq.
$\eqref{dist_to_line}$, the algebra blows up.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_collision_line.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_collision_line.png&quot; alt=&quot;ball_collision_line&quot; class=&quot;no-border&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p class=&quot;warning&quot;&gt;This page experiences a lot of lag if I try to render this math as an html object, so I took the
above screenshot instead. &lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_collision_line&quot;&gt;Click here&lt;/a&gt; for the html version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It’s beauty no doubt challenges that of Euler’s $e^{i\pi} + 1 = 0$&lt;/strong&gt;. Whether or not you agree doesn’t
take away from the fact that this is a legitimate quartic polynomial with respect to time, the
roots of which describe the time until collision of ball $i$ and cushion segment $j$.&lt;/p&gt;

&lt;p&gt;The only thing that
needs to be done is grouping the different orders together so the solution can be presented.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Time until ball-cushion collision event (II)&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Let $\Delta t_E$ be the time until collision between the $i\text{th}$ ball and $j\text{th}$ cushion segment, where the
collision is detected via the collision line. $\Delta t_E$ can
be obtained by solving the roots of the following quartic polynomial:&lt;/p&gt;

\[A \Delta t_E^4 + B \Delta t_E^3 + C \Delta t_E^2 + D \Delta t_E + E = 0
\label{cushion_poly_2}\]

  &lt;p&gt;where $A$ is given by &lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/A_collision_line&quot;&gt;this equation&lt;/a&gt;, $B$ is given by &lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/B_collision_line&quot;&gt;this equation&lt;/a&gt;, $C$ is given by &lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/C_collision_line&quot;&gt;this equation&lt;/a&gt;, $D$ is given by &lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/D_collision_line&quot;&gt;this equation&lt;/a&gt;, and $E$ is given by &lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/E_collision_line&quot;&gt;this equation&lt;/a&gt;.&lt;/p&gt;

  &lt;p&gt;For each real, positive root, it should be determined if $0 \le s \le 1$, where $s$ is given by Eq. $\eqref{s_at_plane}$. In Eq. $\eqref{s_at_plane}$, $\vec{p} _{1}^{(j)}$ and $\vec{p} _{2}^{(j)}$ are given by Eqs. $\eqref{p1_line}$ and $\eqref{p2_line}$. When calculating Eq. $\eqref{s_at_plane}$, the candidate root should be substituted in for $\Delta t_E$.&lt;/p&gt;

  &lt;p&gt;The smallest, real,
positive root where $0 \le s \le 1$ is the time until collision. If no real,
positive root satisfies this requirement, the $i\text{th}$ ball and $j\text{th}$ cushion
segment do not collide.&lt;/p&gt;

&lt;/div&gt;

&lt;p&gt;Wow.&lt;/p&gt;

&lt;h4 id=&quot;ball-slate-collision-times&quot;&gt;Ball-slate collision times&lt;/h4&gt;

&lt;p&gt;In comparison to the ball-cushion collision, this is a breeze.&lt;/p&gt;

&lt;p&gt;The ball-slate collision occurs either when a sliding ball has a $-z-$velocity component, or an airborne ball hits the slate. The first case is somewhat pendantic, because if it has a $-z-$velocity component, the time until the ball-slate collision is by definition 0. So the real calculation is &lt;strong&gt;determining when an airborne ball hits the slate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An airborne ball hits the slate when $r_z = R$, where $R$ is the ball’s radius. According to the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/#section-iv-ball-air-interactions&quot;&gt;airborne equations of motion&lt;/a&gt;, $r_z(t)$ is given by&lt;/p&gt;

\[r_z(t) = r_{0z} + v_{0z} t - \frac{1}{2} g t^2
\notag\]

&lt;p&gt;Setting this to $R$ and solving for time yields the ball-slate collision time:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Time until ball-slate collision event&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Let $\Delta t_E$ be the time until the ball-slate collision event for a given ball.&lt;/p&gt;

  &lt;p&gt;If the ball is sliding,&lt;/p&gt;

\[\Delta t_E = \begin{cases}
    0, &amp;amp; \text{if $v_{0z} &amp;lt; 0$} \\
    \infty, &amp;amp; \text{otherwise}
\end{cases}
\notag\]

  &lt;p&gt;If the ball is airborne,&lt;/p&gt;

\[\Delta t_E = \frac{
    v_{0z} + \sqrt{v_{0z}^2 + 2 g (r_{0z} - R)}
}{
    g
}
\notag\]

&lt;/div&gt;

&lt;p class=&quot;notice&quot;&gt;Reminder, this equation is significant because for a given time $t$, the
ball-slate collision event for &lt;strong&gt;every sliding and airborne ball&lt;/strong&gt; must be
considered as a potential next event.&lt;/p&gt;

&lt;h4 id=&quot;ball-pocket-collision-times&quot;&gt;Ball-pocket collision times&lt;/h4&gt;

&lt;p&gt;So &lt;a href=&quot;#-ball-pocket-collision&quot;&gt;as mentioned&lt;/a&gt;, the ball-pocket collision exists as a means to determine when a ball is pocketed, and to remove the ball from the simulation when it is pocketed. Defining the ball-pocket collision condition requires a &lt;strong&gt;suitable geometry for pockets&lt;/strong&gt;, which I am going to be &lt;strong&gt;very rudimentary&lt;/strong&gt; about.&lt;/p&gt;

&lt;p&gt;Basically, the $j\text{th}$ pocket is going to be a circle with center $(a^{(j)}, b^{(j)})$ and radius $R^{(j)}$. Here’s an illustration because I spoil you:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/pocket_geometry.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/pocket_geometry.jpg&quot; alt=&quot;pocket_geometry&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 17&lt;/strong&gt;. A depiction of the pocket geometry (shown in orange) for the $j\text{th}$ pocket. The center is $(a^{(j)}, b^{(j)})$ and the radius is $R^{(j)}$.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;First of all, we are able to exclude any balls that are stationary, spinning, or airborne. So we are only including balls that are rolling or sliding. Excluding airborne balls simplifies things considerably, since we can assume that $r_z^{(i)}(t)$ has a constant value of $R$, and therefore we can treat the collision condition as a 2D problem.&lt;/p&gt;

&lt;p&gt;Actually, I’d like to modify the above statement slightly. If you absolutely &lt;em&gt;slam&lt;/em&gt; a ball into a pocket, it is likely to be slightly airborne, if only by a millimeter. Yet it still falls into the pocket because the back lip of the pocket is at a height higher than the ball’s radius, thus redirecting the ball &lt;em&gt;into&lt;/em&gt; the pocket. Let’s call this lip height $h_M$ and assume that any ball with $r_z(t)$ less than $h_M$ is pocketed, and any ball greater than $h_M$ isn’t. An obviously crude means of modelling what in reality is a complex rigid body dynamics problem that doesn’t easily lend itself to continuous event-based evolution.&lt;/p&gt;

&lt;p&gt;Based on my own table, it seems like a reasonable value for $h_M$ is $7R/5$.&lt;/p&gt;

&lt;p&gt;To muster up a mathematical collision condition between the $i\text{th}$ ball and the $j\text{th}$ pocket, I define the distance between the ball and the center of the pocket:&lt;/p&gt;

\[d_{ij}(t) = \sqrt{(r_x^{(i)}(t) - a^{(j)})^2 + (r_y^{(i)}(t) - b^{(j)})^2}
\notag\]

&lt;p&gt;The collision is assumed to occur at the moment the ball rolls over the table’s edge:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/pocket_edge.jpg&quot; class=&quot;center-img width-60&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/pocket_edge.jpg&quot; alt=&quot;pocket_edge&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 18&lt;/strong&gt;. Side profile of a pocket and ball during the moment of the ball-pocket collision.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At this moment, $d_{ij}(\Delta t_E) = R^{(j)}$, where $R^{(j)}$ is the radius of the pocket, not the ball. Like we’ve been doing for the ball-ball and ball-cushion collisions, we can plug this into the left-hand-side of the distance equation and solve for $\Delta t_E$:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Time until ball-pocket collision event&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Let $\Delta t_E$ be the time until collision between the $i\text{th}$ ball and the $j\text{th}$ pocket.&lt;/p&gt;

  &lt;p&gt;If the ball is spinning or stationary, $\Delta t_E = \infty$.&lt;/p&gt;

  &lt;p&gt;Otherwise, $\Delta t_E$ can be obtained by solving the roots of the following quartic polynomial:&lt;/p&gt;

\[A \Delta t_E^4 + B \Delta t_E^3 + C \Delta t_E^2 + D \Delta t_E + E = 0
\label{pocket_poly}\]

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

\[A = \frac{1}{2} ( {a_x^{(i)}}^2 + {a_y^{(i)}}^2 )
\notag\]

\[B = a_x^{(i)} b_x^{(i)} + a_y^{(i)} b_y^{(i)}
\notag\]

\[C = a_x^{(i)} (c_x^{(i)} - a^{(j)}) + a_y^{(i)} (c_y^{(i)} - b^{(j)}) + \frac{1}{2} ({b_x^{(i)}}^2 + {b_y^{(i)}}^2)
\notag\]

\[D = b_x^{(i)} (c_x^{(i)} - a^{(j)}) + b_y^{(i)} (c_y^{(i)} - b^{(j)})
\notag\]

\[E = \frac{1}{2} ({a^{(j)}}^2 + {b^{(j)}}^2 + {c_x^{(i)}}^2 + {c_y^{(i)}}^2 - R^2) - (c_x^{(i)} a^{(j)}  + c_y^{(i)} b^{(j)})
\notag\]

  &lt;p class=&quot;notice&quot;&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-alg/ball_pocket_collision_time.pdf&quot;&gt;Scrap paper of the calculation&lt;/a&gt;&lt;/p&gt;

  &lt;p class=&quot;warning&quot;&gt;I’m sorry I used $a$ and $b$ both for ball trajectory coefficients $(^{(i)})$ and for the pocket coordinates $(^{(j)})$. Writing out these equations, I now realize how brutal this is.&lt;/p&gt;

  &lt;p&gt;For each positive, real root, the height of the ball at the time $t + \Delta t_E$ should be calculated, &lt;em&gt;i.e.&lt;/em&gt; $r_z(t + \Delta t_E)$.&lt;/p&gt;

  &lt;p&gt;The smallest real, positive root that satisfied $r_z(t + \Delta t_E) \le h_M$ is the time until collision. If no real, positive roots satisfy this requirement, the $i\text{th}$ ball and $j\text{th}$ pocket do not collide.&lt;/p&gt;

&lt;/div&gt;

&lt;h3 id=&quot;5-summary&quot;&gt;(5) Summary&lt;/h3&gt;

&lt;p&gt;That was a lot of work. I think now would be a good time to consolidate all of this information about the continuous event-based evolution algorithm into a brief summary.&lt;/p&gt;

&lt;p&gt;The train of thought can be boiled down into just 5 statements.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;We have equations of motion for every ball&lt;/li&gt;
  &lt;li&gt;But events disrupt the validity of these equations&lt;/li&gt;
  &lt;li&gt;We can safely evolve all balls up until the next event occurs&lt;/li&gt;
  &lt;li&gt;To find the next event, we calculate all possible event times&lt;/li&gt;
  &lt;li&gt;The one that occurs in the least amount of time is the next event&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Essentially, we have developed these analytic equations of motion that are defined for all of the ball motion states, which assume the ball acts in complete isolation. Of course, there are events such as collisions that are not accounted for in these equations of motion that must be detected and resolved.&lt;/p&gt;

&lt;p&gt;To make sure events don’t screw up our equations of motion, we can only evolve the balls’ states up until the next event that occurs on the table. It is worth clarifying that even if a collision acts between balls 1 and 2, ball 3 can only be evolved up until the time at which balls 1 and 2 collide.&lt;/p&gt;

&lt;p&gt;Once evolved up to the event, the event must be resolved via the appropriate physics equations. Then, its rinse and repeat and the balls’ states can be evolved up until the next event.&lt;/p&gt;

&lt;p&gt;Calculating when the next event occurs involves calculating all possible events between all possible objects. By definition, the event that occurs in the smallest amount of time is chosen as the next event, and all other candidate events are discarded.&lt;/p&gt;

&lt;h3 id=&quot;6-extras&quot;&gt;(6) Extras&lt;/h3&gt;

&lt;p&gt;This section you can find some additional topics you may find of interest.&lt;/p&gt;

&lt;h4 id=&quot;number-of-candidate-events&quot;&gt;Number of candidate events?&lt;/h4&gt;

&lt;p&gt;How many candidate events are there to solve for at each step of the algorithm?&lt;/p&gt;

&lt;p&gt;This depends on the number of pockets, cushions, balls, as well as the states the balls are in. But for the sake of example, let’s consider a standard table with 6 pockets, and a standard set of 15 balls + the cue ball. As you will see later on, I decompose the cushion surface  into straight line segments, each of which must be tested for ball-cushion collisions independently. For a standard table with 6 pockets, there are 18 such segments (Figure 11). So the makeup of agents that are potentially involved in events is as follows:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;16 balls&lt;/li&gt;
  &lt;li&gt;18 cushion segments&lt;/li&gt;
  &lt;li&gt;6 pockets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these numbers, let’s calculate just how many potential event times we have to solve for at each step of the continuous event-based evolution algorithm.&lt;/p&gt;

&lt;p&gt;(1) How many potential transition events?&lt;/p&gt;

&lt;p&gt;In general, this will depend on each ball’s state. For example, there are no transition events available for a stationary ball, but there are two transition events available for a rolling ball (rolling-spinning, rolling-stationary). For simplicity, let’s assume that each ball has 1 transition state available to it. This might be the case immediately after the break, when many balls are in motion. In that case, &lt;strong&gt;we have ~16 potential transition events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(2) How many potential ball-ball events?&lt;/p&gt;

&lt;p&gt;If there are $N$ balls on the table, collisions between each combination must be checked. That means there are $N (N-1) / 2$ potential ball-ball collisions to check. &lt;strong&gt;For 16 balls that’s 120 potential events.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Of course, solving for the roots of the &lt;a href=&quot;#-ball-ball-collision-times&quot;&gt;associated quartic polynomial&lt;/a&gt; for each of these collision cases is not necessary, because when both balls are either stationary or spinning, the collision can immediately be ruled impossible.&lt;/p&gt;

&lt;p&gt;(3) How many potential ball-cushion events?&lt;/p&gt;

&lt;p&gt;With 18 cushion segments and 16 balls, each ball must be tested against each cushion segment explicitly. &lt;strong&gt;That’s $18 \times 16=288$ potential ball-cushion events&lt;/strong&gt;. This one really hurts, but decomposing the cushion surface into linear segments is a necessary evil.&lt;/p&gt;

&lt;p&gt;Just like in the ball-ball collision, solving for the roots of the &lt;a href=&quot;#-ball-cushion-collision-times&quot;&gt;associated quadratic or quartic polynomial&lt;/a&gt; is only necessary if the ball is translating.&lt;/p&gt;

&lt;p&gt;(4) How many potential ball-slate events?&lt;/p&gt;

&lt;p&gt;Ball-slate collision events are relatively rare. They are only relevant when a ball is airborne and/or has a velocity component into the table. So calculating a ball-slate collision is only necessary under these circumstances, which is going to be a pretty rare occurrence, requiring explicit calculation perhaps just a handful of times throughout the entire evolution of a shot. Just to associate a number, let’s estimate that &lt;strong&gt;we have ~1 potential transition event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;(5) How many potential ball-pocket events?&lt;/p&gt;

&lt;p&gt;With 16 balls and 6 pockets, each ball must be tested against each pocket explicitly. &lt;strong&gt;That means $16 \times 6=96$ potential ball-pocket events&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Just like the ball-cushion events, calculating the &lt;a href=&quot;#-ball-pocket-collision-times&quot;&gt;associated quartic polynomial&lt;/a&gt; can be avoided for any stationary or spinning balls.&lt;/p&gt;

&lt;p&gt;(6) How many in total?&lt;/p&gt;

&lt;p&gt;Summing these numbers up yields an upper bound for the number of potential event times which must be explicitly computed during each loop of the evolution algorithm (for a standard table and ball count). &lt;strong&gt;In total, we are looking at ~521 events&lt;/strong&gt;. A typical shot may have anywhere from 10 to 50 events, meaning that in total, we are looking within the range of 5,000-25,000 event time calculations per shot.&lt;/p&gt;

&lt;h2 id=&quot;closing-remarks&quot;&gt;Closing remarks&lt;/h2&gt;

&lt;p&gt;When I first read the Leckie and Greenspan’s paper, understanding it required hours of reading with pencil and paper in hand. It’s not really their fault–it was a well written paper. The problem is that it was written in an academic format, which favors correctness and succinctness over understandability. I hope that this post along with &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/&quot;&gt;the other&lt;/a&gt; serve as a more approachable and more fleshed out perspective on pool simulation theory.&lt;/p&gt;

&lt;p&gt;This marks the &lt;strong&gt;last theory post&lt;/strong&gt; in this blog series, and I gotta say, I am really thankful to be done writing about pool simulation theory. These posts are brutal to write due to their technical nature, so I’m glad that the next posts will be centered around actual implementation of a pool simulator, which will include a lot more code and visualization.&lt;/p&gt;

    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2020/12/20/pooltool-alg/&quot;&gt;The algorithmic theory behind pool/billiards simulation&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on December 20, 2020.&lt;/p&gt;

  </content>
</entry>


<entry>
  <title type="html"><![CDATA[Creating a virtual dog sitter with live audio processing]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2020/07/20/maple-intro/" />
  <id>https://ekiefl.github.io/2020/07/20/maple-intro</id>
  <published>2020-07-20T00:00:00+00:00</published>
  <updated>2020-07-20T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    
&lt;p&gt;I recently moved in with my girlfriend (Kourtney) who has a 6-month old puppy named Maple. She’s a sweet girl
(the puppy), however she was born in the COVID era, and that comes with some behavioral challenges.
The biggest problem is that she’s developed an unrealistic assumption that she can be with us 100%
of the time. And when that unrealistic expectation is challenged by us leaving the apartment, she
barks. Loudly. &lt;strong&gt;I wanted to create an application written in Python that monitors and responds to her
barks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By the end of this post, the program (which you have full, open-access to) will detect dog barks
using &lt;strong&gt;PyAudio&lt;/strong&gt; and make decisions on whether to praise or scold the dog based on its behavior. This is
all done &lt;strong&gt;in real time&lt;/strong&gt;. At this
point, both praising and scolding means playing a pre-recorded voice of the owner that is either of
positive or negative sentiment. The audio, statistics, and time of each bark, as well as statistics
of owner responses are stored in a &lt;strong&gt;SQLite&lt;/strong&gt; database. Finally, I’ll show some interactive plots of
the results using &lt;strong&gt;&lt;a href=&quot;https://plotly.com/python/&quot;&gt;Plotly&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;simple-demo&quot;&gt;Simple demo&lt;/h2&gt;

&lt;p&gt;It’s always good to start simple so I just searched for &lt;strong&gt;python live audio processing&lt;/strong&gt; and found
this blog post by &lt;a href=&quot;https://swharden.com/wp/2016-07-19-realtime-audio-visualization-in-python/&quot;&gt;Scott W Harden&lt;/a&gt;.
In it, he posts a simple script that monitors in real-time the amplitude of the audio signal. I
turned it into a class structure:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;pyaudio&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;argparse&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;CHUNK&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;44100&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LiveStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argparse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Namespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pyaudio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PyAudio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fromstring&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CHUNK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;process_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__enter__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pyaudio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;paInt16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;channels&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;rate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;frames_per_buffer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CHUNK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__exit__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exception_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exception_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;traceback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stop_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;terminate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;process_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;peak&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;average&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;peak&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;%05d %s&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;n&quot;&gt;peak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# uncomment to push highest amplitude frequency to stdout
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;#x = data
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;#w = np.fft.fft(x)
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;#freqs = np.fft.fftfreq(len(x))
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;#max_freq = abs(freqs[np.argmax(w)] * RATE)
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;#peak = max_freq
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;#bars=&quot;o&quot;*int(4000*peak/2**16)
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;#print(&quot;%05d %s&quot;%(peak,bars))
&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;__main__&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LiveStream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s a demo:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/QAOw4R7U4ls&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/tree/ac9515c525435f6c4500762036934e850a3ee1b0&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;h2 id=&quot;event-detection&quot;&gt;Event detection&lt;/h2&gt;

&lt;p&gt;With a bare-bones script that demos a live output of audio amplitude and/or frequency, it was time
to get serious: detecting events. I wanted to be able to detect events since in order to rationally
respond to your dog, you need to be able to detect when it is barking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Events are basically anomalies in the background noise&lt;/strong&gt;, and so to detect events, we need to
properly distinguish background from signal. To do this, I wrote a calibration method that runs at
the start of the program. The premise is to wait until the audio signal reaches an equilibrium, and
then measures the mean ($ \mu $) and standard deviation ($ \sigma $) of the signal strength.  I
consider equilibrium to be established by demanding that the coefficient of variation ($ \sigma/\mu
$) is less than some threshold value, since a low coefficient of variation means a stable signal.
The background mean and standard deviation that satisfied this constraint for equilibrium can then be
used to distinguish signal from noise.&lt;/p&gt;

&lt;p&gt;We can do some back of the envelope calculations to show that if the signal is drawn from a Normal
distribution (a bell-shaped curve), there is a ~16% chance that any given datapoint in the signal will exceed 1 standard
deviation about the mean ($\mu + \sigma$). That probability becomes ~2.5% that it will exceed 2
standard deviations ($\mu + 2\sigma$) and ~0.5% that it exceeds 3 standard deviations ($\mu +
3\sigma$). As a first step, I went ahead and &lt;strong&gt;wrote a detector that detects the start of an event
whenever the signal exceeds 3 standard deviations, and the end of the event whenever it dips below 2
standard deviations&lt;/strong&gt;. Here is a demo showing the efficacy of this approach:&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;From here on in, I started developing within a multi-file environment (the whole repo can
be found &lt;a href=&quot;github.com/ekiefl/maple&quot;&gt;here&lt;/a&gt;). To make everything accessible as possible, each code
snippet and demo video will be followed by a hyperlink that brings you to the stage in the codebase
from where the snippet or demo was taken.&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/k7zHMiv_YtY&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/tree/d923c6689f1bafc9ef7e071a6d4674983a8f8d34&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;h2 id=&quot;better-event-detection&quot;&gt;Better event detection&lt;/h2&gt;

&lt;p&gt;As mentioned in the video, there are a lot of false-positives for the ends of events. In other
words, &lt;strong&gt;many of my spoken sentences were being split up into multiple events&lt;/strong&gt;, even when there was
little or no break in my speaking rhythm. This was happening because the criterion for events ending
was too simple, and based on a single audio frame (reminder: the event ends if the mean audio signal
of an audio frame drops below $\mu + 2\sigma$, where $ \mu $ and $ \sigma $ are the mean and
standard deviation of the background noise). This is problematic because each frame is only a couple
of milliseconds. To more accurately depict the start and stop of events, I wanted to create criteria
that spanned multiple frames.&lt;/p&gt;

&lt;p&gt;In the above video, the program has just 1 state variable called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_event&lt;/code&gt;, and its possible values
are either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;. Here is a flowchart of the transitions possible:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/state_flow_1.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/state_flow_1.jpg&quot; alt=&quot;flow1&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Soon we’ll see that making a more robust event detector inevitably complicates this simple picture.&lt;/p&gt;

&lt;h3 id=&quot;event-start-criterion&quot;&gt;Event start criterion&lt;/h3&gt;

&lt;p&gt;In the video, transitioning from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_event == False&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_event == True&lt;/code&gt; occurred whenever a frame
had a mean signal 3 standard deviations above the mean. Let’s call this threshold value $X$ for
convenience. My new criterion is that there must be $N$ consecutive frames that are all above
$X$. If $N$ consecutive frames all meet this threshold, then the event start is attributed to
the first frame in this frame sequence. &lt;strong&gt;Requiring consecutive frames to pass the threshold
effectively guards against false-positives when loud but short (~ millisecond) sounds are made&lt;/strong&gt;,
which trigger an event. The stringency is thus controlled by $N$: the lower $N$ is, the more
false-positives in event starts you allow.&lt;/p&gt;

&lt;p&gt;Programmatically, whenever a frame’s mean signal exceeds $X$ while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_event == False&lt;/code&gt;, a second
state variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_on_transition&lt;/code&gt; is set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;.  Whenever &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_on_transition == True&lt;/code&gt;, it
basically means, “&lt;em&gt;Ok, we’re not for sure in an event, but things are starting to get loud, and if
we stay in this state for long enough, we’ll for sure know we are in an event&lt;/em&gt;”. If
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_on_transition == True&lt;/code&gt; for $N$ consecutive frames, then the program is convinced it is
actually an event, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_event&lt;/code&gt; is set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_on_transition&lt;/code&gt; is returned to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;. On
the other hand, if any frame fails to exceed $X$, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_on_transition&lt;/code&gt; is set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt; and the
potential event is deemed not an event. To avoid clipping the start of the event because the
detector is busy making its mind up, whenever &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_on_transition == True&lt;/code&gt;, the audio
frames are stored in a buffer and retroactively added to the event.&lt;/p&gt;

&lt;h3 id=&quot;event-end-criterion&quot;&gt;Event end criterion&lt;/h3&gt;

&lt;p&gt;Before, an event ended whenever a frame had a mean signal that dipped below 2 standard deviations
above the mean ($\mu + 2\sigma$). Call this threshold $Y$ for convenience. My new method uses
the same criterion, but rather than ending the event when this occurs, a countdown of $T$ seconds
starts. If $T$ reaches 0, the event ends.  But it is possible to “save” the event, if any frame
during the countdown has a mean signal exceeding $X$ (the event start threshold). In this case,
the event will continue until the next time the mean signal dips below $Y$.  &lt;strong&gt;The countdown
safeguards against against false-positives that end events because there was a momentary lapse in
sound amplitude&lt;/strong&gt;. The stringency is thus controlled by $T$: the lower $T$ is, the more
false-positives in event ends you allow.&lt;/p&gt;

&lt;p&gt;Programmatically, whenever a frame’s mean signal dips below $Y$ while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_event == True&lt;/code&gt;, a second
state variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_off_transition&lt;/code&gt; is set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;True&lt;/code&gt;. This starts a countdown. If any frame during
the countdown exceeds $X$, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_off_transition&lt;/code&gt; is set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;, and the event is given life
anew. But if no frames exceed $X$ during the countdown, the event is deemed to have finished, so
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_event&lt;/code&gt; is set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;in_off_transition&lt;/code&gt; is returned to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With the implementation of these new criteria, here is the updated state logic visualized as a
flow chart:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/state_flow_2.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/state_flow_2.jpg&quot; alt=&quot;flow2&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s a little more complicated, but there is a pleasant symmetry.&lt;/p&gt;

&lt;h3 id=&quot;results&quot;&gt;Results&lt;/h3&gt;

&lt;p&gt;Here is a demo of the new event detector.&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/uQBNo67m85c&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/tree/cac03c052c399b4eb2365c35892e2156500e4397&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Overall, I am happy with how it works and ready to move on.&lt;/p&gt;

&lt;h2 id=&quot;a-generic-audio-detector&quot;&gt;A generic audio detector&lt;/h2&gt;

&lt;p&gt;I noticed at this point that nothing I have done so far has anything to do with dogs and barking.
&lt;strong&gt;Being noncommittal is a great quality in a codebase because it creates flexibility&lt;/strong&gt;. For the
purpose of making this useful to anyone with their own applications, I created a well-polished
branch of the repository that can be used for generic audio event detection. I reorganized
everything into a single file, so after installing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;numpy&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyaudio&lt;/code&gt;, you are ready to rumble:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#! /usr/bin/env python
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;pyaudio&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;CHUNK&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;44100&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;calc_power&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Calculate the power of a discrete time signal&quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pyaudio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PyAudio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stream&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pyaudio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;paInt16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;channels&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;rate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;frames_per_buffer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CHUNK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# To read from stream, self.stream.start_stream must be called
&lt;/span&gt;        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__enter__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;is_active&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__exit__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exc_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exc_val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;traceback&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stop_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Close the stream gracefully&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;is_active&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stop_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;terminate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Monitor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argparse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Namespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CHUNK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Time between each sample
&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Calibration parameters
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# How many seconds is calibration window
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_threshold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.50&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Required ratio of std power to mean power
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_tries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Number of running windows tried until threshold is doubled
&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Event detection parameters
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_start_threshold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# standard deviations above background noise to start an event
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_end_threshold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# standard deviations above background noise to end an event
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.5&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_consecutive&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;background&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;background_std&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detector&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_recs&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;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_events&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;read_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Read a chunk from the stream and cast as a numpy array&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fromstring&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CHUNK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;calibrate_background_noise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Establish a background noise

        Calculates moving windows of power. If the ratio between standard deviation and mean is less
        than a threshold, signifying a constant level of noise, the mean power is chosen as the
        background. Otherwise, it is tried again. If it fails too many times, the threshold is
        increased and the process is repeated.
        &quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;stable&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;power_vals&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;c1&quot;&gt;# Number of chunks in running window based on self.calibration time
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;running_avg_domain&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;tries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;running_avg_domain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;power_vals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calc_power&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt;

                &lt;span class=&quot;c1&quot;&gt;# Test if threshold met
&lt;/span&gt;                &lt;span class=&quot;n&quot;&gt;power_vals&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;power_vals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;power_vals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;power_vals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_threshold&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;background&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;power_vals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;background_std&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;power_vals&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;

                &lt;span class=&quot;c1&quot;&gt;# Threshold not met, try again
&lt;/span&gt;                &lt;span class=&quot;n&quot;&gt;power_vals&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;tries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_tries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;c1&quot;&gt;# Max tries met--doubling calibration threshold
&lt;/span&gt;                    &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Calibration threshold not met after &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tries&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; tries. Increasing threshold &quot;&lt;/span&gt;
                          &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_threshold&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; --&amp;gt; &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1.5&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_threshold&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;tries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_threshold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.5&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibrate_background_noise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detector&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Detector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;background_std&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;background_std&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;background&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;start_thresh&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_start_threshold&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;end_thresh&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_end_threshold&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;num_consecutive&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_consecutive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wait_for_events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;wait_for_events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wait_for_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

            &lt;span class=&quot;c1&quot;&gt;# Do anything you want here
&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;wait_for_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Waits for an event, records the event, and returns the event audio as numpy array&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read_chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_finished&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get_event_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;stream_power_and_pitch_to_stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Call for every chunk to create a primitive stream plot of power and pitch to stdout

        Pitch is indicated with &apos;o&apos; bars, amplitude is indicated with &apos;-&apos;
        &quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;power&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;calc_power&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;-&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;power&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;%05d %s&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;n&quot;&gt;power&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;w&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fft&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fft&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;freqs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fft&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fftfreq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;peak&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;freqs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;argmax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;o&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3000&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;peak&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;%05d %s&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;n&quot;&gt;peak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Detector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;background_std&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;background&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start_thresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end_thresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_consecutive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                 &lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Manages the detection of events

        Parameters
        ==========
        background_std : float
            The standard deviation of the background noise.

        background : float
            The mean of the background noise.

        start_thresh : float
            The number of standard deviations above the background noise that the power must exceed
            for a data point to be considered as the start of an event.

        end_thresh : float
            The number of standard deviations above the background noise that the power dip below
            for a data point to be considered as the end of an event.

        num_consecutive : int
            The number of frames needed that must consecutively be above the threshold to be
            considered the start of an event.

        seconds : float
            The number of seconds that must pass after the `end_thresh` condition is met in order
            for the event to end. If, during this time, the `start_thresh` condition is met, the
            ending of the event will be cancelled.

        dt : float
            The inverse sampling frequency, i.e the time captured by each frame.

        quiet : bool
            If True, nothing is sent to stdout
        &quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg_std&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;background_std&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg_mean&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;background&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_consecutive&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_consecutive&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Recast the start and end thresholds in terms of power values
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start_thresh&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg_mean&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start_thresh&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg_std&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;end_thresh&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg_mean&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end_thresh&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bg_std&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update_event_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;power&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Update event states based on their current states plus the power of the current frame&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_started&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_started&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_off_transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;off_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_off_transition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_finished&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;power&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start_thresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_off_transition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;off_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dt&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;power&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;end_thresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_off_transition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;off_time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_on_transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;# Not in event
&lt;/span&gt;                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;on_counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_consecutive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_on_transition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_started&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;power&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start_thresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;on_counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_on_transition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frames&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;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;power&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start_thresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_on_transition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
                    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;on_counter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;c1&quot;&gt;# Silence
&lt;/span&gt;                    &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;print_to_stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Prints to standard out to create a text-based stream of event detection&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_off_transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;         o &apos;&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;        |||&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_on_transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;         | &apos;&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;&apos;&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_started&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;####### EVENT START #########&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_finished&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;####### EVENT END #########&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Reset event states and storage buffer&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_off_transition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_on_transition&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_finished&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_started&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frames&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;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;append_to_buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_event&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_on_transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frames&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Takes in data and updates event transition variables if need be&quot;&quot;&quot;&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Calculate power of frame
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;power&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;calc_power&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_event_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;power&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Write to stdout if not self.quiet
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;print_to_stdout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# Append to buffer
&lt;/span&gt;        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append_to_buffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_event_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;concatenate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frames&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;__main__&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Monitor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/blob/generic-event-detector/main.py&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Using this code is as easy as saving this file to a script and running it. &lt;strong&gt;It works out of the
box&lt;/strong&gt;. You can process the audio any way you want by changing the contents of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wait_for_events&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;storing-the-data&quot;&gt;Storing the data&lt;/h2&gt;

&lt;p&gt;Detecting events is merely one side of the coin. Equally important is storing the data for
downstream analyses. When deciding on a storage structure, &lt;strong&gt;always start simple&lt;/strong&gt;. So first I
considered storing everything in a simple tab-delimited file, which would have sufficed if I only
wanted to store basic knowledge like when the event happened, how long it lasted, and how loud it
was. Maybe even for some more sophisticated stuff like what frequency ranges it dominated, what
class it belonged to, etc.  But instead, I uncompromisingly wanted to retain as much of the raw data
as possible, so it was important for me to store the audio itself. This way I’m guaranteed not to be
bottlenecked by an incomplete data storage when I inevitably come up with interesting ways to
analyze data that would require going back to the raw audio. So to me, it made sense to store
everything in a database. I have familiarity with &lt;a href=&quot;https://www.sqlite.org/index.html&quot;&gt;SQLite&lt;/a&gt;, so I
wrote a bare-bones Python class to interface with SQLite that’s based off of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; module of
another repo I contribute to, &lt;a href=&quot;https://github.com/merenlab/anvio/blob/master/anvio/db.py&quot;&gt;anvio&lt;/a&gt;. It
supports basic reading and writing of data, as well as playing back stored audio. You can check it
out
&lt;a href=&quot;https://github.com/ekiefl/maple/blob/e6f5e05ada3f336e090e484e01866e72c19e30bb/maple/data.py#L16&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is a demo of the database features:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/eEKg265SXfs&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/tree/f1d476eb59011eebd5f38fc29578b3a09d6ef42a&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;In summary,&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;A database is created for each session.&lt;/li&gt;
  &lt;li&gt;Each database has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self&lt;/code&gt; table and an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;events&lt;/code&gt; table&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self&lt;/code&gt; table that contains administrative info like when the session
started, which microphone was used, and all free parameters like the event threshold parameters
$X$, $Y$, etc.&lt;/li&gt;
  &lt;li&gt;Each row of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;events&lt;/code&gt; table contains all of the info pertaining to an individual event.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is an example &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;events&lt;/code&gt; table:&lt;/p&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;event_id&lt;/td&gt;
      &lt;td&gt;t_start&lt;/td&gt;
      &lt;td&gt;t_end&lt;/td&gt;
      &lt;td&gt;t_len&lt;/td&gt;
      &lt;td&gt;energy&lt;/td&gt;
      &lt;td&gt;power&lt;/td&gt;
      &lt;td&gt;pressure_mean&lt;/td&gt;
      &lt;td&gt;pressure_sum&lt;/td&gt;
      &lt;td&gt;class&lt;/td&gt;
      &lt;td&gt;audio&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:33:52.590223&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:33:53.518187&lt;/td&gt;
      &lt;td&gt;0.927964&lt;/td&gt;
      &lt;td&gt;0.23233255845044&lt;/td&gt;
      &lt;td&gt;0.250368072953735&lt;/td&gt;
      &lt;td&gt;0.0013950959289966&lt;/td&gt;
      &lt;td&gt;0.0012945987986554&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:33:59.424781&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:33:59.797081&lt;/td&gt;
      &lt;td&gt;0.3723&lt;/td&gt;
      &lt;td&gt;0.0124660974593919&lt;/td&gt;
      &lt;td&gt;0.0334840114407519&lt;/td&gt;
      &lt;td&gt;0.000334789915565585&lt;/td&gt;
      &lt;td&gt;0.000124642285565067&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:16.613249&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:17.313352&lt;/td&gt;
      &lt;td&gt;0.700103&lt;/td&gt;
      &lt;td&gt;0.0726653914778307&lt;/td&gt;
      &lt;td&gt;0.103792429796517&lt;/td&gt;
      &lt;td&gt;0.000341944134424603&lt;/td&gt;
      &lt;td&gt;0.000239396114343068&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:17.788128&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:18.161044&lt;/td&gt;
      &lt;td&gt;0.372916&lt;/td&gt;
      &lt;td&gt;0.0286456558738386&lt;/td&gt;
      &lt;td&gt;0.0768153039125127&lt;/td&gt;
      &lt;td&gt;0.000415586707439386&lt;/td&gt;
      &lt;td&gt;0.000154978932591466&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:34.419520&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:34.792764&lt;/td&gt;
      &lt;td&gt;0.373244&lt;/td&gt;
      &lt;td&gt;0.0736813904700202&lt;/td&gt;
      &lt;td&gt;0.197408104269647&lt;/td&gt;
      &lt;td&gt;0.000704391482153759&lt;/td&gt;
      &lt;td&gt;0.000262909894364998&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:40.413646&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:40.970223&lt;/td&gt;
      &lt;td&gt;0.556577&lt;/td&gt;
      &lt;td&gt;0.147239283014793&lt;/td&gt;
      &lt;td&gt;0.264544318243106&lt;/td&gt;
      &lt;td&gt;0.000939155610160731&lt;/td&gt;
      &lt;td&gt;0.000522712412036429&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:41.438835&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:42.090197&lt;/td&gt;
      &lt;td&gt;0.651362&lt;/td&gt;
      &lt;td&gt;0.341081074757945&lt;/td&gt;
      &lt;td&gt;0.523642881773799&lt;/td&gt;
      &lt;td&gt;0.00178524750772019&lt;/td&gt;
      &lt;td&gt;0.00116284238712364&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;7&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:34:59.914203&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:02.610606&lt;/td&gt;
      &lt;td&gt;2.696403&lt;/td&gt;
      &lt;td&gt;8.75341461582365&lt;/td&gt;
      &lt;td&gt;3.24633024656316&lt;/td&gt;
      &lt;td&gt;0.00410196267867266&lt;/td&gt;
      &lt;td&gt;0.011060544472661&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:09.355492&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:11.909793&lt;/td&gt;
      &lt;td&gt;2.554301&lt;/td&gt;
      &lt;td&gt;2670.53404983006&lt;/td&gt;
      &lt;td&gt;1045.50483667746&lt;/td&gt;
      &lt;td&gt;0.0609668444851899&lt;/td&gt;
      &lt;td&gt;0.155727671835365&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;9&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:15.349941&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:15.723554&lt;/td&gt;
      &lt;td&gt;0.373613&lt;/td&gt;
      &lt;td&gt;0.0930470076768425&lt;/td&gt;
      &lt;td&gt;0.24904649376987&lt;/td&gt;
      &lt;td&gt;0.000803286108113117&lt;/td&gt;
      &lt;td&gt;0.000300118132710466&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;10&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:22.231554&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:24.185004&lt;/td&gt;
      &lt;td&gt;1.95345&lt;/td&gt;
      &lt;td&gt;4224.9895692335&lt;/td&gt;
      &lt;td&gt;2162.83476374286&lt;/td&gt;
      &lt;td&gt;0.108183158167411&lt;/td&gt;
      &lt;td&gt;0.21133039032213&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;11&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:25.261638&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:27.487578&lt;/td&gt;
      &lt;td&gt;2.22594&lt;/td&gt;
      &lt;td&gt;6242.85853608116&lt;/td&gt;
      &lt;td&gt;2804.59425504783&lt;/td&gt;
      &lt;td&gt;0.139811369221431&lt;/td&gt;
      &lt;td&gt;0.311211719204751&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;12&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:29.078094&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:30.798650&lt;/td&gt;
      &lt;td&gt;1.720556&lt;/td&gt;
      &lt;td&gt;4640.45106447108&lt;/td&gt;
      &lt;td&gt;2697.06482350535&lt;/td&gt;
      &lt;td&gt;0.13138258673385&lt;/td&gt;
      &lt;td&gt;0.226051097900447&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;13&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:34.001482&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:42.686516&lt;/td&gt;
      &lt;td&gt;8.685034&lt;/td&gt;
      &lt;td&gt;4513.27087836138&lt;/td&gt;
      &lt;td&gt;519.660703499996&lt;/td&gt;
      &lt;td&gt;0.0301028059471401&lt;/td&gt;
      &lt;td&gt;0.261443893146314&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;14&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:52.047493&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:35:52.419169&lt;/td&gt;
      &lt;td&gt;0.371676&lt;/td&gt;
      &lt;td&gt;0.0484807744715422&lt;/td&gt;
      &lt;td&gt;0.130438270083466&lt;/td&gt;
      &lt;td&gt;0.000595550670187947&lt;/td&gt;
      &lt;td&gt;0.000221351890892775&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;15&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:36:08.757406&lt;/td&gt;
      &lt;td&gt;2020-08-14 19:36:10.665634&lt;/td&gt;
      &lt;td&gt;1.908228&lt;/td&gt;
      &lt;td&gt;4241.5389432906&lt;/td&gt;
      &lt;td&gt;2222.76318306335&lt;/td&gt;
      &lt;td&gt;0.11528165374789&lt;/td&gt;
      &lt;td&gt;0.219983679568028&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;�&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event_id&lt;/code&gt; is the unique ID of the event, and increases chronologically.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t_start&lt;/code&gt; is the datetime of the event’s start.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t_end&lt;/code&gt; is the datetime of the event’s end.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t_len&lt;/code&gt; is the duration of the event in seconds.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;energy&lt;/code&gt; is the signal energy, &lt;em&gt;i.e.&lt;/em&gt; the sum of squared signal $ \sum_{i}^{n} S_i(t)^2 $, where $ S(t) $ is the audio signal. This value increases the louder the event is, or the longer the event lasts.
Through my experience it does not seem to scale proportionally with human-perceived “loudness”.
(Side note: I wanted to be able to say how many decibels each event was, but this is extremely
difficult to pin down without detailed knowledge of the microphone’s physics and circuitry).&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;power&lt;/code&gt; is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;energy&lt;/code&gt; divided by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t_len&lt;/code&gt;, &lt;em&gt;i.e.&lt;/em&gt; power is the time derivative of energy.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pressure_mean&lt;/code&gt; is a quantity I feel scales proportionally better with
human-perceived loudness. It is defined as $ \sum_{i}^{n} |S_i(t)|/n $.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pressure_sum&lt;/code&gt; is just like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pressure_mean&lt;/code&gt;, except it is not normalized by the length of the event. It is defined as $ \sum_{i}^{n} | S_i(t) | $.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;class&lt;/code&gt; merely symbolizes my aspirations of one day classifying the events. For example as bark,
“whine”, “dig”, “howl”, etc. For now, it is blank.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;audio&lt;/code&gt; is a gzipped binary object of the numpy array that represents the audio. The compression
and decompression is carried out by these two functions:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;gzip&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;convert_array_to_blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gzip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;compress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;memoryview&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;compresslevel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;convert_blob_to_array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ARRAY_DTYPE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;frombuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gzip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decompress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/blob/e6f5e05ada3f336e090e484e01866e72c19e30bb/maple/utils.py#L52&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;h2 id=&quot;dog-monitoring-trial-1&quot;&gt;Dog monitoring trial #1&lt;/h2&gt;

&lt;p&gt;With both event detection and event storage solved, it’s time for the first trial: leaving
Maple in the confines of her cage and monitoring what happens. Here she is in the crate just &lt;strong&gt;moments before her worst fear
is realized&lt;/strong&gt;: abandonment (for 30 whole minutes).&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/maple_in_cage.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/maple_in_cage.jpg&quot; alt=&quot;in_cage&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;About 10-feet away lies a &lt;a href=&quot;https://fifinemicrophone.com/blogs/news/k669b-faqs&quot;&gt;Fifine K669B USB
microphone&lt;/a&gt; I picked up for this project.
After double checking everything, I start the application:&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;./main.py run
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/blob/e6f5e05ada3f336e090e484e01866e72c19e30bb/main.py&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;… and then quietly leave the apartment.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/maple_sad.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/maple_sad.jpg&quot; alt=&quot;sad&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;results-1&quot;&gt;Results&lt;/h3&gt;

&lt;p&gt;Fast forward 30 minutes, I’m back with at the apartment with 39MB of data. At first I just wanted to
visualize on a line where all the events took place with something I’m familiar with, like
&lt;a href=&quot;https://matplotlib.org/&quot;&gt;matplotlib&lt;/a&gt; or &lt;a href=&quot;https://seaborn.pydata.org/&quot;&gt;seaborn&lt;/a&gt;, but after a quick
Google search, &lt;strong&gt;before I knew it I was learning the basics of &lt;a href=&quot;https://plotly.com/python/&quot;&gt;Plotly&lt;/a&gt;&lt;/strong&gt;,
which can be used to make interactive plots. I’m super glad I spent the time because now I can view
an interactive plot for each session. The plot shows up in your browser after typing&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;./main.py analyze
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;By default it will pick the last session, but you can pick any session by first listing saved
sessions:&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;./main.py analyze &lt;span class=&quot;nt&quot;&gt;--list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and then choosing a specific session, like:&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;./main.py analyze &lt;span class=&quot;nt&quot;&gt;--session&lt;/span&gt; 2020_08_19_16_45_18
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Doing so, yields this:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/histogram_1.html&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/histogram_1_pic.png&quot; alt=&quot;histogram_1&quot; /&gt;&lt;/a&gt;
[&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/histogram_1.html&quot;&gt;&lt;strong&gt;&lt;span style=&quot;color: red&quot;&gt;Click for interactive plot&lt;/span&gt;&lt;/strong&gt;&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The top plot shows how loud Maple was each minute. And the bottom shows
each individual event as a vertical line, where the line’s length reflects how loud the event was
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pressure_sum&lt;/code&gt;). The most striking thing is that &lt;strong&gt;Maple has outbursts followed by periods of
silence&lt;/strong&gt;. In this trial it seems like she has 3 main outbursts:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/timeline_1.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/timeline_1.png&quot; alt=&quot;timeline1&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each outburst is composed of whines, howls, and barks&lt;/strong&gt;. Most common is her whine:&lt;/p&gt;

&lt;p class=&quot;warning&quot;&gt;Audio files are shrill–turn your volume down.&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-intro/whine.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;“&lt;em&gt;i not liking this&lt;/em&gt;” - Maple&lt;/p&gt;

&lt;p&gt;Then, of course, her coveted howl:&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-intro/howl.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;“&lt;em&gt;they forgot me and i need to call them back&lt;/em&gt;” - Maple&lt;/p&gt;

&lt;p&gt;And finally, her ear-piercing bark (trademarked):&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-intro/bark_bark_howwwwl.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;“&lt;em&gt;i frantic and need to get out of this cage so I can find them&lt;/em&gt;” - Maple&lt;/p&gt;

&lt;p&gt;Uncaptured in this trial, it is known that Maple is capable of yet another form of self-expression,
in which she gnaws at the bars of her cage while grunting and foaming at the mouth. This is her most
anxiety-ridden behavior and I was happy to see she didn’t do it–hopefully it is a sign she’s already
progressed even before I had a chance to acquire data.&lt;/p&gt;

&lt;h2 id=&quot;adding-owner-responses&quot;&gt;Adding owner responses&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Missing from the first trial is any mechanism in which the program can “intervene”&lt;/strong&gt;, either by
praising or scolding based on Maple’s behavior. Since Maple, like most other dogs, is very
treat motivated, I think it would be excellent to create a treat dispenser that gives Maple treats
when she’s quiet. Figuring out the details of that is currently in the works. Until then, I decided
that it would be interesting to try and &lt;strong&gt;influence her behavior by playing pre-recorded audio&lt;/strong&gt; of
Kourtney and I either praising or scolding her based on the current state of her behavior.&lt;/p&gt;

&lt;p&gt;This venture was broken up into two parts: (1) creating something that can record the dog owner’s
voice and (2) deciding if and when to intervene.&lt;/p&gt;

&lt;h3 id=&quot;recording-voices&quot;&gt;Recording voices&lt;/h3&gt;

&lt;p&gt;At first I thought, “&lt;em&gt;I’ll just record a stream of audio in QuickTime player where I praise and
scold Maple, chop it up into individual audio clips using Logic Pro X, and then move them into
a folder that the application expects to find such audio&lt;/em&gt;”. But remember when I was talking about
non-committal code being flexible? I realized &lt;strong&gt;my codebase already does what I
want&lt;/strong&gt;: it records and clips audio events. This specifically happens in a class called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Monitor&lt;/code&gt;.
The class is big, so I won’t paste it here, but one can interact with the class in the following
way.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;monitor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Monitor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;event_audio&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;monitor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wait_for_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Calling the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wait_for_event&lt;/code&gt; method of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Monitor&lt;/code&gt; will wait patiently until the start of an event is
detected. Then it will record the audio until the event ends according to the “event detection
criteria” discussed already. After the event ends, it returns the audio, which in the above code is
captured by the variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event_audio&lt;/code&gt;. So to harness this pre-existing functionality, I simply
created another class called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RecordOwnerVoice&lt;/code&gt;, which inherits &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Monitor&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RecordOwnerVoice&lt;/code&gt;, you
guessed it, records the owner’s voice, and it does it by asking a series of questions through a very
simple command line interface (CLI). By inheriting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Monitor&lt;/code&gt;, I didn’t have to write a single line of
code related to recording audio. In fact, almost all the code in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RecordOwnerVoice&lt;/code&gt; pertains to
management of the CLI menu logic:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RecordOwnerVoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Monitor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Record and store audio clips to yell at your dog&quot;&quot;&quot;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;argparse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Namespace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quiet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Monitor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;menu&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;s&quot;&gt;&apos;home&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&apos;msg&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Press [r] to record a new sound, Press [q] to quit. Response: &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&apos;function&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;menu_handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;review&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&apos;msg&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Recording finished. [l] to listen, [r] to retry, [k] to keep. Press [q] to quit. Response: &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&apos;function&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;review_handle&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&apos;msg&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Great! type a name for your audio file (just a name, no extension). Response: &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&apos;function&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name_handle&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&apos;sentiment&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&apos;msg&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Final question. Choose the sentiment: [g] for good, [b] for bad, [w] for warn. Press [q] to quit. Response: &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&apos;function&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sentiment_handle&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;home&apos;&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OwnerRecordings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;You have &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; recordings.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;menu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;function&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;](&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;menu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;msg&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]))&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;done&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Bye.&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;menu_handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;r&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Listening for voice input...&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wait_for_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;review&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;q&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;done&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;invalid input&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;review_handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;l&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Played recording...&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;denoise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;background_audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;audio&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bandpass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;sd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;play&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;blocking&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;r&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Listening for voice input...&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wait_for_event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;k&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;name&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;q&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;done&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;invalid input&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;name_handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Try again.&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos; &apos;&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;No spaces are allowed. Try again.&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;q&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;done&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;sentiment&apos;&lt;/span&gt;


    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sentiment_handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;w&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;sentiment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;warn&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;g&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;sentiment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;good&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;b&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;sentiment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;bad&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;q&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;done&apos;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;invalid input&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;

        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recording&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sentiment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Stored voice input...&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;You now have &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; recordings.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;home&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here is a demo of it in action:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/F-clv3LGKHY&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/tree/f1d476eb59011eebd5f38fc29578b3a09d6ef42a&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

&lt;h3 id=&quot;decision-logic&quot;&gt;Decision logic&lt;/h3&gt;

&lt;p&gt;Now I’ve got 30 recordings of Kourtney and I either praising or scolding Maple. Here is an example
of a praise:&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-intro/good_GIRL_maplee.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;And here is a scold:&lt;/p&gt;

&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://ekiefl.github.io/images/maple/maple-intro/ah_ah_no.wav&quot; type=&quot;audio/wav&quot; /&gt;
Your browser does not support the audio element.
&lt;/audio&gt;

&lt;p&gt;The next thing to do is figure out when these should be sprinkled in based on Maple’s behavior. This
is an endless hole, and in the interest of keeping things pragmatic, I wanted to develop the most
simple heuristics possible that got the job done.&lt;/p&gt;

&lt;h4 id=&quot;praise&quot;&gt;Praise&lt;/h4&gt;

&lt;p&gt;First up, when to praise… To get a sense of things, I studied the interactive plots and decided
upon 4 parameters for praising:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;praise_response_window&lt;/code&gt;&lt;/strong&gt; is how big of a time window should be looked at when considering to
praise. I chose a default of 2 minutes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;praise_max_events&lt;/code&gt;&lt;/strong&gt; is the maximum number of events that can be within
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;praise_response_window&lt;/code&gt; in order to consider praising. In other words, too many events equals no
praise. I chose a default of 10.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;praise_max_pressure_sum&lt;/code&gt;&lt;/strong&gt; sets a threshold for the loudest that any given event in
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;praise_response_window&lt;/code&gt; can be in order to consider praising. If any event has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pressure_sum&lt;/code&gt;
(read: loudness) above this value, no praise for you. I chose a default of 0.15, but the
magnitude is arbitrary and depends on the microphone, its placement, and its settings. To keep it
consistent, I place the microphone in the same place each time and keep all settings consistent.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;praise_cooldown&lt;/code&gt;&lt;/strong&gt; is how much time has passed since the last praise to in order consider
praising. I chose a default of 2 minutes. This prevents rapid-fire praising.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Together, these criteria create the following flowchart, which is processed after each event, or
every 10 seconds (which happens first):&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/praise_flowchart.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/praise_flowchart.jpg&quot; alt=&quot;flow3&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;scold&quot;&gt;Scold&lt;/h4&gt;

&lt;p&gt;Next, we have scolding. I decided to use these 4 parameters for scolding:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold_response_window&lt;/code&gt;&lt;/strong&gt; is how big of a time window should be looked at when considering to
scold. I chose a default of 1 minute.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold_threshold&lt;/code&gt;&lt;/strong&gt; sets a threshold for whether or not enough noise has been made within
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold_response_window&lt;/code&gt; to consider scolding. If the sum of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pressure_sum&lt;/code&gt; values for all events
within &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold_response_window&lt;/code&gt; exceeds this value, the threshold is passed and scolding is
considered. I chose a default of 1.8. For context, in the first trial you can see this was satisfied
4 times.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold_trigger&lt;/code&gt;&lt;/strong&gt; defines how loud the triggering event has to be in order to be in order to
scold. If the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pressure_sum&lt;/code&gt; of the last event exceeds this value, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold_threshold&lt;/code&gt; is
also met, Maple will be scolded. This is to ensure that she is scolded immediately after an
especially loud sound, rather than after a wimper. I chose a default value of 0.15.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scold_cooldown&lt;/code&gt;&lt;/strong&gt; is how much time has passed since the last scold to in order consider
scolding. I chose a default of 3 minutes. This prevents rapid-fire scolding.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Together, these criteria create the following flowchart, which is processed after each event:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/scold_flowchart.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/scold_flowchart.jpg&quot; alt=&quot;flow3&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;implementation&quot;&gt;Implementation&lt;/h4&gt;

&lt;p&gt;I implemented the decision logic for praising and scolding. If the program decides to respond, a
clip with the appropriate sentiment is randomly chosen and played. To store these “owner” events, I
added an additional table to the session databases that includes all of the owner response data:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/owner_response_db.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/owner_response_db.png&quot; alt=&quot;flow3&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Save the headache, consolidate your tunables&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;By this point in the project, I have a lot of tunable variables, and so I wanted to consolidate them
in one place. To do this, I created a YAML file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config&lt;/code&gt; that contains all of my tunable
parameters. It looks like this:&lt;/p&gt;

  &lt;div class=&quot;language-yaml 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;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;general&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# microphone&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;microphone = Built-in Microphone&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Recalibrate after this many minutes has passed&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;recalibration_rate = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;10000&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Store events into DB whenever this many dog events occur. No reason to set this lower than 100&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;max_buffer_size = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;100&lt;/span&gt;

&lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;respond&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Should there be owner responses? If 0, all other parameters in this section are irrelevant&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;should_respond = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Should the owner praise?&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;praise = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The timeframe when considering if to praise (minutes)&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;praise_response_window = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The max number of events that should be within timeframe to consider praising&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;praise_max_events = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The maximum pressure sum of any individual event in the timeframe to consider praising&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;praise_max_pressure_sum = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0.01&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# After praising, wait this many minutes to consider praising again.&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;praise_cooldown = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Should the owner scold?&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;scold = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The timeframe when considering if to scold (minutes)&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;scold_response_window = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1.0&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The sum of pressure sums in timeframe to consider scolding&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;scold_threshold = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1.8&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The pressure sum of the event required to trigger scolding&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;scold_trigger = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0.15&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# After scolding, wait this many minutes to consider scolding again.&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;scold_cooldown = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;

&lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;calibration&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The length of the audio in seconds that is used to calibrate&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;calibration_time = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The standard deviation divided by the mean of the audio signal must be less than this value to be considered calibrated&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;calibration_threshold = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0.3&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# After this many failed attemps, the calibration_threshold will be increased by 0.1 and the process is repeated.&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;calibration_tries = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;

&lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;detector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Standard deviations above background noise to consider start an event&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;event_start_threshold = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The number of chunks in a row that must exceed event_start_threshold in order to start an event&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;num_consecutive = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Standard deviations above background noise to end an event&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;event_end_threshold = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# The number of seconds after a chunk dips below event_end_threshold that must pass for the event to&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# end. If during this period a chunk exceeds event_start_threshold, the event is sustained&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;seconds = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0.25&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# If an event lasts longer than this many seconds, everything is recalibrated&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;hang_time = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;20&lt;/span&gt;

&lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;analysis&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# how many seconds should each bin be&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;bin_size = &lt;/span&gt;&lt;span class=&quot;m&quot;&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

  &lt;p&gt;To keep things organized, they are placed under section headings like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[general]&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[analysis]&lt;/code&gt;,
etc. To make these parameters readily accessible in my multi-file project, I added the following to
the module’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__init__.py&lt;/code&gt;:&lt;/p&gt;

  &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;configparser&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ast&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Load up the configuration file, store as nested dictionary `config`
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__file__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;config&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;config_obj&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;configparser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ConfigParser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;config_obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;config&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;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;section&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;config_obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sections&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;section&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;config_obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;section&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;section&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;literal_eval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;section&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;
  &lt;p&gt;(&lt;a href=&quot;https://github.com/ekiefl/maple/blob/f1d476eb59011eebd5f38fc29578b3a09d6ef42a/maple/__init__.py#L19&quot;&gt;Browse code&lt;/a&gt;)&lt;/p&gt;

  &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;configparser&lt;/code&gt; is in the built-in library and parses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ast&lt;/code&gt; tries its best to interpret
the parameter values as either strings, integers, or floats. By putting this in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__init__.py&lt;/code&gt;
file, whenever &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maple&lt;/code&gt; is imported, parameters in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config&lt;/code&gt; are immediately accessible:&lt;/p&gt;

  &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;maple&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maple&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;general&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;microphone&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Built-in Microphone&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;recalibration_rate&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;max_buffer_size&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;respond&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;should_respond&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;praise&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;praise_response_window&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&apos;praise_max_events&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;praise_max_pressure_sum&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.01&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;praise_cooldown&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;scold&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&apos;scold_response_window&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;scold_threshold&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;scold_trigger&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;scold_cooldown&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&apos;warn&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;warn_response_window&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.25&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;warn_cooldown&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;calibration&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;calibration_time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&apos;calibration_threshold&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;calibration_tries&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;detector&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;event_start_threshold&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;&apos;num_consecutive&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;event_end_threshold&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;seconds&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.25&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;hang_time&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;analysis&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;bin_size&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

  &lt;p&gt;Consolidating your variables into an easy-to-access location is essential.&lt;/p&gt;
&lt;/div&gt;

&lt;h2 id=&quot;trial-2&quot;&gt;Trial #2&lt;/h2&gt;

&lt;p&gt;With what is hopefully a properly implemented praise/scold framework, I decided it was time for
trial #2. The intention of this trial is to do as I did previously, except this time &lt;strong&gt;Maple will be
scolded and/or praised&lt;/strong&gt; based on the decision logic outlined above.&lt;/p&gt;

&lt;p&gt;Will praising help quiet her down, or will it initiate outbreaks? Will scolding curb an outbreak or
aggravate her? I was so excited to find out.&lt;/p&gt;

&lt;p&gt;I set up the speaker and placed it near her. I made sure the praises were soft and soothing while
the scold clips were loud and assertive while not being scary loud. When I was happy, I started the
run and left the apartment.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/maple_sad.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/maple_sad.jpg&quot; alt=&quot;sad&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One hour after Maple’s least favorite pastime, here are the results:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/histogram_2.html&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/maple/maple-intro/histogram_2_pic.png&quot; alt=&quot;histogram_2&quot; /&gt;&lt;/a&gt;
[&lt;a href=&quot;https://ekiefl.github.io/images/maple/maple-intro/histogram_2.html&quot;&gt;&lt;strong&gt;&lt;span style=&quot;color: red&quot;&gt;Click for interactive plot&lt;/span&gt;&lt;/strong&gt;&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The plot is just as before, except scolds (red) and praises (green) have been overlaid in the bottom
plot. Within the first 15 minutes, Maple clearly has two very loud outbursts. To my delight as a
programmer, Maple was indeed scolded during these two outbursts. But unfortunately, it seems like
scolding if anything only aggravated her. I am also happy to see that praises occur during periods
of silence.&lt;/p&gt;

&lt;p&gt;Although this data is too anecdotal to say whether or not scolding/praising really makes a
difference, it at least verifies that the decision logic for praising and scolding makes sense, and
I’m not totally off base. But to really do some interesting stuff, I’m going to need a lot more data…&lt;/p&gt;

&lt;h2 id=&quot;a-longitudinal-study-is-underway&quot;&gt;A longitudinal study is underway&lt;/h2&gt;

&lt;p&gt;So far, I’ve developed a considerable framework for data acquisition and I’m very happy with
the infrastructure. Yet at this point there is almost no data to work with, so over the next few
months, I plan to measure Maple’s activity level and see how she progresses over time. Once I feel
there are enough data, my plan is to analyze how she has (hopefully) improved, and whether
or not verbal praising and/or scolding is effective. In the meantime I may begin working on crafting a DIY
treat dispenser. Regardless of what happens next, I’ll detail it all in the next blog post of this
series, which I’ll link here.&lt;/p&gt;

&lt;p&gt;Bye.&lt;/p&gt;

    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2020/07/20/maple-intro/&quot;&gt;Creating a virtual dog sitter with live audio processing&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on July 20, 2020.&lt;/p&gt;

  </content>
</entry>


<entry>
  <title type="html"><![CDATA[The physics of pool/billiards]]></title>
  <link rel="alternate" type="text/html" href="https://ekiefl.github.io/2020/04/24/pooltool-theory/" />
  <id>https://ekiefl.github.io/2020/04/24/pooltool-theory</id>
  <published>2020-04-24T00:00:00+00:00</published>
  <updated>2020-04-24T00:00:00+00:00</updated>
  <author>
    <name>Evan Kiefl</name>
    <uri>https://ekiefl.github.io</uri>
    <email>kiefl.evan@gmail.com</email>
  </author>
  <content type="html">
    
&lt;p&gt;This post is the first of many in my journey to make a realistic pool/billiards simulator called &lt;em&gt;pooltool&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Before jumping into code, I have to trudge through the theory of both (a) the physics of pool,
and (b) the algorithms for evolving a pool shot. &lt;strong&gt;The physics of pool is what is covered in this
post&lt;/strong&gt;, and algorithms for evolving a pool shot covered in the &lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/&quot;&gt;second post&lt;/a&gt;. Both of these will
contain a lot of equations, and little if any code. If this sounds uninteresting to you, skip ahead
to &lt;a href=&quot;FIXME&quot;&gt;the third post&lt;/a&gt; in this series. With that said, let’s get started.&lt;/p&gt;

&lt;p&gt;A lot has happened since I initially wrote this post, and &lt;strong&gt;pooltool is now a fully fledged 3D game/tool&lt;/strong&gt;. The equations in this post form the underlying physics of pooltool.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_2.png&quot; class=&quot;center-img width-100&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-graphics/gallery_2.png&quot; alt=&quot;pooltoolnow&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-physics-of-billiards&quot;&gt;&lt;strong&gt;The physics of billiards&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;The thing about pool is that it’s pretty old. This is one of the first historical depictions of
billiards, which dates back to 1674.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/1674.png&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/1674.png&quot; alt=&quot;charles_cotton&quot; /&gt;&lt;/a&gt;
&lt;em&gt;Two blokes shootin’ the shit over a game of billiards. &lt;a href=&quot;https://commons.wikimedia.org/w/index.php?curid=6903484&quot;&gt;Source&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While these jolly chaps were smacking balls together, at the the same time in history Isaac Newton was busy inventing
calculus, his self-titled Newtonian physics, and a universal theory of gravitation that wholly
explained the previously disparate phenomena of tides, why things fall, and the motions of celestial
bodies. His contributions to science would spark a revolution in the physical sciences more
illustrious than anyone before him, and arguably ever since him.&lt;/p&gt;

&lt;p&gt;But most importantly by far, &lt;strong&gt;Newton’s
work would enable the theoretical treatment of the game of billiards&lt;/strong&gt;, and as such, the game has been
studied for at least 200 years.&lt;/p&gt;

&lt;p&gt;As a result of its long history, the theoretical treatment of billiards has been sufficiently solved
for most scenarios: &lt;strong&gt;(1)&lt;/strong&gt; ball-cloth interactions, aka how the balls slide, spin, roll, and lie on
the table; &lt;strong&gt;(2)&lt;/strong&gt; ball-ball interactions aka how balls collide with one another; &lt;strong&gt;(3)&lt;/strong&gt;
ball-cushion interactions, aka how balls bounce off rails; &lt;strong&gt;(4)&lt;/strong&gt; ball-air interactions, aka how
the ball behaves when it becomes airborne due to jump shots, etc.; and &lt;strong&gt;(5)&lt;/strong&gt; ball-slate
interactions, aka how the ball bounces on the table.&lt;/p&gt;

&lt;p&gt;I’m pretty sure the above list covers every phenomenon
that happens on (and off) the table. The jury is still out on the finer details of these
interactions, but each of these individual
scenarios have analytical solutions that are accurate to “sufficient” degree. What I mean by
sufficient is that all qualitative effects a pro-player may be expecting to observe manifest
directly from the equations.&lt;/p&gt;

&lt;p&gt;To build a pool simulator, I’m gonna have to get my hands dirty with these equations. I decided having a
reference source was necessary, so I bought the non-exhaustive but heavily referenced modern day
treatment of billiards, “&lt;em&gt;The Physics of Pocket Billiards&lt;/em&gt;” by Wayland C. Marlow. The physics used in this post
comes partly from this book, and partly from random sources on the internet.&lt;/p&gt;

&lt;p&gt;In what follows, I am going to lay out &lt;strong&gt;all&lt;/strong&gt; of the physics I’m going to include in the
simulation.&lt;/p&gt;

&lt;h2 id=&quot;section-i-ball-cloth-interactions&quot;&gt;&lt;strong&gt;Section I&lt;/strong&gt;: ball-cloth interactions&lt;/h2&gt;

&lt;p&gt;In this section I review the ball-cloth interaction, aka how pool balls interact with their playing
surface.&lt;/p&gt;

&lt;p&gt;It is somewhat obvious that the cloth provides a frictional surface that slows the ball’s
motion. Yet, depending on the ball’s spin state, this same friction also leads to curved
trajectories due to the application of forces orthogonal to the ball’s motion. So modelling
the ball-cloth interaction is essential for realism, and quickly gets complicated.&lt;/p&gt;

&lt;p&gt;Let’s go over
some models from least to most realistic.&lt;/p&gt;

&lt;h3 id=&quot;1-no-friction&quot;&gt;(1) No friction&lt;/h3&gt;

&lt;p&gt;This is the null model. The friction coefficients at the point of contact (PoC) between ball and
cloth are 0. This means no energy dissipates from the ball, and it rolls indefinitely. It also spins
indefinitely.&lt;/p&gt;

&lt;h3 id=&quot;2-spinless-ball&quot;&gt;(2) Spinless ball&lt;/h3&gt;

&lt;p&gt;In this model, a frictional force exists between the cloth and ball that opposes the ball’s motion.
This means the ball dissipates energy over time and eventually come to a halt. Sounds like pool to
me.&lt;/p&gt;

&lt;p&gt;However, what’s unrealistic about this model is that the ball has &lt;strong&gt;no spin&lt;/strong&gt;, which is
impossible for a ball moving on a cloth with friction. To see why, let’s look at the force
contributions that act on the ball:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/force_body_diagram.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/force_body_diagram.jpg&quot; alt=&quot;force_body_diagram&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 1&lt;/strong&gt;. Force contributions acting on a ball that has o spin. Here, $ \vec{v} $ is the
velocity of the ball, $ m $ its mass, and $ R $ its radius.  Additionally, we got good old $ \vec{g} $,
the gravitational constant, and the normal force $ \vec{N} $.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this example, pretend the ball initially has no spin (&lt;em&gt;e.g.&lt;/em&gt; it’s not rolling) but is moving
in the $ +x $-direction with a speed $ |\vec{v}| $.&lt;/p&gt;

&lt;p&gt;In the $ y $-axis, there is a
gravitational force (mass $ \times $ gravitational constant $ = m\vec{g} $) pulling the ball
into the table. Since the table is supporting the ball, it exerts an equal and opposite force onto
the ball. This is called the normal force, $ \vec{N} $. Without it, the ball would fall through
the table.&lt;/p&gt;

&lt;p&gt;So even while the ball remains perfectly still on the table, there’s a perpetual
tug-of-war between the ball wanting to accelerate towards the center of the earth, and the table
stopping it from doing so. This contention results in friction whenever the ball moves along the
table. The ball and cloth essentially rub each other the wrong way as the ball moves, and so a
frictional force is exerted on the ball in a direction opposite the ball’s motion, that is denoted
here as $ \vec{F}_f $.&lt;/p&gt;

&lt;p&gt;So what makes the ball spin? Well, since $ \vec{F}_f $ is applied at the point of contact (PoC) between
ball and cloth, this creates a torque on the ball that causes it to rotate. Intuitively,
the bottom of the ball is slowing down, but the top of the ball isn’t, so it ends up going head
over heels.&lt;/p&gt;

&lt;p&gt;To demonstrate this, I took a slow-mo shot of an object ball being struck head on with the cue ball.&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/8Wng2cUH8as&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;Directly after impact, the object ball has a non-zero velocity and no spin. But quickly over time,
the ball transitions from sliding across the cloth (no spin) to rolling across the cloth (yes
spin). I hope it’s convincing footage.&lt;/p&gt;

&lt;p&gt;Wrapping things up for this model, where spin is ignored, you
can imagine that instead of the frictional force being applied at the PoC, it’s applied at the
ball’s center. Then, there is no torque on the ball, and therefore no spin. This provides a
mechanisms that slows the balls down, so it checks that box for realism.&lt;/p&gt;

&lt;p&gt;Overall, this is the kind of model
you can expect from a pool game that offers a primitive “overhead” perspective, since it provides a
passable playing experience for beginners and is simple to code.&lt;/p&gt;

&lt;h3 id=&quot;3-ball-with-arbitrary-spin&quot;&gt;(3) Ball with arbitrary spin&lt;/h3&gt;

&lt;p class=&quot;notice&quot;&gt;From this point on, I’ll refer to a ball with &lt;strong&gt;spin&lt;/strong&gt; as a ball with &lt;strong&gt;angular velocity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this example, I take on the general case of the ball-cloth interaction. This is the most
realistic model I came across that can be solved analytically, and has the following assumptions:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The ball can be in an arbitrary state (but must be on the table)&lt;/li&gt;
  &lt;li&gt;There is a single point of contact (PoC) between ball and cloth&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By &lt;em&gt;the ball can be in an arbitrary state&lt;/em&gt;, what I mean is that it can have an arbitrary velocity
$(\vec{v})$, angular velocity $(\vec{\omega})$, and displacement relative to some origin
$(\vec{r})$. These 3 vectors fully characterize the state of the ball.&lt;/p&gt;

&lt;p&gt;The goal is to find
equations of motion that can evolve these 3 vectors through time. Essentially, these equations are
functions that, when given an initial state $(\vec{v}_0$, $\vec{\omega}_0$, $\vec{r}_0)$, can give
you an updated state $(\vec{v}$, $\vec{\omega}$, $\vec{r})$ some time $t$ later.&lt;/p&gt;

&lt;p&gt;As for the second assumption, a single point of contact is a fairly accurate assumption, but
technically the weight of the ball bunches up the cloth as it moves. The amount of bunching up depends on
how loosely the cloth is stretched over the slate. Additionally, the cloth itself can be compressed,
and cloth fibers and other non-idealities can contact the ball at multiple points. And so in
actuality there does not exist a &lt;strong&gt;point of contact&lt;/strong&gt;, but rather, an &lt;strong&gt;area of contact&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/depression.png&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/depression.png&quot; alt=&quot;depression&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 2&lt;/strong&gt;. The cloth is a compressible surface, and so in actuality there does not exist a “point of
contact”, but rather, an “area of contact”.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The most important thing to realize is that throughout a
ball’s trajectory, it will always be in any of these 4 different &lt;strong&gt;modes&lt;/strong&gt;: &lt;strong&gt;sliding, rolling, spinning,stationary, or airborne&lt;/strong&gt; (all of these states occur during the ball-cloth interaction, with the exception of the airborne state, which is covered &lt;a href=&quot;#section-iv-ball-air-interactions&quot;&gt;later on&lt;/a&gt;). The physics is different for each of these cases, so I tackle them piecewise from least
to most complicated.&lt;/p&gt;

&lt;h4 id=&quot;case-1-stationary&quot;&gt;Case 1: Stationary&lt;/h4&gt;

&lt;p&gt;If the ball is stationary, the ball stays where it is and there is no angular or linear momentum. In
other words:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Stationary equations of motion&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Displacement:&lt;/p&gt;

\[\vec{r}(t) = \vec{r}_0 \label{stationary_r}\]

  &lt;p&gt;Velocity:&lt;/p&gt;

\[\vec{v}(t) = \vec{0} \label{stationary_p}\]

  &lt;p&gt;Angular velocity:&lt;/p&gt;

\[\vec{\omega}(t) = \vec{0} \label{stationary_o}\]

  &lt;p&gt;Validity:&lt;/p&gt;

  &lt;p&gt;$0 \le t &amp;lt; \infty$.&lt;/p&gt;

&lt;/div&gt;

&lt;p class=&quot;notice&quot;&gt;It’s important to keep in mind each of Eqs. $\eqref{stationary_r}$,
$\eqref{stationary_p}$, and $\eqref{stationary_o}$ are vector equations that can be broken down into
3 scalar equations each, one for each spatial dimension. For example, Eq. $\eqref{stationary_o}$ can
be written as $\omega_x(t) = 0$, $\omega_y(t) = 0$, and $\omega_z(t) = 0$. I interchangeably use both
scalar and vector equations, so make sure you are spotting the difference.&lt;/p&gt;

&lt;h4 id=&quot;case-2-spinning&quot;&gt;Case 2: Spinning&lt;/h4&gt;

&lt;p&gt;Spinning is a commonly observed ball state in which there is no linear momentum of the
ball, yet it is spinning like a top:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/A9mweRTxGiw&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;Like in the stationary state, the ball has no linear momentum:&lt;/p&gt;

\[\vec{v}(t) = \vec{v} \notag\]

&lt;p&gt;Likewise, the ball remains in place:&lt;/p&gt;

\[\vec{r}(t) = \vec{r}_0 \notag\]

&lt;p&gt;However, since the ball is rotating, it has an angular velocity. You’ll notice that the ball spins
around the $z-$axis, relative to the coordinate system in Figure 3:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/spinning_diagram_1.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/spinning_diagram_1.jpg&quot; alt=&quot;spinning_diagram_1&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 3&lt;/strong&gt;. A ball spinning in place. In this coordinate system the table is in the $xy-$plane.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spinning around the $z-$axis is not by chance–it is a constraint of the state, since if the ball had any components of its
angular velocity in the $x$ or $y$ directions, it would create a friction with the cloth that would
translate into a linear velocity. Since spinning is characterized by 0 linear velocity
$(\vec{v}=\vec{0})$, angular velocity is strictly in the $z-$axis. In other words,&lt;/p&gt;

\[\omega_x(t) = 0 \notag\]

\[\omega_y(t) = 0 \notag\]

\[\omega_z(t) = \text{ }??? \notag\]

&lt;p&gt;To characterize the $z-$axis angular velocity, I need to introduce some bull****.&lt;/p&gt;

&lt;p&gt;I told you that in this model, there is a single PoC between ball and cloth. If such were &lt;em&gt;truly&lt;/em&gt; the case, there
is nothing to stop the ball from spinning forever (besides air, which I will ignore).&lt;/p&gt;

&lt;p&gt;This is
because a ball spinning in place has &lt;strong&gt;zero speed&lt;/strong&gt; at the infinitesimally small PoC. In
other words, the relative velocity between the ball and cloth at the PoC is 0, and this means
there can exist no frictional force.&lt;/p&gt;

&lt;p&gt;Of course, everyone knows that the ball &lt;em&gt;does&lt;/em&gt; slow, which
is proof that there does not exist a point of contact but rather an area of contact.&lt;/p&gt;

&lt;p&gt;Rather than explicitly define an area of contact, which would greatly complicate the physics, we
account for this embarrassing blunder of the model by introducing a phenomenological friction
parameter that slows down the $z-$component of the ball’s angular velocity over time.&lt;/p&gt;

&lt;p&gt;A phenomenological parameter, you say? It’s a parameter that is added to a model &lt;em&gt;ad hoc&lt;/em&gt;, that explains a
phenomenon (in this case, the slowing down of a ball’s rotation) that does not come from assumptions
of the model. It’s what people do when they want to model an observation but their model is bad and
does not cause the observation. Basically, its cheating.&lt;/p&gt;

&lt;p&gt;After adding a phony friction term, we have our equations of motion solved:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Spinning equations of motion&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Displacement:&lt;/p&gt;

\[\vec{r}(t) = \vec{r}_0 \label{spinning_r}\]

  &lt;p&gt;Velocity:&lt;/p&gt;

\[\vec{v}(t) = \vec{0} \label{spinning_p}\]

  &lt;p&gt;Angular velocity:&lt;/p&gt;

\[\omega_x(t) = 0 \label{spinning_ox}\]

\[\omega_y(t) = 0 \label{spinning_oy}\]

\[\omega_z(t) = \omega_{0z} - \frac{5\mu_{sp}g}{2R}t \label{spinning_oz}\]

  &lt;p&gt;Validity:&lt;/p&gt;

  &lt;p&gt;$0 \le t \le \frac{2R}{5\mu_{sp}g}\omega_{0z}$.&lt;/p&gt;

&lt;/div&gt;

&lt;p&gt;In Eq. $\eqref{spinning_oz}$, $\omega_{0z}$ is angular velocity in the $z-$axis at $t=0$, $\mu_{sp}$
is the coefficient of spinning friction, $g$ is the gravitational constant, and $R$ is the ball’s
radius. The equation states that as time evolves, there is a linear decay in the ball’s angular
velocity. Collectively, these equations are valid until the ball stops rotating, which happens when
$\omega_z(t)$ is $0$. This occurs when $t=(2R\omega_{0z})/(5\mu_{sp}g)$.&lt;/p&gt;

&lt;h4 id=&quot;case-3-rolling&quot;&gt;Case 3: Rolling&lt;/h4&gt;

&lt;p&gt;Think of rolling as driving your car on concrete, whereas sliding would be like driving your car on
ice.&lt;/p&gt;

&lt;p&gt;In the former, your tires grip the road such that at the point of contact, there is no relative
velocity between the tire and the road; each time the tire does one rotation, your car translates
the circumference of your tire.&lt;/p&gt;

&lt;p&gt;But on ice, there is a lot of slippage between the
tire and the ice, and therefore a relative velocity; each time your tire does one rotation, your car
moves far less than one circumference of your tire.&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;In physics textbooks, what I call rolling is actually called &lt;em&gt;rolling without slippage&lt;/em&gt; and what I call
sliding is actually called &lt;em&gt;rolling with slippage&lt;/em&gt;. Sorry for the confusing terminology.&lt;/p&gt;

&lt;p&gt;Alright, so let’s get on with it. Consider a ball that is &lt;em&gt;rolling&lt;/em&gt; in the positive $x-$direction:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/rolling_diagram_1.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/rolling_diagram_1.jpg&quot; alt=&quot;rolling_diagram_1&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 4&lt;/strong&gt;. A rolling ball, that moves in the $x-$direction. The left panel shows a bird’s eye
view, and the right panel shows a profile view. In this coordinate system, the
angular velocity is in the $y-$direction.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Such a ball will move in a straight line until it comes to a rest. As it travels, it will be slowed
down by a frictional force proportional to its velocity, which implies that its velocity will decay
linearly with time:&lt;/p&gt;

\[\vec{v}(t) = \vec{v}_0 - \mu_r g t \hat{v}_0 \label{rolling_velocity}\]

&lt;p&gt;Here, $\mu_r$ is the coefficient of rolling friction, $g$ is the gravitational constant, and
$\hat{v}_0$ is the unit vector that points in the direction of the ball’s travel (according to this
coordinate system, $\hat{v}_0 = \hat{i}$).&lt;/p&gt;

&lt;p&gt;Integrating this equation with respect to time yields the displacement as a function of time:&lt;/p&gt;

\[\vec{r}(t) = \vec{r}_0 + \vec{v}_0 t - \frac{1}{2} \mu_r g t^2 \hat{v}_0 \notag\]

&lt;hr /&gt;

&lt;p&gt;Now for angular velocity. To discuss this, I should formalize the concept of rolling, which is
formally defined as the state in which the &lt;strong&gt;relative velocity&lt;/strong&gt;, $\vec{u}(t)$, between the ball and
cloth at the PoC is $\vec{0}$.&lt;/p&gt;

&lt;p&gt;$\vec{u}(t)$ has two contributions: (1) the linear velocity of
the ball, &lt;em&gt;i.e.&lt;/em&gt; the velocity of the center of mass, and (2) the velocity between ball and cloth
that exists because of the ball’s rotation. Their sum defines the relative velocity:&lt;/p&gt;

\[\vec{u}(t) = \vec{v}(t) + R \hat{k} \times \vec{\omega}(t) \label{rel_vel}\]

&lt;p&gt;For example, here is a shot where I tried to make sure the cue ball &lt;em&gt;only&lt;/em&gt; has linear
velocity:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/Z7ghvKcEDIc&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p class=&quot;warning&quot;&gt;I think we all agree there is some amount of rotation, but let’s just ignore it.&lt;/p&gt;

&lt;p&gt;Since the ball does not rotate, there is no angular velocity, so Eq.  $\eqref{rel_vel}$ reduces to&lt;/p&gt;

\[\vec{u}(t) = \vec{v}(t) \notag\]

&lt;p&gt;Similarly, here is a shot that &lt;em&gt;only&lt;/em&gt; has a velocity between ball and cloth only due to the ball’s rotation.&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/G_aaXbdJavc&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;Right at the moment of contact, the cue ball spins &lt;em&gt;in place&lt;/em&gt; and therefore has no center of mass
velocity. In this instant, Eq. $\eqref{rel_vel}$ becomes&lt;/p&gt;

\[\vec{u}(t) = R \hat{k} \times \vec{\omega}(t) \notag\]

&lt;p&gt;In the particular case shown, the ball has top spin, so the cross product dictates that $\vec{u}$
points in the direction opposite the cue ball’s travel.&lt;/p&gt;

&lt;p&gt;Both of the above examples are cases in which $\vec{u}(t) \ne \vec{0}$, so are therefore cases of
sliding, not rolling. To be rolling $(\vec{u}(t) = \vec{0})$, these contributions must match each other:&lt;/p&gt;

\[-R \hat{k} \times \vec{\omega}(t) = \vec{v}(t) \label{roll_condition}\]

&lt;p&gt;This refers to the condition in which every time the ball does a complete rotation about the
$y-$axis (according to the axes defined in Figure 4), the ball must travel exactly one circumference
(aka $2 \pi R$ in the $x-$axis). Unless this exact condition is met, a moving ball is &lt;em&gt;sliding&lt;/em&gt;, not
&lt;em&gt;rolling&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For how particular this condition seems, it is interesting that in the game of pool,
balls are most often rolling. The reason is that any sliding ball experiences friction that reduces
the magnitude of $\vec{u}(t)$ until it is rolling. In that sense, rolling is somewhat of an
equilibrium state.&lt;/p&gt;

&lt;p&gt;Now that there is a mathematical condition for rolling, &lt;em&gt;i.e.&lt;/em&gt; Eq. $\eqref{roll_condition}$, there is a lot
we can learn about the angular velocity. According to our coordinate system in Figure 4, the RHS of
Eq. $\eqref{roll_condition}$ is strictly in the $+x-$direction. That means the LHS must also be
strictly in the $+x-$direction. Expanding the cross product on the LHS yields:&lt;/p&gt;

\[-R\hat{k} \times \vec{\omega}(t) = R \begin{bmatrix} \omega_y(t) \\ -\omega_x(t) \\ 0 \end{bmatrix} \notag\]

&lt;p&gt;3 really important things result from this equation:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;In order for the RHS to point in the $+x-$direction, as it must, $\omega_x(t)$ is necessarily 0.
So no angular velocity in the direction of motion.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Using Eq. $\eqref{roll_condition}$, it follows that $\omega_y(t) = |\vec{v}(t)|/R$. Since
$\vec{v}(t)$ is known via Eq. $\eqref{rolling_velocity}$, this equation solves the time
evolution of $\omega_y(t)$. Note that $\omega_y(t)$ is strictly positive, which intuitively
refers to the fact that in order to be rolling, the ball must have &lt;em&gt;top spin&lt;/em&gt;, not &lt;em&gt;back spin&lt;/em&gt;.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;$\omega_z(t)$ is absent from this equation, which means that it is a &lt;em&gt;free parameter&lt;/em&gt;: it can
take any value.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Points 1 &amp;amp; 2 solve the time evolution for $\omega_x(t)$ and $\omega_y(t)$, respectively. Meanwhile, point
3 has consequences that you may find highly surprising. For example, we know that in the
rolling state, the ball path is a straight line. Yet the model predicts this is true regardless of
$\omega_z$. Is that really sensical? It may not match your initial intuition, but it does match the
reality:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/sDPNKuwax14&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;In the above example I apply a shit load of clockwise side spin $(\omega_z(t) &amp;lt; 0)$ and as you can
see, the ball follows a straight line. Pretty hard to deny, but it might still be at odds with what
you know about pool. For example, look at O’Sullivan and the gang “swerving” the ball by applying
side spin:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/89g7sQ7zNqo&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;So what’s the difference between those shots, and the one I manufactured? The fundamental difference
is that these players are elevating their cue, which causes $\omega_x$ to be non-zero (angular
velocity &lt;em&gt;in the direction&lt;/em&gt; of motion). This “barrel-roll” rotation is what causes curved
trajectories, otherwise known as swerve or masse. (More on the cue-ball interaction later). On the other hand, my shot did not have any significant amount of $\omega_x$, so whatever
small amount existed quickly dissipated within fractions of a second, yielding an otherwise straight trajectory.&lt;/p&gt;

&lt;p&gt;The takeaway is that $\omega_z(t)$ is decoupled from everything else, and evolves according to Eq.
$\eqref{spinning_oz}$, which was dealt with in Case 2. The only thing left to do is write down the
equations for a given frame of reference. Let’s use a frame of reference that is centered
about the ball’s &lt;em&gt;initial&lt;/em&gt; center of mass coordinates. Then,&lt;/p&gt;

&lt;p&gt;Displacement:&lt;/p&gt;

\[\vec{r}(t) = (v_0 t - \frac{1}{2} \mu_r g t^2) \, \hat{v}_0 \label{rolling_r_general}\]

&lt;p&gt;Velocity:&lt;/p&gt;

\[\vec{v}(t) = (v_0 - \mu_r g t) \, \hat{v}_0  \label{rolling_v_general}\]

&lt;p&gt;Angular velocity:&lt;/p&gt;

\[\vec{\omega}_{xy}(t) = \hat{k} \times \frac{\vec{v}(t)}{R} \label{rolling_oxy_general}\]

\[\omega_z(t) = \omega_{0z} - \frac{5\mu_{sp}g}{2R}t \label{rolling_oz_general}\]

&lt;p&gt;where $v_0$ is the initial speed of the ball. Since angular velocity has 2 decoupled components,
$\vec{\omega}(t)$ is represented by 2 equations.  $\vec{\omega} _{xy} (t)$ defines the angular
velocity projected onto the $xy-$plane, which is parallel with the table.&lt;/p&gt;

&lt;p&gt;The above equations are defined in terms of $\hat{v}_0$, which can point direction in the $xy-$plane.
If I take a frame of reference in which ball motion is in the $+x-$direction, I can drop the
vector notation:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Rolling equations of motion (&lt;strong&gt;ball coordinates&lt;/strong&gt;)&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Displacement:&lt;/p&gt;

\[r_x(t) = v_0 t - \frac{1}{2} \mu_r g t^2 \label{rolling_rx_ball}\]

\[r_y(t) = 0 \label{rolling_ry_ball}\]

\[r_z(t) = 0 \label{rolling_rz_ball}\]

  &lt;p&gt;Velocity:&lt;/p&gt;

\[v_x(t) = v_0 - \mu_r g t \label{rolling_vx_ball}\]

\[v_y(t) = 0 \label{rolling_vy_ball}\]

\[v_z(t) = 0 \label{rolling_vz_ball}\]

  &lt;p&gt;Angular velocity:&lt;/p&gt;

\[\omega_x(t) = 0 \label{rolling_ox_ball}\]

\[\omega_y(t) = \frac{v_x(t)}{R} \label{rolling_oy_ball}\]

\[\omega_z(t) = \omega_{0z} - \frac{5\mu_{sp}g}{2R}t \label{rolling_oz_ball}\]

  &lt;p&gt;Validity:&lt;/p&gt;

  &lt;p&gt;$0 \le t \le \frac{\lvert \vec{v}_0 \rvert}{\mu_r g}$. If $\frac{2R}{5\mu _{sp} g}
\omega _{0z} &amp;lt; \frac{\lvert \vec{v}_0 \rvert}{\mu_r g}$, then $\omega_z(t) = 0$ for $t &amp;gt;
\frac{2R}{5\mu _{sp} g} \omega _{0z}$.&lt;/p&gt;

&lt;/div&gt;

&lt;p&gt;The chosen frame of reference (centered at ball’s initial coordinates, x-axis in direction of
motion) is convenient, but annoying to deal when you are interested in knowing how the ball evolves
&lt;strong&gt;in relation to the table coordinates&lt;/strong&gt;. Suppose $\hat{v}_0$ relates to the table in the following
way:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/table_coordinates.jpg&quot; class=&quot;center-img width-30&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/table_coordinates.jpg&quot; alt=&quot;table_coordinates&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 5&lt;/strong&gt;. Coordinate system in which the table is described. $\phi$ relates the ball’s unit vector
of motion, $\hat{v}_0$, to the table coordinates. The origin (0,0) is the bottom left pocket.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then $\hat{v}_0$ can be expressed in terms of the table coordinates via the following rotation
matrix:&lt;/p&gt;

\[R = \begin{bmatrix}
    \cos\phi &amp;amp; -\sin\phi &amp;amp; 0 \\
    \sin\phi &amp;amp; \cos\phi &amp;amp; 0 \\
    0 &amp;amp; 0 &amp;amp; 1
\end{bmatrix}
\label{rot_mat}\]

&lt;p&gt;We can rotate Eqs. $\eqref{rolling_rx_ball}$-$\eqref{rolling_oz_ball}$ via Eq. $\eqref{rot_mat}$
and subsequently add an initial displacement vector $\vec{r}_0$ to Eqs. $\eqref{rolling_rx_ball}$-$\eqref{rolling_rz_ball}$ in
order to rewrite the rolling equations of motion in the table coordinate system:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Rolling equations of motion (&lt;strong&gt;table coordinates&lt;/strong&gt;)&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Displacement:&lt;/p&gt;

\[r_x(t) = r_{0x} + v_0 \cos(\phi) \, t - \frac{1}{2} \mu_r g \cos(\phi) \, t^2 \label{rolling_rx_table}\]

\[r_y(t) = r_{0y} + v_0 \sin(\phi) \, t - \frac{1}{2} \mu_r g \sin(\phi) \, t^2 \label{rolling_ry_table}\]

\[r_z(t) = 0 \label{rolling_rz_table}\]

  &lt;p&gt;Velocity:&lt;/p&gt;

\[v_x(t) = v_0 \cos(\phi) - \mu_r g \cos(\phi) \, t \label{rolling_vx_table}\]

\[v_y(t) = v_0 \sin(\phi) - \mu_r g \sin(\phi) \, t \label{rolling_vy_table}\]

\[v_z(t) = 0 \label{rolling_vz_table}\]

  &lt;p&gt;Angular velocity:&lt;/p&gt;

\[\omega_x(t) = - \frac{1}{R} \lvert \vec{v}(t) \rvert \sin(\phi) \label{rolling_ox_table}\]

\[\omega_y(t) = \frac{1}{R} \lvert \vec{v}(t) \rvert \cos(\phi) \label{rolling_oy_table}\]

\[\omega_z(t) = \omega_{0z} - \frac{5\mu_{sp}g}{2R}t \label{rolling_oz_table}\]

  &lt;p&gt;Validity:&lt;/p&gt;

  &lt;p&gt;$0 \le t \le \frac{\lvert \vec{v}_0 \rvert}{\mu_r g}$. If $\frac{2R}{5\mu _{sp} g}
\omega _{0z} &amp;lt; \frac{\lvert \vec{v}_0 \rvert}{\mu_r g}$, then $\omega_z(t) = 0$ for $t &amp;gt;
\frac{2R}{5\mu _{sp} g} \omega _{0z}$.&lt;/p&gt;
&lt;/div&gt;

&lt;h4 id=&quot;case-4-sliding&quot;&gt;Case 4: Sliding&lt;/h4&gt;

&lt;p class=&quot;notice&quot;&gt;If you’re ended up here without reading Case 3 (the rolling case), you might consider briefing specifically the
section on relative velocity, otherwise this won’t make any sense.&lt;/p&gt;

&lt;p&gt;Sliding occurs whenever there is a non-zero relative velocity, $\vec{u}(t)$,
between the ball and cloth at the point of contact. If you need to know whether you’re sliding, take this
zaney flowchart questionnaire:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/are_you_sliding.jpg&quot; class=&quot;center-img width-50&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/are_you_sliding.jpg&quot; alt=&quot;are_you_sliding&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 6&lt;/strong&gt;. Determine whether or not you are sliding.  $\lvert \vec{v} \rvert$ is the speed of the
ball, $\omega _{\parallel}$ is the angular velocity in the direction of motion, and $\omega _{\bot}$ is the
angular velocity that is both orthogonal to the direction of motion and parallel to the table. Note
that as discussed in Case 3, $\omega_z$ does not influence $\vec{u}(t)$, and therefore has no impact
on whether or not you are sliding.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If there is any curvature whatsoever in the trajectory of a ball, it occurs while the ball is
sliding (assuming the table is perfectly level). If you shoot a draw or stun shot, the cue ball is
sliding. Directly after an object ball is struck by the cue ball, it is in a sliding state until
friction with the cloth drives it into the rolling state. Starting to get the idea? Good.&lt;/p&gt;

&lt;p&gt;What separates the sliding case from the rolling case is that $\vec{u}(t)$ can point in any
direction in the $xy-$plane. The most important thing to note about this case is that whichever
direction $\vec{u}(t)$ points, a frictional force opposes it. This can lead to curved trajectories.
To see how, consider Figure 7, in which a ball is initially moving in the $+x-$direction, along with angular
velocity also in the $+x-$direction. This is the kind of spin that a screw has when screwed into
wood, where the spin of the screw is about the same axis as the axis of motion–a rather unrealistic
spin to impart on a ball in the game of pool, but useful for the purposes of demonstration:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/sliding_diagram.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/sliding_diagram.jpg&quot; alt=&quot;sliding_diagram&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 7&lt;/strong&gt;. A ball moving in the $+x-$direction with angular velocity also in the $+x-$direction. The
left panel shows a bird’s eye view, and the right panel shows a side view of the table and ball, as
if looking down the barrel of the cue stick. Because $\vec{\omega}(t)$ is parallel to $\vec{v}(t)$,
the $R \hat{k} \times \vec{\omega}(t)$ component in Eq. $\eqref{rel_vel}$ is orthogonal to
$\vec{v}(t)$. The below paragraph defines the force terms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I am used to thinking about friction opposing the ball’s center of mass motion, and that frictional
force is still present in the sliding case and is shown in Figure 7 as $\vec{F}_S$ (straight-line
force).  Yet, the $R \hat{k} \times \vec{\omega}(t)$ contribution to $\vec{u}(t)$ (see Eq.
\eqref{rel_vel}) also creates a frictional force, $\vec{F}_C$ (curved-line force). The sum of these
two force terms, $\vec{F} = \vec{F}_C + \vec{F}_S$, yields the net frictional force manifesting from
the ball-cloth interaction, and is anti-parallel to the relative velocity, $\vec{u}(t)$. (Note that
because $\vec{\omega}(t)$ was exactly parallel to $\vec{v}(t)$, $\vec{F}_C$ and $\vec{F}_S$ are
orthogonal, but in general this is not true). Since there exists a force component, $\vec{F}_C$,
which is orthogonal to the ball’s velocity, &lt;strong&gt;this ball will begin curving to the right&lt;/strong&gt; (the
negative $y-$direction)! The ball will continue to curve until $|\vec{u}(t)| \rightarrow 0$, at
which point the ball enters the rolling state, where it will spend the rest of its days
transiting a line. The equation governing $|\vec{u}(t)| \rightarrow 0$ is&lt;/p&gt;

\[\vec{u}(t) = (u_0 - \frac{7}{2} \mu_s g t ) \, \hat{u}_0
\label{rel_vel_evo}\]

&lt;p&gt;where $u_0$ is the magnitude of $\vec{u}(t=0)$ and $\mu_s$ is the sliding coefficient of friction.&lt;/p&gt;

&lt;p&gt;To establish the equations of motion for the sliding case, let’s again use a frame of reference
centered about the ball’s &lt;em&gt;initial&lt;/em&gt; center of mass coordinates. Additionally, I assume the &lt;em&gt;initial&lt;/em&gt;
center of mass velocity, $\vec{v}(0)$, points in the $+x-$direction. Then,&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Sliding equations of motion (&lt;strong&gt;ball coordinates&lt;/strong&gt;)&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Displacement:&lt;/p&gt;

\[\vec{r}(t) = \vec{v}_0 \, t - \frac{1}{2} \mu_s g t^2 \, \hat{u}_0 \label{sliding_r_ball}\]

  &lt;p&gt;Velocity:&lt;/p&gt;

\[\vec{v}(t) = \vec{v}_0 - \mu_s g t \, \hat{u}_0  \label{sliding_p_ball}\]

  &lt;p&gt;Angular velocity:&lt;/p&gt;

\[\vec{\omega}_{xy}(t) = \vec{\omega}_{0xy} - \frac{5 \mu_s g}{2 R} \, t \, (\hat{k} \times \vec{u}_0) \label{sliding_parallel_ball}\]

\[\omega_z(t) = \omega_{0z} - \frac{5\mu_{sp}g}{2R}t \label{sliding_perp_ball}\]

  &lt;p&gt;Validity:&lt;/p&gt;

  &lt;p&gt;$0 \le t \le \frac{2}{7}\frac{u _0}{\mu _s g}$. If $\frac{2R}{5\mu _{sp} g}
\omega _{0z} &amp;lt; \frac{2}{7}\frac{u _0}{\mu _s g}$, then $\omega_z(t) = 0$ for $t &amp;gt;
\frac{2R}{5\mu _{sp} g} \omega _{0z}$.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;These are essentially the same as the rolling equations, except the acceleration terms in the
$xy-$plane act in the $\hat{u}_0$ direction instead of the $\hat{v}_0$ direction, and the rolling
coefficient of friction $\mu_r$ is replaced with the sliding coefficient of friction $\mu_s$.&lt;/p&gt;

&lt;p&gt;We can express these in table coordinates by applying the rotation matrix (Eq. $\eqref{rot_mat}$),
which yields&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Sliding equations of motion (&lt;strong&gt;table coordinates&lt;/strong&gt;)&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Displacement:&lt;/p&gt;

\[r_x(t) = r_{0x} + v_0 \cos(\phi) \, t \\ - \frac{1}{2} \mu_s g \, ( u_{0x} \cos(\phi) - u_{0y} \sin(\phi) ) \, t^2 \label{sliding_rx_table}\]

\[r_y(t) = r_{0y} + v_0 \sin(\phi) \, t \\ - \frac{1}{2} \mu_s g \, ( u_{0x} \sin(\phi) + u_{0y} \cos(\phi) ) \, t^2 \label{sliding_ry_table}\]

\[r_z(t) = 0 \label{sliding_rz_table}\]

  &lt;p&gt;Velocity:&lt;/p&gt;

\[v_x(t) = v_0 \cos(\phi) \\ - \mu_s g \, ( u_{0x} \cos(\phi) - u_{0y} \sin(\phi) ) \, t \label{sliding_vx_table}\]

\[v_y(t) = v_0 \sin(\phi) \\ - \mu_s g \, ( u_{0x} \sin(\phi) + u_{0y} \cos(\phi) ) \, t \label{sliding_vy_table}\]

\[v_z(t) = 0 \label{sliding_vz_table}\]

  &lt;p&gt;Angular velocity:&lt;/p&gt;

\[\omega_x(t) =
    \omega_{0x} \cos(\phi) - \omega_{0y} \sin(\phi) \\ +
    \frac{5 \mu_s g}{2R} (u_{0y} \cos(\phi) + u_{0x} \sin(\phi)) \, t
\label{sliding_ox_table}\]

\[\omega_y(t) =
    \omega_{0x} \sin(\phi) + \omega_{0y} \cos(\phi) \\ +
    \frac{5 \mu_s g}{2R} (u_{0y} \sin(\phi) - u_{0x} \cos(\phi)) \, t
\label{sliding_oy_table}\]

\[\omega_z(t) = \omega_{0z} - \frac{5\mu_{sp}g}{2R}t \label{sliding_oz_table}\]

  &lt;p&gt;Validity:&lt;/p&gt;

  &lt;p&gt;$0 \le t \le \frac{2}{7}\frac{u _0}{\mu _s g}$. If $\frac{2R}{5\mu _{sp} g}
\omega _{0z} &amp;lt; \frac{2}{7}\frac{u _0}{\mu _s g}$, then $\omega_z(t) = 0$ for $t &amp;gt;
\frac{2R}{5\mu _{sp} g} \omega _{0z}$.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;This concludes the section of ball-cloth interactions, at least for now.&lt;/p&gt;

&lt;h2 id=&quot;section-ii-ball-ball-interactions&quot;&gt;&lt;strong&gt;Section II&lt;/strong&gt;: ball-ball interactions&lt;/h2&gt;

&lt;p&gt;This section is dedicated to the collision physics between two balls.&lt;/p&gt;

&lt;p&gt;When physically modelling a
phenomenon, the sky is the limit in terms of how real you want to get. In consideration of the
ball-ball interaction, a complete classical description would entail treating the balls as
compressible objects–perhaps even modelling the pressure waves that emanate within each ball during
a collision. Perhaps this treatment is most necessary during the break shot, and I would be very
interested to know how the degree of realism of such a treatment compares to the more pragmatic
approaches I will be taking. Speaking of which, here are the two models I will present:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Elastic, instantaneous, frictionless&lt;/li&gt;
  &lt;li&gt;Elastic, instantaneous (TODO)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In each of these models, multi-ball collisions are not considered, &lt;em&gt;i.e.&lt;/em&gt; each interaction is
pairwise.&lt;/p&gt;

&lt;h3 id=&quot;1-elastic-instantaneous-frictionless&quot;&gt;(1) Elastic, instantaneous, frictionless&lt;/h3&gt;

&lt;p&gt;In this model, collisions are perfectly elastic, which means no energy dissipates as a result of the
collision. This is not true for several reasons. First of all, pool balls make noise when they
collide, and those sound waves are a form of energy dissipation from the system. Then there is heat
generated via the collision, another form of energy dissipation. Are there more instances of energy
dissipation? Those are the ones I can think of anyways.&lt;/p&gt;

&lt;p&gt;Another assumption is that the balls interact instantaneously. Like everything, pool balls are
viscoelastic objects and have some minute degree of compressibility. When objects compress, they
exhibit a spring-like response, like how this bouncy ball compresses into the ground:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/tTt886y0rWI&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;As seen in the clip, this creates an interaction between the bouncy ball and the ground that lasts a
finite period of time. Pool balls are subject to the same phenomenon, to a degree order&lt;strong&gt;s&lt;/strong&gt; of
magnitude less exaggerated. However slight the effect may be, in reality pool balls interact over a
finite period of time, a time that this model will ignore.&lt;/p&gt;

&lt;p&gt;The final assumption is that the ball-ball interaction is frictionless, &lt;em&gt;i.e.&lt;/em&gt; perfectly slippery.
This implies that there is no transfer of spin from one ball to another, which is commonly known as
throw.&lt;/p&gt;

&lt;p class=&quot;notice&quot;&gt;The frictionless assumption is the worst of these assumptions, since friction between balls is what
causes spin- and cut-induced throw, which are effects that exhibit substantial influence on shot
outcome, and must be accounted for by amateurs and pros alike. In the next model, I will account for
friction between balls.&lt;/p&gt;

&lt;p&gt;For this model, I first tackle the simple scenario in which a moving ball strikes a stationary ball.
Then, I handle the general case of 2 moving balls.&lt;/p&gt;

&lt;h4 id=&quot;case-1-stationary-ball&quot;&gt;Case 1: stationary ball&lt;/h4&gt;

&lt;p&gt;Assuming the elastic, instantaneous, and frictionless model, consider a moving ball that hits a
stationary ball, shown in Figure 8:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/ball_ball_collision_1.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/ball_ball_collision_1.jpg&quot; alt=&quot;ball_ball_collision_1&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 8&lt;/strong&gt;. Ball A (blue) strikes ball B (red) with an incoming velocity $\vec{v}_0$ in the
$+x-$direction. The unfilled circle shows where ball A ends up striking ball B. During contact, the
line connecting the centers of the two balls (the line of centers) forms an angle $\alpha$ with
$\vec{v}_0$. The outgoing velocity of ball B, $\vec{v}_B$, runs along the line of centers, and the
outgoing velocity of ball A, $\vec{v}_B$, is perpendicular to $\vec{v}_B$.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If we imagine that Ball A is the cue ball and ball B is an object ball, this represents a “cut shot”
of $\alpha$ degrees. We would like to know how to resolve this collision. What do I mean by
“resolve”? I mean, Given the state of the balls the moment &lt;em&gt;before&lt;/em&gt; the collision, what is the state
of the balls the moment &lt;em&gt;after&lt;/em&gt; the collision. Just like in &lt;a href=&quot;#section-i-ball-cloth-interactions&quot;&gt;Section
I&lt;/a&gt;, the state of a ball is defined by its position, velocity,
and angular velocity.&lt;/p&gt;

&lt;p&gt;Some of these I can bang out right away.
Suppose the collision happens between $t = \tau$ and $t = \tau + dt$, keeping in mind instantaneity
of the collision dictates that $dt$ is infinitesimally small. There is thus no amount of time for
the balls to change position in the moments immediately before and after the collision. Therefore&lt;/p&gt;

\[\vec{r}_A(\tau+dt) = \vec{r}_A(\tau) \notag\]

\[\vec{r}_B(\tau+dt) = \vec{r}_B(\tau) \notag\]

&lt;p&gt;One down. Since I’m not accounting for friction effects, there is no loss or change in angular velocity due
to the collision:&lt;/p&gt;

\[\vec{\omega}_A(\tau+dt) = \vec{\omega}_A(\tau) \notag\]

\[\vec{\omega}_B(\tau+dt) = \vec{\omega}_B(\tau) \notag\]

&lt;p&gt;Two down. This leaves only the velocities of the balls, which certainly do change. To solve the outgoing
velocities, I’m going to need some conservation of momentum and energy. Figure 8 details the scenario above. For
convenience, the coordinate system is defined so that $\vec{v}_0$ points in the $+x-$direction.&lt;/p&gt;

&lt;p&gt;Before getting into equations, what’s clear immediately is that we know the outgoing direction of
Ball B just from geometry: it is parallel to the line that connects the centers of the balls at the
moment of impact. That’s because this line marks the direction of force that Ball A applies to Ball
B. Now, let’s figure out the outgoing direction of Ball B.&lt;/p&gt;

&lt;p&gt;According to conservation of linear momentum, the momentum before the collision equals the momentum
after it:&lt;/p&gt;

\[m\vec{v}_0 = m\vec{v}_A + m\vec{v}_B
\notag\]

\[\vec{v}_0 = \vec{v}_A + \vec{v}_B
\label{p_1}\]

&lt;p&gt;Concurrently, conservation of energy states that the energy before the collision equals the energy after it&lt;/p&gt;

\[E(\tau) = E(\tau + dt)
\label{con_energy}\]

&lt;p&gt;Since the model ignores angular momentum transfer, we need only account for the kinetic energy resulting from
linear translation of the balls. Plugging kinetic energy terms into Eq. $\eqref{con_energy}$ yields&lt;/p&gt;

\[\frac{1}{2} m \lvert \vec{v}_0 \rvert ^2 = \frac{1}{2} m \lvert \vec{v}_A \rvert ^2 + \frac{1}{2} m \lvert \vec{v}_B \rvert ^2
\notag\]

\[\lvert \vec{v}_0 \rvert ^2 = \lvert \vec{v}_A \rvert ^2 + \lvert \vec{v}_B \rvert ^2
\notag\]

\[\vec{v}_0 \cdot \vec{v}_0 = \lvert \vec{v}_A \rvert ^2 + \lvert \vec{v}_B \rvert ^2
\label{E_1}\]

&lt;p&gt;Plugging Eq. $\eqref{p_1}$ into the LHS of Eq. $\eqref{E_1}$ yields something very interesting:&lt;/p&gt;

\[(\vec{v}_A + \vec{v}_B) \cdot (\vec{v}_A + \vec{v}_B) = \lvert \vec{v}_A \rvert ^2 + \lvert \vec{v}_B \rvert ^2
\notag\]

\[\lvert \vec{v}_A \rvert ^2 + \lvert \vec{v}_B \rvert ^2  +  2 \, \vec{v}_A \cdot \vec{v}_B = \lvert \vec{v}_A \rvert ^2 + \lvert \vec{v}_B \rvert ^2
\notag\]

\[\vec{v}_A \cdot \vec{v}_B = 0
\label{perp}\]

&lt;p&gt;The inner product of $\vec{v}_A$ and $\vec{v}_B$ is 0, which means that outgoing velocities of the 2
balls are $90^{\circ}$ to one another! Since the direction of $\vec{v}_B$ is known from geometry,
the direction of $\vec{v}_A$ is known as well. To determine the magnitudes, I superpose the 3
velocity vectors on top of each other:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/ball_ball_velocity_vectors.jpg&quot; class=&quot;center-img width-50&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/ball_ball_velocity_vectors.jpg&quot; alt=&quot;ball_ball_velocity_vectors&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 9&lt;/strong&gt;. Geometrical relationships between $\vec{v}_0$, $\vec{v}_A$, and $\vec{v}_B$.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This diagram contains 3 critical pieces of information.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;$\vec{v}_B$ makes an angle, $\alpha$, with the incoming velocity, $\vec{v}_0$. This is known
because Ball A imparts an impulse to Ball B in the direction parallel to the line connecting
their two centers of mass.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The sum of $\vec{v}_A$ and $\vec{v}_B$ is $\vec{v}_0$. This is known from Eq. $\eqref{p_1}$, the
conservation of linear momentum.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;$\vec{v}_A$ is perpendicular to $\vec{v}_B$. This is known from Eq. $\eqref{perp}$, which used
both conservation of energy and linear momentum.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Given these facts, I can soh-cah-toa my way to the answer. Expressed in terms of
$\alpha$ and $v_0$, I get:&lt;/p&gt;

\[\vec{v}_A(t+\tau) = (v_0 \sin\alpha) \, \hat{v}_A
\notag\]

\[\vec{v}_B(t+\tau) = (v_0 \cos\alpha) \, \hat{v}_B
\notag\]

&lt;p&gt;Putting it all together, we have our equations for the elastic, instantaneous, and frictionless ball-ball collision
in the specific case where one ball is stationary:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Elastic, instantaneous, frictionless ball-ball collision (&lt;strong&gt;stationary case&lt;/strong&gt;)&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Displacement:&lt;/p&gt;

\[\vec{r}_A(\tau+dt) = \vec{r}_A(\tau)
\label{rA_simple}\]

\[\vec{r}_B(\tau+dt) = \vec{r}_B(\tau)
\label{rB_simple}\]

  &lt;p&gt;Velocity:&lt;/p&gt;

\[\vec{v}_A(t+\tau) = (v_0 \sin\alpha) \, \hat{v}_A
\label{vA_simple}\]

\[\vec{v}_B(t+\tau) = (v_0 \cos\alpha) \, \hat{v}_B
\label{vB_simple}\]

  &lt;p&gt;Angular velocity:&lt;/p&gt;

\[\vec{\omega}_A(\tau+dt) = \vec{\omega}_A(\tau)
\label{oA_simple}\]

\[\vec{\omega}_B(\tau+dt) = \vec{\omega}_B(\tau)
\label{oB_simple}\]

&lt;/div&gt;

&lt;h4 id=&quot;case-2-both-moving&quot;&gt;Case 2: both moving&lt;/h4&gt;

&lt;p&gt;Relaxing the assumption that one ball is stationary may at first seem like a terrible idea–the
introduced complexity must be horrible.&lt;/p&gt;

&lt;p&gt;And that intuition is basically correct. Treating both balls as
moving would be a nightmare. Yet even when both balls are moving, I don’t need to &lt;em&gt;treat&lt;/em&gt; it that
way. Instead, I can change to a frame of reference that moves with one of the balls. In such a frame
of reference, that ball is stationary. A picture of the situation is shown in Figure 10:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/ball_ball_collision_2.jpg&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/ball_ball_collision_2.jpg&quot; alt=&quot;ball_ball_collision_2&quot; /&gt;&lt;/a&gt;
&lt;em&gt;&lt;strong&gt;Figure 10&lt;/strong&gt;. In the left panel, Ball A (blue) and Ball B (red) are both moving and due to collide at
the position of the unfilled circles. If $\vec{v}_B$ is subtracted from the velocities of both
balls (see yellow vectors), the frame of reference is changed to one that moves
with ball B, shown in the right panel. In this scenario, ball B is stationary, and the situation
reduces to Case 1, and specifically the situation depicted in Figure 8.&lt;/em&gt;&lt;/p&gt;

&lt;p class=&quot;warning&quot;&gt;In Figure 10 panel II, the balls should contact with the same orientation as shown in panel I (about
$50^{\circ}$ to the $x-$axis), rather than as pictured.&lt;/p&gt;

&lt;p&gt;Since physics behaves the same as viewed from all inertial reference frames, I am well within my rights
to make this transformation during the collision. After solving the outgoing state post-collision,
I can reverse the transformation and voila, I’m done. This makes Case 2 trivial, since it can be
reduced to Case 1. Explicitly, the procedure goes like this:&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt;Elastic, instantaneous, frictionless ball-ball collision (&lt;strong&gt;both moving&lt;/strong&gt;)&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;First, make the following transformation so Ball B is stationary:&lt;/p&gt;

\[\vec{v}_B&apos;(\tau) = \vec{v}_B(\tau) - \vec{v}_B(\tau) = \vec{0}
\label{trans_B}\]

\[\vec{v}_A&apos;(\tau) = \vec{v}_A(\tau) - \vec{v}_B(\tau)
\label{trans_A}\]

  &lt;p&gt;The velocities of the collisions are resolved via Eqs. $\eqref{vA_simple}$ and $\eqref{vB_simple}$,
where Eq. $\eqref{trans_A}$ is substituted as $\vec{v}_0$. This yields post-collision velocity
vectors $\vec{v}_A’(\tau + dt)$ and $\vec{v}_B’(\tau + dt)$, which can be transformed back to the table
frame of reference via the inverse transformation (adding back $\vec{v}_B(\tau)$):&lt;/p&gt;

\[\vec{v}_A(\tau + dt) = \vec{v}_A&apos;(\tau + dt) + \vec{v}_B(\tau)
\label{inv_trans_A}\]

\[\vec{v}_B(\tau + dt) = \vec{v}_B&apos;(\tau + dt) + \vec{v}_B(\tau)
\label{inv_trans_B}\]

&lt;/div&gt;

&lt;h2 id=&quot;section-iii-ball-cushion-interactions&quot;&gt;&lt;strong&gt;Section III&lt;/strong&gt;: ball-cushion interactions&lt;/h2&gt;

&lt;p&gt;The ball-cushion interaction is probably the most difficult to model accurately. There are so many
factors to consider. The height, shape, friction, and compressibility of the cushion. The incoming
angle, velocity, and spin of the ball. All of these have significant effects on how the rail
influences the ball’s outgoing state. Let’s take a look in slow motion:&lt;/p&gt;

&lt;div class=&quot;youtube-embed&quot;&gt;
    &lt;iframe src=&quot;https://www.youtube.com/embed/yWH-CbV6BwQ&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;First, you can really see that the rail deforms substantially throughout its interaction with the ball.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/cushion_depression.png&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/cushion_depression.png&quot; alt=&quot;cushion_depression&quot; /&gt;&lt;/a&gt;
&lt;em&gt;Pool ball significantly deforming the cushion &lt;a href=&quot;https://www.youtube.com/watch?v=yWH-CbV6BwQ&quot;&gt;Source&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The implication is two-fold.&lt;/p&gt;

&lt;p&gt;First, the interaction is non-instantaneous. In fact, it persists far
longer than the ball-ball interaction. Yet finite-time interactions open up a can of worms for
multi-body dynamics, since a second ball may join the party and collide with the first ball whilst
it is interacting with the cushion.&lt;/p&gt;

&lt;p&gt;For this reason, along with the inherent complexity of the
soft-body physics, modelling the interaction as non-instantaneous is likely unfeasible.&lt;/p&gt;

&lt;p&gt;Second,
there is no single point of contact (PoC) between ball and cushion. Rather, the interaction occurs
over a line of contact (LoC), each infinitesimal segment of which contributes to the applied force.&lt;/p&gt;

&lt;p&gt;These complexities are why &lt;strong&gt;the ball-cushion interaction is the least accurately modelled aspect of
pool physics simulations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Did you notice that the ball pops into the air post-collision? This happens
because the apex of the cushion is at a height greater than the ball’s radius, and so the outgoing
velocity of the ball has a component that goes &lt;em&gt;into&lt;/em&gt; the table. Consequently, the slate of the
table applies a normal force to the ball, &lt;strong&gt;popping it up into the air&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Importantly, I want to
distinguish between the ball-slate interaction and the ball-cushion interaction: the ball-cushion
interaction creates the outgoing velocity of the ball, which under most circumstances has a
component &lt;em&gt;into&lt;/em&gt; the table. An infinitesimally small amount of time later, the ball-slate
interaction occurs, which prevents translation into the table’s surface, and in some cases pops
the ball into the air if its speed is great enough. This section deals strictly with the
ball-cushion interaction, and in the next section I will treat the ball-slate interaction.&lt;/p&gt;

&lt;p&gt;Given the complexity of the ball-cushion interaction, I must admit I feel a
little in over my head. So far, I have considered 3 models: Mathavan &lt;em&gt;et. al&lt;/em&gt;,
2010; Marlow, 1994; Han, 2005. Let’s take a look.&lt;/p&gt;

&lt;h3 id=&quot;1-mathavan-et-al-2010&quot;&gt;(1) Mathavan &lt;em&gt;et. al&lt;/em&gt;, 2010&lt;/h3&gt;

&lt;p&gt;After searching the literature, the most complete treatment I have found is &lt;a href=&quot;https://www.researchgate.net/publication/245388279_A_theoretical_analysis_of_billiard_ball_dynamics_under_cushion_impacts&quot;&gt;this work by Mathavan
&lt;em&gt;et. al&lt;/em&gt;
(2010)&lt;/a&gt;
entitled, “&lt;em&gt;A theoretical analysis of billiard ball dynamics under cushion impacts&lt;/em&gt;”. They develop a
model that must be solved numerically using differential equations, which is relatively complex. To get
a rough idea of how involved this model is, check out the force body diagram in Figure 4:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/mathavan_2010_1.png&quot; class=&quot;center-img width-90&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/mathavan_2010_1.png&quot; alt=&quot;mathavan_2010_1&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Force body diagram from Mathavan et. al, 2010. &lt;a href=&quot;https://www.researchgate.net/publication/245388279_A_theoretical_analysis_of_billiard_ball_dynamics_under_cushion_impacts&quot;&gt;source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the kind of thing that takes a long time to wrap your ahead around the meaning of the
variables. Despite the model’s complexity, it is still very simple in comparison to reality because
it assumes the cushion deformation is insignificant, and thus the interaction instantaneous. Despite
this being a large departure from reality, they report really good agreement with experiment.&lt;/p&gt;

&lt;p&gt;This is definitely the best model I could find that I would be willing to implement, but at this
moment in time I don’t &lt;em&gt;really&lt;/em&gt; want to solve differential equations on-the-fly every time there is a
ball-cushion interaction. Perhaps I could solve the differential equations for the entire parameter
space and then parameterize the solution space somehow, but for now I would like to avoid this model
altogether and look for something simpler.&lt;/p&gt;

&lt;h3 id=&quot;2-marlow-1994&quot;&gt;(2) Marlow, 1994&lt;/h3&gt;

&lt;p&gt;So Marlow has a book called “The Physics of Pocket Billiards” that I use as a reference for pool
physics. In general, its very comprehensive, but in the case of the ball-cushion interaction he
presents an incomplete and inconsistent treatment. Moving on.&lt;/p&gt;

&lt;h3 id=&quot;3-han-2005&quot;&gt;(3) Han, 2005&lt;/h3&gt;

&lt;p&gt;I enjoy &lt;a href=&quot;https://link.springer.com/article/10.1007/BF02919180&quot;&gt;this treatment by Han, 2005&lt;/a&gt;,
entitled, “&lt;em&gt;Dynamics in carom and three cushion billiards&lt;/em&gt;”. It assumes instantaneity and negligible
cushion deformation. It is simple, analytic, and appears to be physically plausible. That said, it
is less realistic than Mathavan &lt;em&gt;et. al&lt;/em&gt;, 2010.&lt;/p&gt;

&lt;p&gt;Alright, so let’s go over the model. Han chooses the following frame of reference:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/han_1.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/han_1.jpg&quot; alt=&quot;han_1&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 11&lt;/strong&gt;. A ball colliding with a rail viewed from above. The frame of reference is defined so
that the rail is perpendicular to the $x-$axis, and the $+x=$direction points away from the playing
surface. The incoming velocity makes an angle $\phi$ with the $x-$axis.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the instant of contact, $t=\tau$, the ball has a state $(\vec{r}(\tau), \, \vec{v}(\tau), \,
\vec{\omega}(\tau))$, and immediately afterwards it has the state $(\vec{r}(\tau + dt), \, \vec{v}(\tau + dt), \, \vec{ \omega}(\tau + dt))$.
Since Han assumes instantaneity, $dt$ is an infinitesimal amount of time. Since there is no funny
business going on with cushion deformation, we know that the position at $\tau$ will equal the
position at $\tau + dt$:&lt;/p&gt;

\[\vec{r}(\tau + dt) = \vec{r}(\tau)
\notag\]

&lt;p&gt;Resolving the velocity and angular velocity requires some geometry of the ball-cushion interface.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/han_0.jpg&quot; class=&quot;center-img width-70&quot;&gt;&lt;img src=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/han_0.jpg&quot; alt=&quot;han_0&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Figure 12&lt;/strong&gt;. A ball colliding with a rall viewed from the side. It is the same scenario as in
Figure 11. $R+\epsilon$ defines the height of the cushion, where contact is made with the ball.
$\theta$ is determined uniquely by $\epsilon$, and defines the direction of the impulse the cushion
imparts on the ball.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One of the most important determinants in this model, and in reality, is the height of the rail.
This will determine the direction that the cushion applies force to the ball. The higher the
cushion, the more the cushion redirects the ball &lt;em&gt;into&lt;/em&gt; the table. Also, the higher the cushion,
the more $y-$axis torque will be applied to the ball. The cushion is specified by $\epsilon$, the
height above center ball, and uniquely determines $\theta$, the angle that the line between the PoC
and the center of the ball makes with the $x-$axis:&lt;/p&gt;

\[\theta = \arcsin(\epsilon/R)
\label{eps_theta}\]

&lt;p&gt;You may wonder, why not have $\epsilon = 0$? Then $\theta=0$ and the ball won’t be redirected into
the table.&lt;/p&gt;

&lt;p&gt;This is idealistic thinking for 2 reasons. In practice, it is not uncommon for balls to
leave the table when struck hard. If an airborne ball hit a rail with $\epsilon = 0$, it would go
flying, so $\epsilon &amp;gt; 0$ protects against that.&lt;/p&gt;

&lt;p&gt;Yet even if a ball remained firmly planted to the
table, consider a rolling ball striking the cushion perpendicularly. It’s spin is in the
$+y-$direction of Figures 11 and 12. When in contact with the rail, the rolling spin tries to push
the cushion down, and in an equal and opposite manner the cushion pushes the ball &lt;strong&gt;up&lt;/strong&gt;, sending the ball
airborne.&lt;/p&gt;

&lt;p&gt;If you’ve ever played on a table with rails that pops the balls up, its probably because
$\epsilon$ is too low. For these reasons, in practice $\epsilon$ is typically $0.1 R$ to $0.2 R$.
This isn’t a perfect solution to the airborne problem however, because as we saw in the slow mo
video, redirecting the ball into the table can also cause balls to become airborne via the
ball-slate interaction. That said, redirection into the slate is preferable to redirection into the
air, since the slate’s low coefficient of restitution dampens most of the energy.&lt;/p&gt;

&lt;p&gt;Using rigid body dynamics, Han solves the outgoing ball state for the scenario
depicted in Figures 11 and 12.&lt;/p&gt;

&lt;p class=&quot;warning&quot;&gt;After surveying the equations for consistency, I found 2 mistakes, that I outline in &lt;a href=&quot;https://ekiefl.github.io/images/pooltool/pooltool-theory/ball_cushion_inhwan_han.pdf&quot;&gt;this
worksheet&lt;/a&gt;. The equations below have these mistakes
&lt;strong&gt;accounted for&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;extra-info&quot;&gt;
  &lt;p&gt;&lt;span class=&quot;extra-info-header&quot;&gt; ball-cushion interaction (&lt;strong&gt;Han 2005 model&lt;/strong&gt;)&lt;/span&gt;&lt;/p&gt;

  &lt;p&gt;Let all of these quantities be:&lt;/p&gt;

\[s_x = v_x(\tau) \sin\theta - v_y(\tau) \cos\theta + R \omega_y(\tau)
\label{han_sx}\]

\[s_y = -v_y(\tau) - R \omega_z(\tau) \cos\theta + R \omega_x(\tau) \sin\theta
\label{han_sy}\]

\[c = v_x(\tau) \cos\theta
\label{han_c}\]

\[I = \frac{2}{5} m R^2
\label{han_I}\]

\[P_{zE} = m c \, (1 + e)
\label{han_pze}\]

\[P_{zS} = \frac{2m}{7} \sqrt{s_x^2 + s_y^2}
\label{han_pzs}\]

  &lt;p&gt;Displacement:&lt;/p&gt;

\[\vec{r}(\tau + dt) = \vec{r}(\tau)\]

  &lt;p&gt;Velocity (if $P_{zS} \le P_{zE}$):&lt;/p&gt;

\[v_x(\tau + dt) = -\frac{2}{7}s_x \sin\theta - (1+e) \, c \cos\theta
\label{han_vx_1}\]

\[v_y(\tau + dt) = \frac{2}{7}s_y
\label{han_vy_1}\]

\[v_z(\tau + dt) = \frac{2}{7}s_x \cos\theta - (1+e) \, c \sin\theta
\label{han_vz_1}\]

  &lt;p&gt;Velocity (if $P_{zS} &amp;gt; P_{zE}$):&lt;/p&gt;

\[v_x(\tau + dt) = -c \, (1+e) (\mu \cos\phi \sin\theta + \cos\theta)
\label{han_vx_2}\]

\[v_y(\tau + dt) = c \, (1+e) \, \mu \sin\phi
\label{han_vy_2}\]

\[v_z(\tau + dt) = c \, (1+e) (\mu \cos\phi \cos\theta - \sin\theta)
\label{han_vz_2}\]

  &lt;p&gt;Angular velocity:&lt;/p&gt;

\[\omega_x(\tau + dt) = - \frac{m R}{I} v_x(\tau + dt) \sin\theta
\label{han_ox}\]

\[\omega_x(\tau + dt) = \frac{m R}{I} (v_x(\tau + dt) \sin\theta - v_z(\tau+dt) \cos\theta)
\label{han_oy}\]

\[\omega_z(\tau + dt) = \frac{m R}{I} v_y(\tau + dt) \cos\theta
\label{han_oz}\]

&lt;/div&gt;

&lt;h2 id=&quot;section-iv-ball-air-interactions&quot;&gt;&lt;strong&gt;Section IV&lt;/strong&gt;: ball-air interactions&lt;/h2&gt;

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

&lt;h2 id=&quot;section-v-ball-slate-interactions&quot;&gt;&lt;strong&gt;Section V&lt;/strong&gt;: ball-slate interactions&lt;/h2&gt;

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

&lt;h2 id=&quot;section-vi-ball-cue-interactions&quot;&gt;&lt;strong&gt;Section VI&lt;/strong&gt;: ball-cue interactions&lt;/h2&gt;

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

&lt;h2 id=&quot;conclusion&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;That’s everything–at least for now. My knowledge will (hopefully) increase over time, and
when it does, it will be added to this post.&lt;/p&gt;

&lt;p&gt;Next time I will be discussing the various algorithms for simulating a pool shot, and the approach I
intend to take for my own simulation, which is called continuous event-based shot evolution.&lt;/p&gt;


    &lt;p&gt;&lt;a href=&quot;https://ekiefl.github.io/2020/04/24/pooltool-theory/&quot;&gt;The physics of pool/billiards&lt;/a&gt; was originally published by Evan Kiefl at &lt;a href=&quot;https://ekiefl.github.io&quot;&gt;Evan Kiefl&lt;/a&gt; on April 24, 2020.&lt;/p&gt;

  </content>
</entry>

</feed>
