<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Stalefish Labs</title><link>https://stalefishlabs.com/tags/weather/</link><description>We build simple, thoughtful tools for gathering your people, getting outside, and spending less time planning and more fun time together — because the best things happen when everyone shows up.</description><generator>Hugo 0.155.2</generator><language>en-us</language><lastBuildDate>Sun, 19 Apr 2026 06:16:18 +0000</lastBuildDate><atom:link href="https://stalefishlabs.com/tags/weather/index.xml" rel="self" type="application/rss+xml"/><item><title>Flipping the Question: From 'Is It Too Wet?' to 'Is It Too Dry?'</title><link>https://stalefishlabs.com/the-lab/2026-03-25-flipping-the-question/</link><pubDate>Wed, 25 Mar 2026 00:00:00 -0700</pubDate><guid>https://stalefishlabs.com/the-lab/2026-03-25-flipping-the-question/</guid><description>The same engine that tells riders to stay home tells gardeners to grab the hose. Watering is its own problem.</description><content:encoded>&lt;p&gt;When I first built the Groundwise engine for the &lt;a href="https://stalefishlabs.com/apps/ridewise/"
&gt;Ridewise app&lt;/a&gt;, it answered one question: &lt;strong&gt;is it too wet to ride?&lt;/strong&gt; Low wetness meant good conditions. High wetness meant stay home. Exactly what I needed for mountain biking, skateboarding, and other outdoor wheeled activities where surface conditions impacted by weather mattered.&lt;/p&gt;
&lt;p&gt;Then I started thinking about my garden.&lt;/p&gt;
&lt;p&gt;I have raised beds, a few containers on the patio, and a lawn that I&amp;rsquo;d like to keep alive without drowning it. I don&amp;rsquo;t get obsessive about it, and to be honest my lawn is far from impressive. I&amp;rsquo;m not one of those people who try to maintain golf course grass, far from it. But I&amp;rsquo;ve still regularly done that assessment where I look at the sky, vaguely remember whether it rained, and make the same kind of gut call on watering that I used to make about trails. The variables were familiar: recent rain, temperature, sun exposure, how fast things dry. I was running the same mental model, just asking the opposite question.&lt;/p&gt;
&lt;p&gt;That realization is what turned one app into three. If the engine could quantify moisture on a surface, it could answer both &amp;ldquo;is it too wet to ride?&amp;rdquo; and &amp;ldquo;is it too dry, should I water?&amp;rdquo;&lt;/p&gt;
&lt;h2 id="the-inversion"&gt;The Inversion&lt;/h2&gt;
&lt;p&gt;The engine always calculates a wetness score from 0 (bone dry) to 1 (saturated). The &lt;code&gt;EngineMode&lt;/code&gt; enum controls what that score means:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wetness concern&lt;/strong&gt; (Ridewise, Fieldwise): Low wetness = Yes (go ride or play). High wetness = No (too wet).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dryness concern&lt;/strong&gt; (Yardwise): Low wetness = Yes (water today). High wetness = No (skip watering).&lt;/p&gt;
&lt;p&gt;The verdict is the same enum, &lt;code&gt;Yes&lt;/code&gt;, &lt;code&gt;Maybe&lt;/code&gt;, &lt;code&gt;No&lt;/code&gt;, but the interpretation flips. This means the consuming app doesn&amp;rsquo;t need to know which mode produced the result. It renders green for Yes, orange for Maybe, red for No, regardless.&lt;/p&gt;
&lt;p&gt;The thresholds shift too:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;th&gt;Maybe&lt;/th&gt;
&lt;th&gt;No&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wetness concern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.30&lt;/td&gt;
&lt;td&gt;0.30 – 0.60&lt;/td&gt;
&lt;td&gt;≥ 0.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dryness concern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.15&lt;/td&gt;
&lt;td&gt;0.15 – 0.45&lt;/td&gt;
&lt;td&gt;≥ 0.45&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Yardwise thresholds are lower because the stakes are asymmetric. This wasn&amp;rsquo;t immediately obvious until testing early versions of the app, when I realized that missing a watering day rarely if ever harms established plants. Riding a muddy trail damages the surface. So Yardwise is more lenient than the other apps, and leans toward &amp;ldquo;maybe check the soil&amp;rdquo; rather than &amp;ldquo;definitely water&amp;rdquo; — this makes the verdict less alarming. The reality is that the cost of a false positive (unnecessary watering) is real (overwatering causes its own problems), while the cost of a false negative (skipping one day) is usually trivial.&lt;/p&gt;
&lt;h2 id="manual-watering-tracking-what-the-weather-doesnt-know"&gt;Manual Watering: Tracking What the Weather Doesn&amp;rsquo;t Know&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s where Yardwise diverges from Ridewise and Fieldwise most sharply. The weather API knows about rain. It doesn&amp;rsquo;t know that you ran the sprinkler for 20 minutes yesterday evening.&lt;/p&gt;
&lt;p&gt;Yardwise lets users log manual watering events with timestamps. The engine converts these into precipitation equivalents that then get added to the rain score alongside actual precipitation, with the same time-weighted decay:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;0-24 hours ago: 0.7x weight (70% contribution)
24-48 hours ago: 0.2x weight
48-72 hours ago: 0.1x weight
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A watering yesterday contributes about 0.175 inches to the effective precipitation (0.25 × 0.7). That&amp;rsquo;s enough to shift a borderline &amp;ldquo;water today&amp;rdquo; verdict to &amp;ldquo;check soil&amp;rdquo; or even &amp;ldquo;skip.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The decay is important. A watering three days ago shouldn&amp;rsquo;t prevent the engine from recommending water today, the moisture is long gone, especially in containers with fast-draining potting mix. The time-weighted model handles this naturally.&lt;/p&gt;
&lt;h3 id="cold-weather-extends-watering-memory"&gt;Cold Weather Extends Watering Memory&lt;/h3&gt;
&lt;p&gt;One wrinkle: cold weather slows evapotranspiration. A watering event that would normally decay in 48 hours can remain relevant for up to 96 hours during cold weather. The engine extends the effective window based on the same cold factor used for dormancy:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;wateringWindow = 48 + 48 × coldFactor
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At 34°F, the cold factor is about 0.62, giving a window of roughly 78 hours. At 45°F, it&amp;rsquo;s about 56 hours. This prevents the engine from recommending watering when yesterday&amp;rsquo;s water is still sitting in cold, slowly-evaporating soil. Keep in mind that winter watering is pretty rare, reserved almost exclusively for new plantings (trees, shrubs, etc.) and only during an unusual dry spell.&lt;/p&gt;
&lt;h2 id="establishment-sensitivity-protecting-young-plants"&gt;Establishment Sensitivity: Protecting Young Plants&lt;/h2&gt;
&lt;p&gt;Newly seeded lawns and recently transplanted plants need more water than established ones, and they&amp;rsquo;re far more susceptible to drought. The engine handles this through an establishment sensitivity boost that layers on top of the surface type&amp;rsquo;s base damage sensitivity.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Establishment&lt;/th&gt;
&lt;th&gt;Sensitivity Boost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Established&lt;/td&gt;
&lt;td&gt;+0.0 (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newly planted&lt;/td&gt;
&lt;td&gt;+0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newly seeded&lt;/td&gt;
&lt;td&gt;+0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This boost affects the sensitivity veto, the mechanism that pushes borderline verdicts toward protection. With a newly seeded lawn (base sensitivity 0.4 + boost 0.5 = 0.9 effective sensitivity), a Maybe verdict with wetness below 0.3 gets overridden to Yes: water those seeds.&lt;/p&gt;
&lt;p&gt;For established plants with no boost, the same borderline conditions stay as Maybe: check the soil yourself. The engine doesn&amp;rsquo;t push you to water unless there&amp;rsquo;s a fragile surface that genuinely needs protection.&lt;/p&gt;
&lt;p&gt;This is the same sensitivity veto that Fieldwise uses to protect natural grass athletic fields from being played on when borderline wet. Same mechanism, opposite direction. In wetness-concern mode, high sensitivity + borderline conditions → No (protect from use). In dryness-concern mode, high sensitivity + borderline conditions → Yes (protect from drought).&lt;/p&gt;
&lt;h2 id="dormancy-and-evaporative-demand"&gt;Dormancy and Evaporative Demand&lt;/h2&gt;
&lt;p&gt;Two Yardwise-specific wetness modifiers that were covered in the &lt;a href="https://stalefishlabs.com/the-lab/2026-03-23-edge-cases/"
&gt;edge cases article&lt;/a&gt; deserve a brief recap in the context of the inversion:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cold-weather dormancy&lt;/strong&gt; injects additional wetness into the score when temperatures drop below 55°F. This suppresses watering recommendations by pushing the score toward the &amp;ldquo;skip&amp;rdquo; zone because dormant plants don&amp;rsquo;t benefit from watering and the moisture promotes root rot and fungal growth.&lt;/p&gt;
&lt;p&gt;From the engine&amp;rsquo;s perspective, dormancy is saying: &amp;ldquo;the soil might technically be dry, but the plant doesn&amp;rsquo;t need water right now, so act as if conditions are wetter than they are.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Low evaporative demand&lt;/strong&gt; does something similar on cool, overcast, humid days. Even if the soil surface looks dry, the plant isn&amp;rsquo;t losing much water through transpiration when there&amp;rsquo;s no sun and the air is already saturated. A small wetness boost (up to 0.25) prevents unnecessary watering recommendations.&lt;/p&gt;
&lt;p&gt;Both modifiers only activate in dryness-concern mode. They don&amp;rsquo;t make sense for riders or field sports, cold weather doesn&amp;rsquo;t make a trail less muddy, and overcast skies don&amp;rsquo;t affect whether the ground is rideable or a football field is playable.&lt;/p&gt;
&lt;h2 id="yardwise-surface-types"&gt;Yardwise Surface Types&lt;/h2&gt;
&lt;p&gt;The Groundwise family shares a surface type system, but Yardwise introduces types that don&amp;rsquo;t exist in the riding or sports world:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Potting mix&lt;/strong&gt; (containers) dries at 2.0x — the same rate as concrete. This isn&amp;rsquo;t a coincidence. Potting mix is engineered for drainage, just like concrete. Containers are also typically exposed to wind on all sides, which accelerates drying considerably. The result: containers often need daily watering in summer, even after rain. The engine captures this naturally because the high drying multiplier clears moisture quickly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Amended soil with mulch&lt;/strong&gt; dries at 0.7x — the slowest rate in the system, tied with clay. Mulch deliberately slows evaporation. A mulched garden bed after rain stays moist for days. The engine&amp;rsquo;s residual wetness model (which only activates for absorbent surfaces above 0.25 inches of rain) extends this further, keeping the score in &amp;ldquo;skip watering&amp;rdquo; territory well after a good rain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fresh seed&lt;/strong&gt; has the highest effective damage sensitivity in the system: base 0.8 plus an establishment boost of 0.5, capped at 1.0. The engine is maximally protective of newly seeded areas, pushing any borderline verdict toward &amp;ldquo;water.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compost&lt;/strong&gt; is the most forgiving surface: 1.2x drying multiplier and only 0.3 damage sensitivity. Compost generates internal heat from decomposition, which drives some of its own drying. And it&amp;rsquo;s hard to damage, overwatering a compost pile isn&amp;rsquo;t really a concern. The engine mostly stays out of the way for compost, skewing toward &amp;ldquo;skip&amp;rdquo; unless conditions are genuinely dry.&lt;/p&gt;
&lt;h2 id="the-insight-that-made-it-work"&gt;The Insight That Made It Work&lt;/h2&gt;
&lt;p&gt;The hardest part of building Yardwise wasn&amp;rsquo;t the code, it was convincing myself that the inversion was valid. Every instinct I&amp;rsquo;d built while developing Ridewise and Fieldwise said &amp;ldquo;wet = bad.&amp;rdquo; Retraining my thinking to &amp;ldquo;wet = good (for gardening)&amp;rdquo; took effort.&lt;/p&gt;
&lt;p&gt;What made it click was realizing that the engine isn&amp;rsquo;t really about wet or dry. It&amp;rsquo;s about &lt;strong&gt;moisture state relative to the ideal for a given surface and activity.&lt;/strong&gt; For a trail, the ideal is dry. For a garden bed, the ideal is moist (not soaked!). The engine measures distance from ideal in both directions.&lt;/p&gt;
&lt;p&gt;Once I saw it that way, the inversion was mechanical. Same measurements, same model, same pipeline. Just a different definition of &amp;ldquo;good.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="next-in-the-series"&gt;Next in the Series&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://stalefishlabs.com/the-lab/2026-03-27-uncertain-verdicts/"
&gt;final article&lt;/a&gt; covers the verdict UX, why three states beat a percentage, how the engine communicates uncertainty through confidence levels and contributing factors, and the role of the recovery outlook in managing expectations.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://stalefishlabs.com/apps/yardwise/"
target="_blank"
&gt;Yardwise&lt;/a&gt; is available for iOS from &lt;a href="https://stalefishlabs.com"
target="_blank"
&gt;Stalefish Labs&lt;/a&gt;. Same engine as &lt;a href="https://stalefishlabs.com/apps/ridewise/"
target="_blank"
&gt;Ridewise&lt;/a&gt; and &lt;a href="https://stalefishlabs.com/apps/fieldwise/"
target="_blank"
&gt;Fieldwise&lt;/a&gt;, different question.&lt;/em&gt;&lt;/p&gt;</content:encoded><dc:creator>Michael Morrison</dc:creator><enclosure url="https://stalefishlabs.com/wetdryyard.png" type="image/png" length="0"/></item><item><title>Edge Cases That Break Your Weather Model</title><link>https://stalefishlabs.com/the-lab/2026-03-23-edge-cases/</link><pubDate>Mon, 23 Mar 2026 00:00:00 -0700</pubDate><guid>https://stalefishlabs.com/the-lab/2026-03-23-edge-cases/</guid><description>Freeze-thaw, snow melt, rain intensity, plant dormancy — the real-world complexity simple weather models miss.</description><content:encoded>&lt;p&gt;This is a continuation of a deep-dive into the weather engine that drives our Groundwise apps. Last we looked into the core &lt;a href="https://stalefishlabs.com/the-lab/2026-03-17-drying-model/"
&gt;drying model&lt;/a&gt;, which handles most days well. Sun, wind, time, surface type — combine them sensibly and you get a reasonable verdict most of the time.&lt;/p&gt;
&lt;p&gt;Then winter arrives and everything breaks. Interestingly, the engine was developed during the epic ice storm of 2026 that absolutely decimated parts of the southeastern U.S., including Nashville, TN where we&amp;rsquo;re based. So I got to experience first-hand a very real edge case as I was building the engine, and literally each day presented new challenges to clarify for example how long it actually takes a trail to recover from a prolonged frozen period.&lt;/p&gt;
&lt;p&gt;This article covers the edge cases that forced the most complex logic in the Groundwise engine, the scenarios where the simple model produces confidently wrong answers.&lt;/p&gt;
&lt;h2 id="freezethaw-360-lines-of-humility"&gt;Freeze/Thaw: 360 Lines of Humility&lt;/h2&gt;
&lt;p&gt;The frozen conditions check is the single largest block of code in the engine. It runs first, before any moisture modeling, because frozen ground is a fundamentally different hazard than wet ground.&lt;/p&gt;
&lt;p&gt;The naive approach would be simple: if the temperature is below 32°F, say No. But that misses almost everything that matters.&lt;/p&gt;
&lt;h3 id="hard-surfaces-vs-ground-different-failure-modes"&gt;Hard Surfaces vs. Ground: Different Failure Modes&lt;/h3&gt;
&lt;p&gt;A frozen concrete skatepark and a frozen dirt trail are dangerous for completely different reasons, and they recover at completely different speeds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concrete and asphalt&lt;/strong&gt; develop ice when moisture freezes on the surface. It&amp;rsquo;s a thin layer — dangerous because it&amp;rsquo;s invisible, but it clears relatively quickly once temperatures rise. At 55°F, a frozen skatepark is typically safe within 6 hours. At 50°F, give it 12. At 45°F, 24 hours, or just wait until 3 days after the last rain when there&amp;rsquo;s simply no moisture left to freeze.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dirt and natural grass&lt;/strong&gt; hold moisture internally. When they freeze, the ice is &lt;em&gt;in&lt;/em&gt; the ground, not just on top of it. Thawing releases that moisture all at once, creating a slurry that&amp;rsquo;s worse than the original wet conditions. A ground trail at 45°F needs 72 hours to fully recover from a freeze — not because ice persists that long, but because the thaw creates its own wetness event. It&amp;rsquo;s worth noting that one thing the engine does not attempt to factor in is the counterintuitive scenario where frozen mountain bike trails are sometimes &lt;em&gt;more&lt;/em&gt; rideable when fully frozen due to the combo of frost heave breaking up the surface tread and then a deep freeze solidifying that oatmeal-like top layer into a grippy crunch. If you bundle up, some of the best winter riding here is when a trail fully heaves and freezes, but that seemed like a special enough case to not factor in (at least not yet!), especially when you factor in ride comfort.&lt;/p&gt;
&lt;p&gt;The engine models the concrete/asphalt vs. dirt/grass freeze issue with separate escape conditions for draining vs. absorbent surfaces:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Temperature&lt;/th&gt;
&lt;th&gt;Hard Surface Recovery&lt;/th&gt;
&lt;th&gt;Ground Recovery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;≥ 55°F&lt;/td&gt;
&lt;td&gt;6 hours&lt;/td&gt;
&lt;td&gt;24 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;≥ 50°F&lt;/td&gt;
&lt;td&gt;12 hours&lt;/td&gt;
&lt;td&gt;48 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;≥ 45°F&lt;/td&gt;
&lt;td&gt;24 hours (or 3+ days since rain)&lt;/td&gt;
&lt;td&gt;72 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 45°F&lt;/td&gt;
&lt;td&gt;Still frozen&lt;/td&gt;
&lt;td&gt;Still frozen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="the-near-freezing-zone"&gt;The Near-Freezing Zone&lt;/h3&gt;
&lt;p&gt;32°F isn&amp;rsquo;t a magic line. Ice can persist on surfaces at 34°F, especially in shade. And moisture can pose a freeze risk at 38°F if temperatures are dropping. Throw in wind and it&amp;rsquo;s pretty evident that a simple 32°F freeze check isn&amp;rsquo;t sufficient.&lt;/p&gt;
&lt;p&gt;The engine treats 32-38°F as a caution zone. If there&amp;rsquo;s been recent precipitation (last 24 hours) and the temperature is in this range, the verdict is No with medium confidence. Not because ice is definitely present, but because the risk isn&amp;rsquo;t worth it.&lt;/p&gt;
&lt;h3 id="overnight-freeze-currently-above-freezing"&gt;Overnight Freeze, Currently Above Freezing&lt;/h3&gt;
&lt;p&gt;This is the most common winter scenario and the hardest to get right, especially in a moderate climate like the southeast where it can freeze overnight and then rise to sunny and 50s during the day. So an overnight freeze that changes to 52°F mid-day&amp;hellip;is the trail rideable?&lt;/p&gt;
&lt;p&gt;It depends on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How long it&amp;rsquo;s been above freezing&lt;/strong&gt; — just crossed 32°F an hour ago? Still frozen. Been above freezing since 9am and it&amp;rsquo;s now 2pm? Probably thawed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What surface you&amp;rsquo;re on&lt;/strong&gt; — concrete sheds faster than dirt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Whether there&amp;rsquo;s recent precipitation&lt;/strong&gt; — dry freeze is different from wet freeze.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Humidity&lt;/strong&gt; — above 70% humidity with a recent overnight freeze means possible frost even on surfaces that look dry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The engine tracks &lt;code&gt;hoursBelowFreezing&lt;/code&gt; and &lt;code&gt;hoursSinceThawBegan&lt;/code&gt; to model this progression. A surface that was frozen for 48 hours takes longer to fully thaw than one that dipped below freezing for 4 hours overnight.&lt;/p&gt;
&lt;h3 id="extended-freeze-with-no-recent-rain"&gt;Extended Freeze With No Recent Rain&lt;/h3&gt;
&lt;p&gt;It gets even trickier when you remove precipitation for a while. Here&amp;rsquo;s a counterintuitive case: it&amp;rsquo;s been below freezing for three days, but it hasn&amp;rsquo;t rained or snowed in a week. Is it safe?&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;hard surfaces&lt;/strong&gt;: probably yes. No moisture means nothing to freeze. The engine still checks humidity (frost can form from humid air alone), but without recent precipitation, hard surfaces are likely clear.&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;ground surfaces&lt;/strong&gt;: still no. Even without recent rain, ground retains moisture from weeks of accumulated precipitation. A multi-day freeze locks that moisture in. When thaw comes, it releases.&lt;/p&gt;
&lt;h2 id="snow-melt-a-bell-curve-not-a-line"&gt;Snow Melt: A Bell Curve, Not a Line&lt;/h2&gt;
&lt;p&gt;Snow melt seems simple — snow melts, things get wet, then they dry. But the wetness injection from melting snow follows a curve that&amp;rsquo;s nothing like the linear decay used for rain timing.&lt;/p&gt;
&lt;p&gt;The engine models thaw wetness on a bell curve based on melt progress:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Melt Progress&lt;/th&gt;
&lt;th&gt;Wetness Factor&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 5%&lt;/td&gt;
&lt;td&gt;0.7 → 1.0&lt;/td&gt;
&lt;td&gt;Starting to melt, moisture building&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-40%&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;Peak wetness, active melt, ground saturated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40-70%&lt;/td&gt;
&lt;td&gt;1.0 → 0.4&lt;/td&gt;
&lt;td&gt;Most snow gone, drainage beginning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70%+&lt;/td&gt;
&lt;td&gt;0.4 → 0.2&lt;/td&gt;
&lt;td&gt;Residual moisture, mostly clear&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Melt progress is estimated from hours since thaw began, with the assumption that most accumulations take about 48 hours to fully melt. Temperature above freezing drives the rate, warmer means faster.&lt;/p&gt;
&lt;p&gt;The peak wetness window (5-40% melted) is deliberately wide. During active melt, the ground is receiving a continuous supply of water. Unlike rain, which dumps water and stops, snow melt is a slow, sustained event that can keep surfaces saturated for hours.&lt;/p&gt;
&lt;p&gt;Snow accumulation matters too. Did you know that on average, 10 inches of snow is equivalent to 1 inch of liquid rain (a 10:1 ratio). However, this ratio varies significantly based on temperature, ranging from 5 inches (wet snow) to over 20 inches (dry, powdery snow) of snow for every 1 inch of liquid. But what we really care about is how much snow results in complete saturation. Here&amp;rsquo;s how the engine scales the initial wetness injection for snow:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;snowFactor = clamp(0.5 + snowAccumulation / 4.0, 0, 1)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Half an inch gives a snowFactor of 0.625. Four inches saturates at 1.0. This prevents a light dusting from being treated the same as a significant snowfall.&lt;/p&gt;
&lt;h3 id="when-theres-no-tracked-snow"&gt;When There&amp;rsquo;s No Tracked Snow&lt;/h3&gt;
&lt;p&gt;Sometimes the engine knows there was a freeze/thaw cycle but doesn&amp;rsquo;t have explicit snow accumulation data. In that case, it falls back to estimating initial moisture from freeze severity:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;initialMoisture = 0.3 + 0.3 × freezeSeverity
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Where freeze severity scales from 0.3 (24 hours below freezing) to 0.6 (4+ days below freezing). Longer freezes lock more ground moisture, producing more wetness when thaw arrives.&lt;/p&gt;
&lt;p&gt;The decay uses a half-life model, a baseline of 48 hours, stretched dramatically when overnight temperatures keep dropping below freezing. A thaw that refreezes each night can keep surfaces problematic for over a week, because each night arrests the drying process and each morning restarts the moisture release.&lt;/p&gt;
&lt;h2 id="precipitation-intensity-same-amount-different-impact"&gt;Precipitation Intensity: Same Amount, Different Impact&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://stalefishlabs.com/the-lab/2026-03-17-drying-model/"
&gt;drying model article&lt;/a&gt; covered the rain pattern multiplier (0.7x for light, 1.1x for steady, 1.5x for downpour), but intensity creates edge cases beyond just the multiplier.&lt;/p&gt;
&lt;p&gt;A quarter inch of light rain over four hours &lt;strong&gt;soaks into absorbent surfaces gradually&lt;/strong&gt;. The ground absorbs it progressively, and drainage keeps up. Surface water is minimal. This is the kind of rain you hear gardeners, landscapers, and farmers wishing for as it&amp;rsquo;s the best watering rain for plants, slow and steady.&lt;/p&gt;
&lt;p&gt;A quarter inch dumped in 20 minutes, on the other hand, &lt;strong&gt;overwhelms drainage&lt;/strong&gt;. Water pools on the surface, runs off trails causing erosion, and saturates the top layer before it can percolate down. The surface is wetter even though the total precipitation is identical.&lt;/p&gt;
&lt;p&gt;The 1.5x downpour multiplier captures this, but it&amp;rsquo;s an approximation. In reality, the impact depends heavily on the surface&amp;rsquo;s infiltration rate, slope, and drainage design — none of which the engine models directly. And it doesn&amp;rsquo;t try.&lt;/p&gt;
&lt;p&gt;This is one of the spots where the engine&amp;rsquo;s approximation is &amp;ldquo;good enough for most cases&amp;rdquo; rather than trying to be physically accurate to the extreme. A purpose-built trail with water bars and crowned surfaces handles a downpour better than a flat trail in a natural depression. The engine treats both as dirt with a 1.0x drying multiplier, which is imprecise but still more useful than ignoring intensity entirely. This is also worth flagging where often a Maybe verdict is truly the best answer because it tells you conditions are borderline, and your own local knowledge may tip the final ride decision. Yes, the app is attempting to assist in the human intuition of weather conditions, but not replace it entirely.&lt;/p&gt;
&lt;h2 id="cold-weather-dormancy-protecting-plants-from-themselves"&gt;Cold-Weather Dormancy: Protecting Plants From Themselves&lt;/h2&gt;
&lt;p&gt;This edge case is Yardwise-specific, but it&amp;rsquo;s one of my favorites because it&amp;rsquo;s a case where the &amp;ldquo;right&amp;rdquo; answer is counterintuitive.&lt;/p&gt;
&lt;p&gt;When temperatures drop below 55°F, most plants enter dormancy. Their water uptake drops dramatically. Watering dormant plants typically doesn&amp;rsquo;t help them, it can all too easily create conditions for root rot and fungal disease. In this scenario the engine needs to &lt;strong&gt;suppress watering recommendations&lt;/strong&gt; even when the soil looks dry.&lt;/p&gt;
&lt;p&gt;The dormancy calculation uses a non-linear curve:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;coldFactor = pow((55 - temperature) / 23, 0.6)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Why &lt;code&gt;pow(x, 0.6)&lt;/code&gt; instead of linear? Because the suppression should be strong even at moderate cold. At 45°F (10 degrees below the 55°F threshold), a linear model gives a cold factor of 0.43. The power curve gives 0.56, which is meaningfully stronger. The difference matters because 45°F is genuinely cold enough to suppress plant activity, and the engine should reflect that.&lt;/p&gt;
&lt;p&gt;The dormancy score gets amplified by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Freeze history&lt;/strong&gt; — if it&amp;rsquo;s been below 32°F for 24+ hours recently, plants are deeply dormant. Add 0.25 × coldFactor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recent cold-weather watering&lt;/strong&gt; — if the user watered during cold weather, that water persists much longer because evapotranspiration is minimal. The residual window extends from 48 hours to up to 96 hours. And yes, evapotranspiration is a real word&amp;hellip;I couldn&amp;rsquo;t resist using it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But there&amp;rsquo;s a warm-day dampener too. If the daytime high reaches 65°F even though it&amp;rsquo;s cold right now, the suppression eases. This handles the classic spring pattern: cold mornings, warm afternoons. Plants &lt;em&gt;are&lt;/em&gt; still somewhat active on those warm afternoon hours, and a warm day drives real evaporation.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;warmDampening = clamp((warmerTemp - 65) / 30, 0, 0.5)
dormancyMoisture *= (1.0 - warmDampening)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At 80°F daytime highs, the dampening cuts dormancy suppression in half. At 95°F, it&amp;rsquo;s fully halved. The cold mornings still matter, but the warm afternoons are doing real drying work.&lt;/p&gt;
&lt;h2 id="low-evaporative-demand-when-dry-isnt-thirsty"&gt;Low Evaporative Demand: When Dry Isn&amp;rsquo;t Thirsty&lt;/h2&gt;
&lt;p&gt;Another Yardwise-specific edge case: on overcast, humid days with moderate temperatures, the soil might technically be &amp;ldquo;dry&amp;rdquo; by the engine&amp;rsquo;s moisture model, but the plants aren&amp;rsquo;t actually losing much water because evaporative demand is low.&lt;/p&gt;
&lt;p&gt;Cloudy skies reduce solar radiation. High humidity reduces the vapor pressure gradient that drives transpiration. If both conditions are present and the temperature is under 85°F, the engine injects a small wetness boost (up to 0.25) to push borderline &amp;ldquo;water today&amp;rdquo; verdicts toward &amp;ldquo;check soil&amp;rdquo; or &amp;ldquo;skip.&amp;rdquo;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;cloudFactor = clamp((cloudCover - 0.3) / 0.7, 0, 1)
humidityFactor = clamp((humidity - 0.4) / 0.5, 0, 1)
demandReduction = cloudFactor × 0.55 + humidityFactor × 0.45
boost = demandReduction × 0.25
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The guard at 85°F is important. Hot weather drives real water demand regardless of clouds or humidity. Above that threshold, evaporative demand is high enough that the boost doesn&amp;rsquo;t apply.&lt;/p&gt;
&lt;p&gt;This is a small adjustment, 0.25 at maximum, but it prevents the engine from recommending watering on days when the lawn genuinely doesn&amp;rsquo;t need it. Overwatering is a real problem, especially for casual gardeners who might not realize that a cool, overcast day means their yard is fine.&lt;/p&gt;
&lt;h2 id="the-meta-lesson"&gt;The Meta-Lesson&lt;/h2&gt;
&lt;p&gt;Every one of these edge cases started as a wrong verdict. A friend texted me &amp;ldquo;your app said Yes but the trail was iced over.&amp;rdquo; A beta tester reported &amp;ldquo;it&amp;rsquo;s telling me to water but it&amp;rsquo;s 40 degrees.&amp;rdquo; More personally, I&amp;rsquo;m literally looking at my skateboard ramp covered in ice and the app telling me it&amp;rsquo;s shred-ready. Nope.&lt;/p&gt;
&lt;p&gt;The temptation each time was to add a quick patch, an &lt;code&gt;if&lt;/code&gt; statement for the specific scenario. What I tried to do instead was understand &lt;em&gt;why&lt;/em&gt; the model was wrong and fix the underlying assumption. Usually the answer was: the model was treating something as a smooth continuum when reality has phase transitions and state changes.&lt;/p&gt;
&lt;p&gt;Water doesn&amp;rsquo;t gradually become less liquid as it gets colder. It freezes. Plants don&amp;rsquo;t linearly reduce water uptake as temperature drops. They go dormant. Snow doesn&amp;rsquo;t dry like rain. It melts.&lt;/p&gt;
&lt;p&gt;The edge cases are where the domain expertise lives. The drying model is arithmetic. The freeze/thaw logic is hard-won knowledge about how the physical world actually works.&lt;/p&gt;
&lt;h2 id="next-in-the-series"&gt;Next in the Series&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://stalefishlabs.com/the-lab/2026-03-25-flipping-the-question/"
&gt;next article&lt;/a&gt; covers the Yardwise inversion, how the engine flips from &amp;ldquo;is it too wet?&amp;rdquo; to &amp;ldquo;is it too dry?&amp;rdquo; and the additional features (manual watering tracking, establishment sensitivity) that make gardening a distinct problem from riding and field sports.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The Groundwise engine powers &lt;a href="https://stalefishlabs.com/apps/ridewise/"
target="_blank"
&gt;Ridewise&lt;/a&gt;, &lt;a href="https://stalefishlabs.com/apps/fieldwise/"
target="_blank"
&gt;Fieldwise&lt;/a&gt;, and &lt;a href="https://stalefishlabs.com/apps/yardwise/"
target="_blank"
&gt;Yardwise&lt;/a&gt; — all available for iOS from &lt;a href="https://stalefishlabs.com"
target="_blank"
&gt;Stalefish Labs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content:encoded><dc:creator>Michael Morrison</dc:creator><enclosure url="https://stalefishlabs.com/mtbfrozenedge.png" type="image/png" length="0"/></item><item><title>Turning Raw Weather Into a Drying Model</title><link>https://stalefishlabs.com/the-lab/2026-03-17-drying-model/</link><pubDate>Tue, 17 Mar 2026 00:00:00 -0700</pubDate><guid>https://stalefishlabs.com/the-lab/2026-03-17-drying-model/</guid><description>How we model surface drying after rain, and why a downpour and a drizzle leave very different trails.</description><content:encoded>&lt;p&gt;In the &lt;a href="https://stalefishlabs.com/the-lab/2026-03-05-weather-engine-intro/"
&gt;first article&lt;/a&gt; about my foray into using weather data to assess surface conditions for riding activities, I introduced the Groundwise engine, a software decision system that turns weather data into a yes/maybe/no verdict for outdoor conditions. This article goes deeper into the core moisture model: how the engine takes raw weather observations and calculates a wetness score that drives the verdict. The hope is to show how a simple question such as &amp;ldquo;can I ride my mountain bike today?&amp;rdquo; spiraled into a fairly complex yet intriguing design challenge. Totally fair if you aren&amp;rsquo;t entranced by weather math, I get it, but I feel like there&amp;rsquo;s some value in pulling back the curtain to reveal how much goes into solving a seemingly simple problem.&lt;/p&gt;
&lt;p&gt;The Groundwise model is relatively restrainted in scope, it isn&amp;rsquo;t trying to simulate soil physics. It&amp;rsquo;s trying to approximate what an experienced local would know instinctively — &amp;ldquo;it rained pretty hard yesterday, but it&amp;rsquo;s been warm and windy all morning, so the trails are probably fine.&amp;rdquo; The goal is to encode that intuition into something repeatable and surface-aware.&lt;/p&gt;
&lt;h2 id="weighted-precipitation-not-all-rain-is-equal"&gt;Weighted Precipitation: Not All Rain Is Equal&lt;/h2&gt;
&lt;p&gt;The engine tracks precipitation across three time windows: the last 24 hours, 24-48 hours ago, and 48-72 hours ago. But it doesn&amp;rsquo;t treat them equally. Here&amp;rsquo;s how they are weighted differently by the weather precipitation equation:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;weatherPrecip = rain24h × 0.7 + (rain48h - rain24h) × 0.2 + (rain72h - rain48h) × 0.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Recent rain dominates. An inch in the last 24 hours contributes seven times more to the score than an inch from two days ago. This matches real-world observation, where rain from three days ago has had significant time to drain and evaporate, while yesterday&amp;rsquo;s rain is still actively affecting conditions.&lt;/p&gt;
&lt;p&gt;The subtraction matters too. &lt;code&gt;rain48h&lt;/code&gt; is a &lt;em&gt;cumulative&lt;/em&gt; total (it includes the last 24 hours), so the formula isolates each window&amp;rsquo;s unique contribution. A half an inch total over 48 hours where all of it fell in the last 24 tells a very different story than half an inch spread evenly.&lt;/p&gt;
&lt;h3 id="rain-score-normalization"&gt;Rain Score Normalization&lt;/h3&gt;
&lt;p&gt;A recent rain results in a raw precipitation value that gets normalized to a 0-1 scale, so effectively a percentage:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rainScore = min(1.0, totalPrecip / 0.75)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Three-quarters of an inch saturates the score (maximum value of 1, or 100%). Beyond that, more rain doesn&amp;rsquo;t make things &lt;em&gt;more&lt;/em&gt; wet from the engine&amp;rsquo;s perspective — you&amp;rsquo;re already at maximum precipitation/saturation, which matches reality in that a trail, yard, or any other outdoor surface has a limit to how wet it can get. This prevents a 3-inch deluge from producing a wildly different result than a 1-inch soaking. Both are &amp;ldquo;very wet&amp;rdquo; — the distinction that matters is how long ago it happened and how fast things are drying.&lt;/p&gt;
&lt;h3 id="why-intensity-matters"&gt;Why Intensity Matters&lt;/h3&gt;
&lt;p&gt;The engine tracks rain pattern: light, steady, or downpour. It then applies a multiplier to the base wetness given one of those patterns:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Multiplier&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Light&lt;/td&gt;
&lt;td&gt;0.7x&lt;/td&gt;
&lt;td&gt;Gentle rain soaks in gradually, less runoff, less surface pooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;td&gt;Sustained saturation, ground can&amp;rsquo;t keep up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downpour&lt;/td&gt;
&lt;td&gt;1.5x&lt;/td&gt;
&lt;td&gt;Overwhelms drainage, causes pooling and surface flooding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A quarter inch of light rain over several hours is genuinely different from a quarter inch dumped in 20 minutes. The light rain absorbs more evenly. The downpour creates surface water, overwhelms drainage on absorbent surfaces, and can cause erosion on trails.&lt;/p&gt;
&lt;h2 id="drying-strength-the-recovery-side"&gt;Drying Strength: The Recovery Side&lt;/h2&gt;
&lt;p&gt;Wetness is only half the picture. The other half is how aggressively conditions are drying things out. The engine calculates a composite drying strength from four weather factors:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;drying = (tempFactor × 0.30) + (windFactor × 0.30) + (humidityFactor × 0.25) + (skyFactor × 0.15)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Temperature (30% weight):&lt;/strong&gt; Warmer air holds more moisture and drives evaporation. The factor scales linearly from 0 at 50°F to 1.0 at 90°F. Below 50°F, evaporation slows dramatically. Above 90°F, you&amp;rsquo;re drying as fast as conditions allow.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;tempFactor = clamp((tempF - 50) / 40, 0, 1)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Wind (30% weight):&lt;/strong&gt; Moving air carries moisture away from surfaces. Scales linearly up to 20 mph, where the effect plateaus - going from 20 to 40 mph doesn&amp;rsquo;t double the drying rate.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;windFactor = clamp(sustainedWindMph / 20, 0, 1)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Humidity (25% weight):&lt;/strong&gt; Dry air absorbs moisture more readily. This is the inverse, where low humidity means strong drying.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;humidityFactor = clamp(1.0 - effectiveHumidity, 0, 1)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When the weather API provides a dew point instead of relative humidity, the engine converts it using the Magnus formula. Dew point is actually the more reliable measurement — relative humidity fluctuates throughout the day even when actual moisture content stays constant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sky cover (15% weight):&lt;/strong&gt; Clear skies mean solar radiation hitting surfaces directly, which drives evaporation. Clouds block that. This gets the lowest weight because its effect is real but smaller than the others.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;skyFactor = clamp(1.0 - cloudCover, 0, 1)
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="exposure-matters"&gt;Exposure Matters&lt;/h3&gt;
&lt;p&gt;All of this gets modified by where the surface actually sits. A trail under heavy tree canopy dries differently than an open field fully exposed to sunlight:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exposure&lt;/th&gt;
&lt;th&gt;Rain Multiplier&lt;/th&gt;
&lt;th&gt;Drying Multiplier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exposed&lt;/td&gt;
&lt;td&gt;1.0x&lt;/td&gt;
&lt;td&gt;1.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shaded&lt;/td&gt;
&lt;td&gt;0.7x&lt;/td&gt;
&lt;td&gt;0.6x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Covered&lt;/td&gt;
&lt;td&gt;0.0x&lt;/td&gt;
&lt;td&gt;0.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Shaded spots get less rain (canopy intercepts 30%) but also dry 40% slower (less sun, less wind). Covered spots (under a roof or overhang) get no rain at all but still have air circulation for moderate drying.&lt;/p&gt;
&lt;p&gt;This creates an interesting dynamic: a shaded trail might actually be &lt;em&gt;drier&lt;/em&gt; than an exposed one after light rain (less water reached it) but &lt;em&gt;wetter&lt;/em&gt; after heavy rain (the rain that got through takes longer to leave).&lt;/p&gt;
&lt;h3 id="drying-classification"&gt;Drying Classification&lt;/h3&gt;
&lt;p&gt;The composite score maps to three tiers, each returning a fixed drying effectiveness value:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score Range&lt;/th&gt;
&lt;th&gt;Classification&lt;/th&gt;
&lt;th&gt;Effectiveness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;≥ 0.7&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;0.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.4 – 0.7&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 0.4&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I deliberately chose discrete tiers over a continuous curve. The difference between a 0.41 and a 0.69 drying score isn&amp;rsquo;t meaningful in practice — both represent &amp;ldquo;conditions are helping somewhat.&amp;rdquo; The tiers prevent false precision while still capturing the real distinction between &amp;ldquo;sunny and breezy&amp;rdquo; (strong), &amp;ldquo;overcast and mild&amp;rdquo; (moderate), and &amp;ldquo;cold, humid, and still&amp;rdquo; (weak).&lt;/p&gt;
&lt;h2 id="combining-it-all-the-wetness-score"&gt;Combining It All: The Wetness Score&lt;/h2&gt;
&lt;p&gt;I warned you this problem has more nuance that it would seem. But hang in there, we&amp;rsquo;re getting closer to arriving at some meaningful mathematical conclusions. For example, the wetness score blends precipitation and drying into a single 0-1 value:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;baseWetness = rainScore × 0.5 + (rainScore × timingScore) × 0.6
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This formula does something subtle. The first term (&lt;code&gt;rainScore × 0.5&lt;/code&gt;) ensures that heavy rain always contributes &lt;em&gt;some&lt;/em&gt; wetness regardless of timing. The second term (&lt;code&gt;rainScore × timingScore&lt;/code&gt;) captures the interaction — recent heavy rain is much worse than old heavy rain. The timing score decays linearly from 1.0 (just stopped) to 0.0 (24+ hours ago).&lt;/p&gt;
&lt;p&gt;After the base calculation, the rain pattern multiplier is applied (0.7x for light, 1.1x for steady, 1.5x for downpour), then drying reduces it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;dryingEffect = dryingScore × 0.3 × dryingEffectiveness
wetness = baseWetness × (1.0 - dryingEffect)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;dryingEffectiveness&lt;/code&gt; factor prevents drying from being too aggressive when rain just ended. If rain stopped 30 minutes ago, even strong drying conditions haven&amp;rsquo;t had time to do much yet. The factor scales up as time passes, so drying&amp;rsquo;s impact grows the longer it&amp;rsquo;s been since rain.&lt;/p&gt;
&lt;h3 id="a-worked-example"&gt;A Worked Example&lt;/h3&gt;
&lt;p&gt;To put all these equations into perspective, let&amp;rsquo;s trace through a real scenario to arrive at a Groundwise verdict: &lt;strong&gt;0.4 inches of steady rain ended 8 hours ago. It&amp;rsquo;s 72°F, 12 mph wind, 45% humidity, 20% cloud cover. Exposed dirt trail.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rain score:&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;weatherPrecip = 0.4 × 0.7 = 0.28 (all in last 24h)
rainScore = min(1.0, 0.28 / 0.75) = 0.37
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Timing score:&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;hours = 480 min / 60 = 8
timingScore = max(0, 1.0 - 8/24) = 0.67
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Drying strength:&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;temp = (72-50)/40 × 0.30 = 0.165
wind = 12/20 × 0.30 = 0.18
humidity = 0.55 × 0.25 = 0.1375
sky = 0.80 × 0.15 = 0.12
total = 0.60 → Moderate (effectiveness = 0.5)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Wetness:&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;base = 0.37 × 0.5 + (0.37 × 0.67) × 0.6 = 0.185 + 0.149 = 0.334
× steady pattern (1.1) = 0.367
dryingEffectiveness = max(0.2, 1.0 - 0.67 × 0.5) = 0.665
dryingEffect = 0.5 × 0.3 × 0.665 = 0.10
wetness = 0.367 × (1.0 - 0.10) = 0.33
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Verdict: Maybe&lt;/strong&gt; (0.33 is just above the 0.3 threshold). The trail is borderline — rideable but still damp. On a dirt trail (damage sensitivity 0.5), the sensitivity veto wouldn&amp;rsquo;t trigger. A reasonable call either way, and the engine goes with Maybe, giving the rider the opportunity to then weigh locale-specific details such as trail sensitivity to damage.&lt;/p&gt;
&lt;p&gt;Now change one variable — make it 85°F instead of 72°F:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;temp = (85-50)/40 × 0.30 = 0.2625
total drying = 0.70 → Strong (effectiveness = 0.8)
dryingEffect = 0.8 × 0.3 × 0.665 = 0.16
wetness = 0.367 × (1.0 - 0.16) = 0.308
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Still Maybe, but barely. A bit more wind or another hour of drying and it flips to Yes. That matches intuition — a hot afternoon pulls moisture out fast.&lt;/p&gt;
&lt;h2 id="residual-wetness-the-ground-remembers"&gt;Residual Wetness: The Ground Remembers&lt;/h2&gt;
&lt;p&gt;The timing-based model handles most scenarios well, but it has a blind spot: &lt;strong&gt;ground saturation after heavy rain on absorbent surfaces.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If it rained an inch on Monday and you&amp;rsquo;re checking the trail on Wednesday, the timing score says &amp;ldquo;it&amp;rsquo;s been 48 hours, moisture contribution is minimal.&amp;rdquo; But anyone who&amp;rsquo;s walked a clay trail two days after heavy rain knows that&amp;rsquo;s wrong. The ground is still holding water.&lt;/p&gt;
&lt;p&gt;The engine adds a residual wetness component for absorbent surfaces (dirt, grass, clay, amended soil) when precipitation exceeded 0.25 inches:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;volumeFactor = clamp((rain - 0.25) / 0.75, 0, 1)
decayFactor = based on hours since rain and drying strength
surfaceRetention = inverse of surface drying multiplier
dryingReduction = strength-based reduction
residualWetness = 0.55 × volumeFactor × decayFactor × surfaceRetention × (1.0 - dryingReduction)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The peak residual value of ~0.55 deliberately sits in the middle of the Maybe range. It&amp;rsquo;s not enough to trigger a hard No, but it keeps the engine from prematurely saying Yes on saturated ground.&lt;/p&gt;
&lt;p&gt;Clay surfaces (drying multiplier 0.7x) retain the most residual moisture. Concrete and metal (2.0x+) don&amp;rsquo;t get residual wetness at all, they&amp;rsquo;re draining surfaces that hold little (concrete) to no (metal) water.&lt;/p&gt;
&lt;h2 id="what-this-model-doesnt-do"&gt;What This Model Doesn&amp;rsquo;t Do&lt;/h2&gt;
&lt;p&gt;A few things I intentionally left out:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Soil type modeling.&lt;/strong&gt; Real evapotranspiration models account for soil composition, drainage rates, water table depth. The engine uses surface type as a proxy instead. A dirt trail in sandy Arizona soil dries differently than one in Georgia red clay, and the engine can&amp;rsquo;t distinguish them. The tradeoff is simplicity — asking users to classify their soil composition seemed like a bridge too far. If you disagree, let us know, but I decided to err on the side of simplicity at least in terms of user inputs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microclimate effects.&lt;/strong&gt; Two spots a mile apart can have meaningfully different conditions. The engine uses a single weather observation for each saved location, which might come from a station several miles away. Elevation, valley effects, and urban heat islands aren&amp;rsquo;t modeled.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Drainage infrastructure.&lt;/strong&gt; A well-designed trail with proper drainage handles rain better than a flat trail in a depression. An athletic field with subsurface drainage is ready for play sooner than one without. The engine doesn&amp;rsquo;t know about this. Surface type captures some of it (artificial turf implies drainage engineering), but it&amp;rsquo;s imperfect.&lt;/p&gt;
&lt;p&gt;These are all real limitations, and I&amp;rsquo;d rather be transparent about them than pretend the model is more precise than it is. The engine targets &amp;ldquo;better than guessing&amp;rdquo; — not &amp;ldquo;better than walking over and checking yourself.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="next-in-the-series"&gt;Next in the Series&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://stalefishlabs.com/the-lab/2026-03-19-one-engine-three-apps/"
&gt;next article&lt;/a&gt; covers how this engine is packaged as a shared Swift framework consumed by three different apps — each with its own surface library, threshold calibration, and verdict interpretation. Same core math, three different products. It&amp;rsquo;s a lesson in code reuse, and the benefits of solving a problem once in just a general enough way to apply that solution multiple times.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The drying model powers &lt;a href="https://stalefishlabs.com/apps/ridewise/"
target="_blank"
&gt;Ridewise&lt;/a&gt;, &lt;a href="https://stalefishlabs.com/apps/fieldwise/"
target="_blank"
&gt;Fieldwise&lt;/a&gt;, and &lt;a href="https://stalefishlabs.com/apps/yardwise/"
target="_blank"
&gt;Yardwise&lt;/a&gt; — all available for iOS from &lt;a href="https://stalefishlabs.com"
target="_blank"
&gt;Stalefish Labs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content:encoded><dc:creator>Michael Morrison</dc:creator><enclosure url="https://stalefishlabs.com/floodedgrass.png" type="image/png" length="0"/></item><item><title>I Built a Weather Engine That Tells You When to Ride, Play, or Water</title><link>https://stalefishlabs.com/the-lab/2026-03-12-weather-engine-intro/</link><pubDate>Thu, 12 Mar 2026 00:00:00 -0700</pubDate><guid>https://stalefishlabs.com/the-lab/2026-03-12-weather-engine-intro/</guid><description>One decision engine, three audiences: a yes/maybe/no verdict for riders, rec league players, and gardeners.</description><content:encoded>&lt;p&gt;Every mountain biker knows the ritual. It rained last night. You check the radar, clear now. You check the trail association&amp;rsquo;s Facebook page, nobody&amp;rsquo;s posted. You text your riding buddy: &amp;ldquo;Think the trails are good?&amp;rdquo; They don&amp;rsquo;t know either. So you either stay home and miss a perfectly rideable day, or show up and chew through muddy trails that needed another six hours to dry.&lt;/p&gt;
&lt;p&gt;I got tired of guessing so I built a weather decision engine that answers the question directly: &lt;strong&gt;is it too wet to ride?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Then I realized the same engine could answer two more questions: &lt;strong&gt;is this field playable?&lt;/strong&gt; and &lt;strong&gt;does my garden need watering?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the story of the Groundwise engine, a single Swift package that powers three iOS apps by asking the same underlying question from different perspectives.&lt;/p&gt;
&lt;h2 id="the-problem-with-did-it-rain"&gt;The Problem With &amp;ldquo;Did It Rain?&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Weather apps mostly tell you what &lt;em&gt;will&lt;/em&gt; happen, and to a lesser degree they can tell you &lt;em&gt;what&lt;/em&gt; happened. What they don&amp;rsquo;t tell you is &lt;em&gt;what it means&lt;/em&gt; for the specific thing you want to do.&lt;/p&gt;
&lt;p&gt;Half an inch of rain means completely different things depending on context:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On a &lt;strong&gt;concrete skatepark&lt;/strong&gt;, it sheds in a couple hours. You&amp;rsquo;re probably fine by afternoon.&lt;/li&gt;
&lt;li&gt;On a &lt;strong&gt;dirt trail&lt;/strong&gt;, it might need 24 hours with good sun and wind. Or 48 hours or more if it&amp;rsquo;s overcast and cold.&lt;/li&gt;
&lt;li&gt;On a &lt;strong&gt;clay tennis court&lt;/strong&gt;, you might be waiting even longer, clay holds moisture and damages easily when wet.&lt;/li&gt;
&lt;li&gt;In a &lt;strong&gt;container garden on a sunny patio&lt;/strong&gt;, that half inch drained through the potting mix hours ago and your herbs might need water again tomorrow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The answer isn&amp;rsquo;t just &amp;ldquo;how much rain,&amp;rdquo; it&amp;rsquo;s how much rain, on what surface, how long ago, and what the drying conditions have been since.&lt;/p&gt;
&lt;h2 id="from-weather-data-to-a-verdict"&gt;From Weather Data to a Verdict&lt;/h2&gt;
&lt;p&gt;So I built a weather analysis engine that takes in a handful of weather observations and produces a single categorical verdict regarding an action on a specific surface: &lt;strong&gt;Yes&lt;/strong&gt;, &lt;strong&gt;Maybe&lt;/strong&gt;, or &lt;strong&gt;No&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The inputs are straightforward, stuff you&amp;rsquo;d get from any weather API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Precipitation totals (last 24, 48, and 72 hours)&lt;/li&gt;
&lt;li&gt;How long since rain ended&lt;/li&gt;
&lt;li&gt;Whether it was light, steady, or a downpour&lt;/li&gt;
&lt;li&gt;Current temperature, wind speed, humidity, and cloud cover&lt;/li&gt;
&lt;li&gt;Whether it&amp;rsquo;s actively raining right now&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plus one critical piece of context: &lt;strong&gt;what surface are you asking about?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The engine defines 17 surface types across three categories. Each has a drying multiplier that controls how fast moisture clears, for example:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Drying Speed&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metal skateboard ramps&lt;/td&gt;
&lt;td&gt;2.8x&lt;/td&gt;
&lt;td&gt;Fastest, sheds water, heats up quickly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Artificial turf&lt;/td&gt;
&lt;td&gt;2.5x&lt;/td&gt;
&lt;td&gt;Engineered to drain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asphalt&lt;/td&gt;
&lt;td&gt;2.2x&lt;/td&gt;
&lt;td&gt;Dark surface, absorbs heat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concrete&lt;/td&gt;
&lt;td&gt;2.0x&lt;/td&gt;
&lt;td&gt;Drains well, slower to heat than asphalt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Potting mix&lt;/td&gt;
&lt;td&gt;2.0x&lt;/td&gt;
&lt;td&gt;Loose, fast-draining soil&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compost&lt;/td&gt;
&lt;td&gt;1.2x&lt;/td&gt;
&lt;td&gt;Active decomposition generates some heat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dirt/ground&lt;/td&gt;
&lt;td&gt;1.0x&lt;/td&gt;
&lt;td&gt;Baseline, absorbs and holds water&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural grass&lt;/td&gt;
&lt;td&gt;1.0x&lt;/td&gt;
&lt;td&gt;Root systems retain moisture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amended soil (mulched)&lt;/td&gt;
&lt;td&gt;0.7x&lt;/td&gt;
&lt;td&gt;Mulch deliberately slows evaporation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clay&lt;/td&gt;
&lt;td&gt;0.7x&lt;/td&gt;
&lt;td&gt;Dense, holds water, slow to release&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That 4x difference between metal and clay isn&amp;rsquo;t just a number, it&amp;rsquo;s the difference between &amp;ldquo;rideable in an hour&amp;rdquo; and &amp;ldquo;unplayable until tomorrow.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="the-decision-pipeline"&gt;The Decision Pipeline&lt;/h2&gt;
&lt;p&gt;The engine runs checks in a specific order, with the most critical conditions evaluated first:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Is the ground frozen?&lt;/strong&gt; If there&amp;rsquo;s an ice or snow hazard, the verdict is No regardless of moisture. This check alone is about 360 lines of code, freeze/thaw cycles are surprisingly complex. A concrete skatepark clears ice in 6 hours at 55°F, but a dirt trail might need 72 hours at 45°F to fully thaw and dry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Is it actively raining?&lt;/strong&gt; Straightforward gate. If precipitation is falling, the answer is No (or Maybe for very light drizzle on a fast-draining surface with strong drying conditions).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Calculate a wetness score.&lt;/strong&gt; This is where the real modeling happens. The engine combines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;rain score&lt;/strong&gt; weighted toward recent precipitation (70% from last 24h, 20% from 24-48h, 10% from 48-72h)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;timing score&lt;/strong&gt; that decays linearly over 24 hours since rain ended&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;drying strength&lt;/strong&gt; composite that factors in temperature, wind, humidity, and cloud cover&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Residual wetness&lt;/strong&gt; for absorbent surfaces, heavy rain on dirt or clay retains moisture well beyond what the timing score captures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Apply the verdict thresholds.&lt;/strong&gt; A wetness score below 0.3 means Yes (dry enough). Between 0.3 and 0.6 means Maybe (borderline). Above 0.6 means No (too wet).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Sensitivity veto.&lt;/strong&gt; High-sensitivity surfaces (like newly seeded lawns or natural grass athletic fields) get pushed from Maybe toward a more protective verdict. The engine would rather tell you to wait than let you damage a surface that&amp;rsquo;s expensive to repair.&lt;/p&gt;
&lt;h2 id="one-engine-two-perspectives"&gt;One Engine, Two Perspectives&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re wondering how the above, which is described in terms of &lt;strong&gt;using&lt;/strong&gt; a surface, could possibly apply to a container garden, it&amp;rsquo;s a fair question. And this is where the engine morphed into serving a series of apps instead of just one:&lt;/p&gt;
&lt;p&gt;The engine calculates a &lt;strong&gt;wetness score&lt;/strong&gt;. For riders and athletes, high wetness is bad, you want dry conditions. For gardeners, high wetness is good, it means you can skip watering.&lt;/p&gt;
&lt;p&gt;Same math, opposite interpretation:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Wetness Score&lt;/th&gt;
&lt;th&gt;Rider/Athlete View&lt;/th&gt;
&lt;th&gt;Gardener View&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low (&amp;lt; 0.3)&lt;/td&gt;
&lt;td&gt;Yes, go ride!&lt;/td&gt;
&lt;td&gt;Yes, water today&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium (0.3-0.6)&lt;/td&gt;
&lt;td&gt;Maybe, check conditions&lt;/td&gt;
&lt;td&gt;Maybe, check soil&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High (&amp;gt; 0.6)&lt;/td&gt;
&lt;td&gt;No, too wet&lt;/td&gt;
&lt;td&gt;No, skip watering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The engine calls this &lt;code&gt;EngineMode&lt;/code&gt;, either &lt;code&gt;wetnessConcern&lt;/code&gt; or &lt;code&gt;drynessConcern&lt;/code&gt;. A single enum that flips the entire interpretation layer while keeping the underlying moisture model identical.&lt;/p&gt;
&lt;p&gt;Yardwise (the gardening app) uses slightly shifted thresholds, 0.15 and 0.45 instead of 0.3 and 0.6, because the stakes of getting it wrong are lower. Missing a watering day is a mild inconvenience. Riding a muddy trail damages the trail. Playing softball on a muddy clay infield isn&amp;rsquo;t great. You get the idea.&lt;/p&gt;
&lt;h2 id="why-not-just-show-a-percentage"&gt;Why Not Just Show a Percentage?&lt;/h2&gt;
&lt;p&gt;Early versions of the apps showed a moisture percentage, and it wasn&amp;rsquo;t good. I didn&amp;rsquo;t know what to do with &amp;ldquo;the trail is at 47% moisture.&amp;rdquo; Is that rideable? It depends on the surface, on your own risk/comfort tolerance, on whether you care about trail damage, etc. A percentage pushes the decision back onto the user, which is exactly the problem the app was supposed to solve.&lt;/p&gt;
&lt;p&gt;The solution was to simplify down to three states: Yes/Maybe/No, with an explanation of &lt;em&gt;why&lt;/em&gt;. The Maybe state exists specifically for conditions where reasonable people would disagree. I have near me both public and private mountain bike trails, and the tolerance for mud varies widely based on level of traffic, so Maybe is when deferring to rider&amp;rsquo;s judgement is actually correct. The engine shows you the contributing factors (temperature is helping, but humidity is holding things back, for example) so you can make the final call.&lt;/p&gt;
&lt;p&gt;Each verdict also carries a &lt;strong&gt;confidence level&lt;/strong&gt; (Low, Medium, High). Confidence drops when timing data is missing, when rain has been patchy and unpredictable, or when the wetness score lands right on a threshold boundary.&lt;/p&gt;
&lt;h2 id="whats-coming-in-this-series"&gt;What&amp;rsquo;s Coming in This Series&lt;/h2&gt;
&lt;p&gt;If you find talk of drying multipliers and residual wetness scores riveting, then hold on to your butts! This is merely the first article in a series about building the Groundwise engine. Coming up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Turning Raw Weather Into a Drying Model&lt;/strong&gt; - The math behind drying strength, residual wetness, and why rain intensity matters as much as total accumulation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One Engine, Three Apps&lt;/strong&gt; - How to structure a Swift package that serves multiple products with different surface libraries and threshold calibrations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge Cases That Break Your Weather Model&lt;/strong&gt; - Freeze/thaw cycles, cold-weather plant dormancy, the surprising complexity of &amp;ldquo;is snow melting?&amp;rdquo;, and dispatches from the great Nashville ice storm of 2026&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flipping the Question&lt;/strong&gt; - How the Yardwise inversion works, including manual watering tracking and evaporative demand suppression&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Building Confidence Into Uncertain Verdicts&lt;/strong&gt; - Why three states beat a percentage, and how to communicate uncertainty without creating anxiety&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-apps"&gt;The Apps&lt;/h2&gt;
&lt;p&gt;The Groundwise engine powers three apps, all available for iOS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stalefishlabs.com/apps/ridewise/"
target="_blank"
&gt;&lt;strong&gt;Ridewise&lt;/strong&gt;&lt;/a&gt;, ride conditions for trails, skateparks, bike parks, and pump tracks&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stalefishlabs.com/apps/fieldwise/"
target="_blank"
&gt;&lt;strong&gt;Fieldwise&lt;/strong&gt;&lt;/a&gt;, field conditions for rec league sports on grass, turf, clay, and hard courts&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stalefishlabs.com/apps/yardwise/"
target="_blank"
&gt;&lt;strong&gt;Yardwise&lt;/strong&gt;&lt;/a&gt;, watering guidance for casual gardeners with lawns, beds, and containers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three are built by &lt;a href="https://stalefishlabs.com"
target="_blank"
&gt;Stalefish Labs&lt;/a&gt;, a small indie studio focused on simple tools for getting outside and doing things together.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;If you&amp;rsquo;ve built something that turns raw data into human decisions, I&amp;rsquo;d love to hear how you approached the threshold and confidence problem. Drop a comment or find us on &lt;a href="https://bsky.app/profile/stalefishlabs.bsky.social"
target="_blank"
&gt;Bluesky&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content:encoded><dc:creator>Michael Morrison</dc:creator><enclosure url="https://stalefishlabs.com/mtbstorm.png" type="image/png" length="0"/></item><item><title>The Weather Says Clear Skies. So Why Is the Trail a Swamp?</title><link>https://stalefishlabs.com/the-lab/2026-03-05-why-is-the-trail-a-swamp/</link><pubDate>Thu, 05 Mar 2026 00:00:00 -0800</pubDate><guid>https://stalefishlabs.com/the-lab/2026-03-05-why-is-the-trail-a-swamp/</guid><description>Weather apps tell you what the weather is. They don't tell you what last night's rain did to the ground beneath your tires.</description><content:encoded>&lt;p&gt;I love mountain biking. I love skateboarding. I love pretty much anything that involves wheels and dirt and concrete and the outdoors. What I don&amp;rsquo;t love is driving 30 minutes to a trailhead only to find the trail is a rutted, muddy mess, despite the forecast showing nothing but sunshine.&lt;/p&gt;
&lt;p&gt;This is sadly not an isolated occurrence.&lt;/p&gt;
&lt;h2 id="the-gap-between-weather-and-reality"&gt;The Gap Between Weather and Reality&lt;/h2&gt;
&lt;p&gt;Weather apps are great at telling you what the weather is and often what it will be. Temperature, wind, humidity, chance of rain, it&amp;rsquo;s all there. What they don&amp;rsquo;t tell you is what last night&amp;rsquo;s rain did to the ground beneath your tires. That half inch at 2am? On a south-facing dirt trail with good drainage, it might be bone dry by noon. On a shaded clay singletrack in the woods? Come back Thursday. Or what about the concrete skatepark in direct sunlight vs. the shaded halfpipe layered in Skatelite? Not the same.&lt;/p&gt;
&lt;p&gt;For years I did what every rider or skater does. I&amp;rsquo;d check the radar, look at the hourly history, maybe glance at the 48-hour precipitation total, and make a gut call. Sometimes I&amp;rsquo;d text a buddy: &amp;ldquo;You think Warner is rideable?&amp;rdquo; And they&amp;rsquo;d text back something equally scientific: &amp;ldquo;Probably?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The variables are too fuzzy. How much did it rain? When did it stop? What&amp;rsquo;s the temperature now? Is the wind helping things dry? Is the trail in full sun or deep shade? Is the surface dirt, gravel, concrete, wood? Did it freeze overnight and is now thawing into a soupy mess? Each of these factors interacts with the others in ways that are genuinely hard to hold in your head all at once.&lt;/p&gt;
&lt;h2 id="so-i-built-an-app"&gt;So I Built an App&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m a developer, so naturally my response to this frustration was to write software. What started as a personal side project, just a quick tool to check if my local spots were rideable, turned into something much bigger once I realized how many people share this exact problem.&lt;/p&gt;
&lt;p&gt;The app pulls real weather data, looks at recent precipitation, drying conditions, temperature trends, freeze-thaw cycles, surface type, sun exposure, all of it, and synthesizes it into a simple answer: &lt;strong&gt;Yes&lt;/strong&gt;, &lt;strong&gt;Maybe&lt;/strong&gt;, or &lt;strong&gt;No&lt;/strong&gt;. It tells you whether conditions are likely suitable for riding right now, and gives you a seven-day outlook so you can plan ahead.&lt;/p&gt;
&lt;p&gt;Building it forced me to formalize all of those gut-feel heuristics I&amp;rsquo;d been running in my head for years. How much rain is too much? How long does it take to dry? Does Ramp Armor dry faster than wood or metal? When does a freeze actually matter? Translating intuition into code is humbling, because it forces you to confront how much of your &amp;ldquo;expertise&amp;rdquo; is just vibes.&lt;/p&gt;
&lt;h2 id="its-still-an-inexact-science"&gt;It&amp;rsquo;s Still an Inexact Science&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the thing I want to be honest about: even with a carefully crafted and dedicated prediction engine running real weather data through a multi-phase algorithm, this is still an inexact science. Ground conditions are hyperlocal. Two trails a mile apart can behave completely differently based on soil composition, tree cover, elevation, and drainage. No amount of weather data can perfectly capture what&amp;rsquo;s happening at a specific spot on the earth&amp;rsquo;s surface.&lt;/p&gt;
&lt;p&gt;The app gives you a well-informed estimate, and in my experience, it&amp;rsquo;s right far more often than my old gut-check method. But it&amp;rsquo;s not infallible, and I never want to pretend it is.&lt;/p&gt;
&lt;p&gt;That honesty is actually baked into the product. Users can submit feedback when a verdict doesn&amp;rsquo;t match what they found on the ground. And behind the scenes, I use an LLM to process those reports, analyzing the weather conditions, the surface type, the user&amp;rsquo;s description of what they actually encountered, and then use that to identify patterns where the engine&amp;rsquo;s assumptions might be off. It&amp;rsquo;s a feedback loop that lets the prediction model evolve based on real-world ground truth from actual riders at actual spots.&lt;/p&gt;
&lt;p&gt;I think that&amp;rsquo;s what makes this problem so interesting. It sits right at the intersection of atmospheric data, soil science, and lived experience. No model will ever be perfect, but a model that learns from the people using it every day can keep getting better.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s Next&lt;/h2&gt;
&lt;p&gt;In a &lt;a href="https://stalefishlabs.com/the-lab/2026-03-11-weather-engine-intro/"
&gt;future post&lt;/a&gt;, I plan to pull back the curtain on the prediction engine itself, how it evaluates moisture, what the drying model looks like, how freeze-thaw cycles are handled, and why certain surfaces behave so differently from others. It&amp;rsquo;s a surprisingly deep rabbit hole, and I think anyone who&amp;rsquo;s ever stared at a weather app trying to decide &amp;ldquo;is it rideable?&amp;rdquo; will find it interesting.&lt;/p&gt;
&lt;p&gt;Until then, check the app, trust the verdict, and if it&amp;rsquo;s wrong, tell me. That&amp;rsquo;s how we make it better.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://stalefishlabs.com/apps/ridewise/"
target="_blank"
&gt;Ridewise&lt;/a&gt; is available now on the App Store, built by &lt;a href="https://stalefishlabs.com"
target="_blank"
&gt;Stalefish Labs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content:encoded><dc:creator>Michael Morrison</dc:creator><enclosure url="https://stalefishlabs.com/ridewise.png" type="image/png" length="0"/></item></channel></rss>