<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-4442573390356643522</id><updated>2026-09-22T14:06:52.359-04:00</updated><category term="Flex"/><category term="ArcGIS"/><category term="Big Data"/><category term="Python"/><category term="Spark"/><category term="Machine Learning"/><category term="Java"/><category term="Talks"/><category term="GenAI"/><category term="ArcGIS Pro"/><category term="GIS"/><category term="Mobile"/><category term="Shapefile"/><category term="Clustering"/><category term="Heat Map"/><category term="AI"/><category term="Docker"/><category term="JavaScript"/><category term="Jupyter"/><category term="MLX"/><category term="Computer Vision"/><category term="DuckDB"/><category term="Generative AI"/><category term="GeoAI"/><category term="MPC"/><category term="ArcPy"/><category term="AutoGluon"/><category term="AutoML"/><category term="Claude Code"/><category term="Control Systems"/><category term="DQN"/><category term="Deep Reinforcement Learning"/><category term="Gymnasium"/><category term="Jetson"/><category term="LunarLander"/><category term="MCP"/><category term="Model Predictive Control"/><category term="NYC"/><category term="OpenCV"/><category term="PyTorch"/><category term="Quadratic Programming"/><category term="RAG"/><category term="Reinforcement Learning"/><category term="Technology"/><category term="WebGPU"/><category term="langchain"/><category term="ollama"/><title type='text'>Thunderhead Explorer</title><subtitle type='html'>Tips and Tricks using GIS BigData, GeoAI, GenAI, ML and other fun stuff :-)</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>204</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-8089864158300877921</id><published>2026-09-22T14:06:52.359-04:00</published><updated>2026-09-22T14:06:52.359-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="MLX"/><category scheme="http://www.blogger.com/atom/ns#" term="MPC"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>The Trainee Got Better. It Still Cannot Fly Alone.</title><content type='html'>&lt;p&gt;I ended &lt;a href=&quot;https://thunderheadxpler.blogspot.com/2026/09/the-engineer-and-trainee-adaptive-mpc.html&quot;&gt;The Engineer and the Trainee&lt;/a&gt; with a plan: roll out the shielded pilot, relabel every visited state with MPC, retrain, and see whether the trainee could finally fly on its own. That is still the plan. But before getting there I did something lazier, and the result was more interesting than I expected. I also fixed something that had been bothering me every time I watched a flight.&lt;/p&gt;

&lt;p style=&quot;text-align:center&quot;&gt;&lt;a href=&quot;https://github.com/mraad/lunar-mpc-laya&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/mraad/lunar-mpc-laya/main/docs/assets/distilled-shield.gif&quot; alt=&quot;Distilled Laya proposing commands from telemetry while adaptive MPC vetoes unsafe ones, landing through a 60% engine loss&quot; width=&quot;1000&quot; style=&quot;max-width:100%;height:auto&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align:center&quot;&gt;&lt;em&gt;The same recorded flight as last time, re-run with a smoother controller and a retrained model. The engine loses 60% of its thrust at ten seconds, the estimate falls to 40%, and the shield now steps in on about one command in eight instead of one in three.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;The lander had the shakes&lt;/h2&gt;

&lt;p&gt;Watching a descent, the ship twitched. Not a rendering artifact &amp;mdash; the controller really was changing its mind. I measured it: over 30 flights the beam search picked a different command on &lt;strong&gt;77.7%&lt;/strong&gt; of decisions. Every fifth of a second it re-derived the whole plan from scratch, and when two commands were nearly tied it would happily alternate between them. The tank chattered between throttle settings and the attitude jets flipped sign.&lt;/p&gt;

&lt;p&gt;The fix is one term in the cost function: charge a candidate for differing from the command actually being flown. One knob, normalized so it covers tilt and throttle together. Here is what it bought, measured over seeds 3000&amp;ndash;3009 on all three pads:&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&amp;nbsp;&lt;/th&gt;&lt;th&gt;No penalty&lt;/th&gt;&lt;th&gt;With penalty&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Decisions that change command (nominal)&lt;/td&gt;&lt;td&gt;77.7%&lt;/td&gt;&lt;td&gt;20.6%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Stage-to-stage tilt jerk&lt;/td&gt;&lt;td&gt;5.37&amp;deg; RMS&lt;/td&gt;&lt;td&gt;1.69&amp;deg; RMS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Landings, all three scenarios&lt;/td&gt;&lt;td&gt;30/30&lt;/td&gt;&lt;td&gt;30/30&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Fuel remaining at &amp;times;0.4 thrust&lt;/td&gt;&lt;td&gt;62.6&lt;/td&gt;&lt;td&gt;65.6&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Not a trade at all, which surprised me. Smoother, and it landed the same flights using &lt;em&gt;less&lt;/em&gt; fuel and slightly fewer decisions. The commands it stopped issuing were mostly ones it would have undone on the next stage anyway.&lt;/p&gt;

&lt;h2&gt;The same fix broke the sibling project&lt;/h2&gt;

&lt;p&gt;Naturally I tried the identical change in &lt;a href=&quot;https://github.com/mraad/lunar-mpc&quot;&gt;lunar-mpc&lt;/a&gt;, the Box2D controller this one descends from. On nominal flights it looked like a win: 49 of 50 strict landing checks went to 50 of 50. I nearly kept it.&lt;/p&gt;

&lt;p&gt;Then I measured the engine-fault scenario &amp;mdash; the one that repository exists to demonstrate &amp;mdash; and it fell from 49/50 to &lt;strong&gt;41/50&lt;/strong&gt;. The fixed-model controller went from 42/50 to 24/50. The reason is obvious in hindsight: that lander&#39;s main engine is binary, on or off, so rapid pulsing is precisely how it holds a thrust level between the two. Charging for the pulses removes its only means of modulating thrust. Here, with three throttle settings, holding one is a real option.&lt;/p&gt;

&lt;p&gt;So I reverted it there and wrote the numbers into that repository&#39;s research limits, because a rule that only holds in one of two codebases is worth recording where someone will trip over it. The broader lesson I have now written into my own notes: sweep a control-cost change over &lt;em&gt;every&lt;/em&gt; published scenario before adopting it, not the one that runs quickest.&lt;/p&gt;

&lt;h2&gt;A consequence I did not plan for&lt;/h2&gt;

&lt;p&gt;Once the controller penalizes changing command, its next choice depends on what it is currently doing. Roughly four decisions in five are now &amp;ldquo;hold what you are doing.&amp;rdquo; That makes the flown command the single most predictive feature of the teacher&#39;s answer &amp;mdash; and the trainee could not see it.&lt;/p&gt;

&lt;p&gt;So the prompt gained one sentence: &lt;em&gt;&amp;ldquo;Current command: tilt hold, engine half.&amp;rdquo;&lt;/em&gt; I want to be precise about why that is not a relapse into the hint-giving of the first experiment. MPC&#39;s &lt;strong&gt;requested&lt;/strong&gt; command is the teacher&#39;s answer, and the earlier post showed Laya simply copies it. The &lt;strong&gt;flown&lt;/strong&gt; command is actuator state, the kind any onboard controller reads from its own vehicle. The trainee still has to decide what to do next.&lt;/p&gt;

&lt;h2&gt;Retraining, on four GPUs this time&lt;/h2&gt;

&lt;p&gt;Same recipe as before, except the data came from the smoothed teacher and the run used all four RTX PRO 4500 Blackwell cards instead of three. Eight epochs in 235 seconds. Per-question validation accuracy went from &lt;strong&gt;80.0% to 90.2%&lt;/strong&gt;, and it passed 85% after the first epoch, better than anything the previous run reached in eight. A genuinely better student.&lt;/p&gt;

&lt;p&gt;Then I flew it on the same 90 held-out flights, and this is the part worth the post:&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&amp;nbsp;&lt;/th&gt;&lt;th&gt;Previous checkpoint&lt;/th&gt;&lt;th&gt;Retrained&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Validation accuracy&lt;/td&gt;&lt;td&gt;80.0%&lt;/td&gt;&lt;td&gt;90.2%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Landings, unshielded&lt;/td&gt;&lt;td&gt;0 / 90&lt;/td&gt;&lt;td&gt;1 / 90&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Landings, with the MPC shield&lt;/td&gt;&lt;td&gt;90 / 90&lt;/td&gt;&lt;td&gt;90 / 90&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Laya&#39;s own choice kept&lt;/td&gt;&lt;td&gt;55&amp;ndash;62%&lt;/td&gt;&lt;td&gt;80&amp;ndash;87%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Shield overrides&lt;/td&gt;&lt;td&gt;30&amp;ndash;37%&lt;/td&gt;&lt;td&gt;12&amp;ndash;17%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Ten points of accuracy bought exactly one landing out of ninety. The trainee is still not a pilot. One wrong command puts the ship slightly off the teacher&#39;s trajectory, the next state is less familiar, agreement collapses to about 17%, and the flight diverges. That is behaviour cloning without a corrective signal, and making the student more accurate on the teacher&#39;s own path does not fix it &amp;mdash; which is a sharper argument for DAgger than I had before, because it says the missing signal is off-trajectory states rather than label quality.&lt;/p&gt;

&lt;p&gt;What the better student &lt;em&gt;did&lt;/em&gt; buy is a shield that works less. Overrides roughly halved. Four commands in five that fly the ship are now the model&#39;s own. The fused pilot leans further toward the trainee while physics still holds the veto.&lt;/p&gt;

&lt;h2&gt;About Jev, since everyone is asking&lt;/h2&gt;

&lt;p&gt;TypeSafe AI put &lt;a href=&quot;https://typesafe.ai/blog/introducing-system-one-models-and-jev&quot;&gt;Jev&lt;/a&gt; into limited early access on 15 September, and the reaction has been loud. It is not a language model: you hand it a block of state and typed questions, and it returns a probability distribution per question plus a confidence score. No text, nothing to parse.&lt;/p&gt;

&lt;p&gt;Look at what my lander actually asks its model. A sentence of telemetry, two choice questions &amp;mdash; tilt {left, hold, right} and engine {off, half, full} &amp;mdash; and a probability per option. That &lt;em&gt;is&lt;/em&gt; Jev&#39;s contract. Laya sits behind a single call, &lt;code&gt;agent.predict(prompt, QUESTIONS)&lt;/code&gt;, and nothing below it knows what produced the answer. Swapping in a decision model would be an adapter, not a redesign.&lt;/p&gt;

&lt;p&gt;I have not measured Jev, and its speed, price and &amp;ldquo;cannot hallucinate&amp;rdquo; claims are the vendor&#39;s, not mine. But my results already answer some of what people are excited about, because those parts belong to the architecture rather than the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A faster, better proposer is not a pilot.&lt;/strong&gt; I just moved a proposer from 80% to 90% and got one extra solo landing in ninety. Cheaper proposals do not change that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Well-typed is not feasible.&lt;/strong&gt; Every command Laya emits is already well-formed &amp;mdash; &lt;code&gt;left&lt;/code&gt;/&lt;code&gt;hold&lt;/code&gt;/&lt;code&gt;right&lt;/code&gt; is always in range &amp;mdash; and 12&amp;ndash;17% of them were still overridden because simulating them forward cost more than the plan. A type system rules out unparseable answers, not physically bad ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The usual objection has a runtime answer.&lt;/strong&gt; People worry that a model returning bare numbers hides its reasoning. My shield never asks the model to explain itself; it rolls the proposal forward under learned physics and compares. Verification beats explanation when you can afford to simulate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calibration is the part I actually want&lt;/strong&gt;, and it is not free. My shield margin is a cost delta on a scale where a plan costs 0 to about 33, with a 1000-unit crash penalty on top. Confidence is a probability. Nothing converts one into the other, and a well-calibrated probability does not supply the conversion. The real step is calibrating the event the shield decides: the chance that a proposal&#39;s best rollout exceeds the plan by more than a given gap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things I would check before assuming a fit. My prompt is almost entirely numbers, and weakness on numbers is a reported limitation of this model class, so interface fit and content fit are separate questions. And a hosted API gives up the offline, on-device property that this project currently has, with roughly 14 ms per decision on a Mac and no network at all.&lt;/p&gt;

&lt;h2&gt;The usual caveats&lt;/h2&gt;

&lt;p&gt;The beam search is incomplete and the cost weights are hand-picked. The evaluation is ten held-out seeds per pad. One honest wrinkle: the original cost weights were tuned on seeds 0&amp;ndash;9, but the new switching weight was swept on seeds 3000&amp;ndash;3005, which sit inside the evaluation set. Every weight in that sweep landed every flight, so the choice turned on jerk and fuel rather than landings, and the landing counts are unchanged from before the penalty existed &amp;mdash; but it is tuned on that set, and the results page says so rather than leaving it implicit. None of this is flight software.&lt;/p&gt;

&lt;p&gt;Next up is still DAgger, and now calibration has a concrete definition to aim at. The browser lab, the measured results, the training recipe and the full write-up are at &lt;a href=&quot;https://github.com/mraad/lunar-mpc-laya&quot;&gt;github.com/mraad/lunar-mpc-laya&lt;/a&gt;. Go fly it and tell me where it breaks.&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/8089864158300877921/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/8089864158300877921' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/8089864158300877921'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/8089864158300877921'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/the-trainee-got-better-it-still-cannot.html' title='The Trainee Got Better. It Still Cannot Fly Alone.'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-3912667290088102041</id><published>2026-09-21T16:41:20.166-04:00</published><updated>2026-09-21T16:41:20.166-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="MLX"/><category scheme="http://www.blogger.com/atom/ns#" term="MPC"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>The Engineer and the Trainee: Adaptive MPC Guiding and Shielding Laya</title><content type='html'>&lt;p&gt;Two of my recent lander experiments kept nagging at me. In &lt;a href=&quot;https://thunderheadxpler.blogspot.com/2026/09/one-lander-three-controllers.html&quot;&gt;One Lander, Three Controllers&lt;/a&gt;, an adaptive MPC learned its engine during the flight and landed through a power loss with no training data at all. In &lt;a href=&quot;https://thunderheadxpler.blogspot.com/2026/09/lunar-laya-from-state-and-text-to.html&quot;&gt;Lunar Laya&lt;/a&gt;, a small decision model on Apple MLX turned a text description of the flight into structured choices, but the text carried hints from a fixed feedback law that assumed a healthy engine. Cut the engine power and that model crashed every time, faithfully following bad advice. So I asked the obvious question: what happens if the engineer writes the hints for the trainee? The result is &lt;a href=&quot;https://github.com/mraad/lunar-mpc-laya&quot;&gt;Lunar MPC + Laya&lt;/a&gt;.&lt;/p&gt;

&lt;p style=&quot;text-align:center&quot;&gt;&lt;a href=&quot;https://github.com/mraad/lunar-mpc-laya&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/mraad/lunar-mpc-laya/main/docs/assets/distilled-shield.gif&quot; alt=&quot;Distilled Laya proposing commands from telemetry while adaptive MPC vetoes unsafe ones, landing through a 60% engine loss&quot; width=&quot;1000&quot; style=&quot;max-width:100%;height:auto&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align:center&quot;&gt;&lt;em&gt;An actual recorded flight. Laya proposes every command from telemetry alone; adaptive MPC vetoes about one in three. The engine loses 60% of its thrust at ten seconds, the estimate drops to 40%, and the lander touches down at seventy.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;The engineer and the trainee&lt;/h2&gt;

&lt;p&gt;The game is the same arcade-style lander from Lunar Laya: three pads on bumpy terrain, a tilt control, an engine with off, half and full, and one decision every fifth of a second. Land upright, slowly, inside a pad, before the fuel runs out. The twist is an unannounced fault: partway through the flight the main engine loses 30% or 60% of its power, and the pilot is never told.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MPC is the engineer.&lt;/strong&gt; It has the physics written down. Before every move it simulates thousands of command sequences three seconds ahead, picks the cheapest, executes only the first command, and replans. It also compares what it predicted with what actually happened and updates its estimate of engine strength by recursive least squares. Nobody trained it; I wrote its cost function by hand, and I ported it from the Box2D world of the earlier post to the arcade physics here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laya is the trainee.&lt;/strong&gt; It reads a sentence like &amp;ldquo;Altitude 200 m, vertical velocity &amp;minus;8 m/s, tilt 3 degrees&amp;rdquo; and answers two multiple-choice questions: which way to tilt, how much engine. It learned from examples. It has no idea what gravity is.&lt;/p&gt;

&lt;h2&gt;Three ways they work together&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The engineer whispers to the trainee.&lt;/strong&gt; Laya&#39;s prompt already ended with a requested rotation and power. I swapped the source of that request from the fixed feedback law to MPC&#39;s first planned command, and changed nothing else: same weights, same prompt template, byte for byte. With the old requests, Laya landed 17 of 30 flights through a 30% power loss and 0 of 30 through a 60% loss. With MPC&#39;s requests, 30 of 30 in both. Same brain, better advice. The interesting detail is that MPC&#39;s adaptation now reaches the language model through the words in the prompt. Laya never sees the fault; it sees requests that already account for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The engineer checks the trainee&#39;s work.&lt;/strong&gt; The original Lunar Laya overrode the model every time it disagreed with guidance. That is safe but leaves the model no room. Here the shield is predictive: MPC forces Laya&#39;s proposal as the first command of a second beam search and compares the best achievable cost with its own plan. Only a proposal that is predicted to be worse gets replaced. Laya keeps its independence; physics keeps a veto.&lt;/p&gt;

&lt;p&gt;And here is the honest part. With the original checkpoint the shield never fired. Not once in 28,026 decisions. That model was fine-tuned to follow its prompt&#39;s request, and it follows it perfectly, so its flights are identical to MPC&#39;s own. Worse, when I appended a single sentence to the prompt with the engine estimate, request following collapsed to zero and every flight was lost. The checkpoint is welded to its training template. Useful to know before anyone puts a language model in a control loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The trainee learns from the engineer, then flies with a safety net.&lt;/strong&gt; So I trained a second checkpoint. Same base model, same recipe as the first post, three RTX PRO 6000 GPUs, about seven minutes. This time the teacher was adaptive MPC, the rollouts included both fault levels, and the requested labels were removed from the prompt entirely. Laya has to decide from telemetry.&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Pilot&lt;/th&gt;&lt;th&gt;Nominal&lt;/th&gt;&lt;th&gt;Thrust &amp;times;0.7&lt;/th&gt;&lt;th&gt;Thrust &amp;times;0.4&lt;/th&gt;&lt;th&gt;Laya&#39;s choice kept&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Distilled Laya, alone&lt;/td&gt;&lt;td&gt;0/30&lt;/td&gt;&lt;td&gt;0/30&lt;/td&gt;&lt;td&gt;0/30&lt;/td&gt;&lt;td&gt;&amp;mdash;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Distilled Laya + MPC shield&lt;/td&gt;&lt;td&gt;30/30&lt;/td&gt;&lt;td&gt;30/30&lt;/td&gt;&lt;td&gt;30/30&lt;/td&gt;&lt;td&gt;about two thirds&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Alone, it cannot land a single flight. Validation accuracy was 80% per question, which sounds fine until you remember that one wrong command moves the ship slightly off the teacher&#39;s trajectory, the next state is less familiar, and agreement with MPC falls to 10&amp;ndash;18%. That is the textbook behaviour-cloning failure, reproduced on my desk. With the shield on, the same weights land 90 of 90, and roughly two thirds of the commands that actually fly the ship are Laya&#39;s own. That is the first configuration where the fused pilot is neither controller alone.&lt;/p&gt;

&lt;h2&gt;Why bother, if the engineer alone can land?&lt;/h2&gt;

&lt;p&gt;Because the engineer can only do what its rules say. Its preferences are numbers in a cost function. It cannot be taught from examples, cannot take an instruction in plain language, and cannot tell you how confident it is. The trainee can do all three. What I want is a pilot with the trainee&#39;s flexibility and the engineer&#39;s guarantee that physics gets the last word. That pilot now exists and lands. It just needs the engineer to step in less often.&lt;/p&gt;

&lt;h2&gt;Try it in the browser&lt;/h2&gt;

&lt;p&gt;I ported the physics and the MPC to VanillaJS so you can fly it: click anywhere in the sky to place the lander, pick a pad, add a fault, and launch. A Node check verifies that the JavaScript reproduces every command of six Python-recorded flights. With the local Python server running, the same page flies both Laya checkpoints live on MLX and replays every recorded decision with the model&#39;s probabilities, MPC&#39;s request, Laya&#39;s proposal, and whether the shield fired.&lt;/p&gt;

&lt;p&gt;The usual caveats apply. The beam search is incomplete, the cost weights were hand-tuned on ten development seeds, the evaluation is ten held-out seeds per pad, and some starts are unrecoverable for every pilot, including MPC. The shield can only be as good as MPC&#39;s own plan.&lt;/p&gt;

&lt;p&gt;Next is the obvious one: roll out the shielded pilot, relabel every visited state with MPC, and retrain, so the trainee learns from the neighbourhood of its own mistakes. If that raises the raw landing count above zero and cuts the override rate, the loop is working. Source, docs, measured results and the browser lab are all at &lt;a href=&quot;https://github.com/mraad/lunar-mpc-laya&quot;&gt;github.com/mraad/lunar-mpc-laya&lt;/a&gt;. More to come.&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/3912667290088102041/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/3912667290088102041' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3912667290088102041'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3912667290088102041'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/the-engineer-and-trainee-adaptive-mpc.html' title='The Engineer and the Trainee: Adaptive MPC Guiding and Shielding Laya'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-2665077756717008666</id><published>2026-09-21T10:54:53.632-04:00</published><updated>2026-09-21T10:54:53.632-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="MLX"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Lunar Laya: From State and Text to Decisions on a Mac</title><content type='html'>&lt;p&gt;I keep coming back to Lunar Lander. It gives me a very visible way to ask a simple question: what does a model actually decide? Turn too much, burn too late, or miss the pad, and the result is right there on the screen.&lt;/p&gt;

&lt;p&gt;After exploring learning and control approaches to this problem, I wanted to try another angle: give a small model the current state and a text instruction, then ask it for a decision that the application can execute and measure. That experiment is now public as &lt;a href=&quot;https://github.com/mraad/lunar-laya&quot;&gt;Lunar Laya&lt;/a&gt;, an Atari-inspired lander using Laya with local inference on Apple MLX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I believe the input should be a state and a text, even a structured text. The output should always be structured and very deterministic, so it can be controlled and measured.&lt;/strong&gt; This implementation is a representation of that idea. The fact that the model can be small enough to run locally in an MLX environment is proof that it can be done for this kind of bounded decision task.&lt;/p&gt;

&lt;p style=&quot;text-align:center&quot;&gt;&lt;a href=&quot;https://github.com/mraad/lunar-laya&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/mraad/lunar-laya/main/docs/assets/trained-laya.gif&quot; alt=&quot;Trained Laya model controlling a lunar lander through a successful local MLX landing&quot; width=&quot;1000&quot; style=&quot;max-width:100%;height:auto&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align:center&quot;&gt;&lt;em&gt;An actual trained-model flight using FP16 MLX inference, shown at 4&amp;times; simulation speed. The input includes explicit guidance requests; no post-inference overrides were applied. The smaller Q6 version was evaluated separately.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;A small decision with a clear contract&lt;/h2&gt;

&lt;p&gt;The simulation exposes position, velocity, tilt, fuel and the landing target. A feedback controller calculates the requested correction. Those values and requests are serialized into text, and Laya answers two choice questions: rotate left, hold or right; and set the engine to off, half or full. Here, Laya is the decision model, not the LayaAir game engine.&lt;/p&gt;

&lt;p&gt;The model scores the allowed choices. The runtime selects the highest-scoring option and returns a structured answer with probabilities. For example, the choices inside an answer can look like this, with probabilities omitted for readability:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  &quot;rotation&quot;: {&quot;choice&quot;: &quot;hold&quot;},
  &quot;engine&quot;: {&quot;choice&quot;: &quot;half&quot;}
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The application maps those choices to a fixed command: zero rotation and 50% thrust. I can record the proposed command, what was executed, the next state and how long the decision took. That gives me something to inspect when a landing fails, and something precise to compare after changing the model.&lt;/p&gt;

&lt;p&gt;That is what I mean by a deterministic interface: a fixed set of outputs, an explicit selection rule and a predictable mapping into application behavior. Neural scores can still change with precision or hardware. A well-defined output contract makes those changes testable; it does not make every prediction correct.&lt;/p&gt;

&lt;h2&gt;Training it, then making it smaller&lt;/h2&gt;

&lt;p&gt;I added supervised training on three CUDA GPUs, keeping the original checkpoint available. Local inference uses MLX on the Mac; Python runs the simulator and guidance. A VanillaJS application lets us replay the recorded flights, inspect probabilities and compare controllers. It displays the experiments rather than running the model in the browser.&lt;/p&gt;

&lt;p&gt;Now the best part: the compact 6-bit version passed all 1,928 held-out validation examples and landed all 30 evaluated flights across three pads. Its 11,147 decisions agreed with the guidance requests, and its recorded state transitions matched the trained FP16 evaluation exactly.&lt;/p&gt;

&lt;p&gt;On an Apple M4 Max, peak active MLX inference memory dropped from about &lt;strong&gt;872 MiB to 370 MiB&lt;/strong&gt;, a 57.6% reduction. The weight file went from 614 MiB to 249 MiB. Those are model-runtime measurements: total process peak memory was about 709 MiB for Q6, including the surrounding software.&lt;/p&gt;

&lt;p&gt;The compact runtime loads packed MLX weights, removes an unused classifier and evaluates the two control questions sequentially. It also disables retained allocator caching to favor memory use. There is a tradeoff: median decision time increased from about 10.1 ms to 13.5 ms in this comparison.&lt;/p&gt;

&lt;p&gt;I tried smaller precisions too. Four-bit missed 215 validation answers; five-bit missed two. Six-bit was the smallest configuration tested that passed every validation answer. A smaller file is only useful if the behavior survives the change! The &lt;a href=&quot;https://github.com/mraad/lunar-laya/blob/main/docs/mlx-optimization.md&quot;&gt;optimization notes&lt;/a&gt; include the measurements and reproduction commands.&lt;/p&gt;

&lt;h2&gt;What this experiment demonstrates&lt;/h2&gt;

&lt;p&gt;The requested rotation and engine labels are already in the input. The trained model is learning to follow those requests. The feedback controller supplies the navigation knowledge, so the successful landings demonstrate guidance following, not independent discovery of a flight policy. The repository also includes a baseline that executes guidance directly, which is sufficient for this simulator.&lt;/p&gt;

&lt;p&gt;For me, the interesting result is the working connection between state, text and a bounded action. I want that same discipline when a model selects a GIS operation or chooses the next step in a data workflow: define what it can return, validate the result, and measure what happens. The lander makes that idea easy to watch.&lt;/p&gt;

&lt;p&gt;The next experiment I would like to try is varying the instruction while keeping the output contract fixed, then measuring where the behavior holds and where it breaks. For now, the &lt;a href=&quot;https://github.com/mraad/lunar-laya&quot;&gt;source code&lt;/a&gt;, &lt;a href=&quot;https://github.com/mraad/lunar-laya/blob/main/docs/training.md&quot;&gt;training details&lt;/a&gt; and &lt;a href=&quot;https://github.com/mraad/lunar-laya/blob/main/docs/media.md&quot;&gt;animation instructions&lt;/a&gt; are available. Take a look, try it on your Mac, and let me know where you would use a state-plus-text decision interface. More to come. :-)&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/2665077756717008666/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/2665077756717008666' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/2665077756717008666'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/2665077756717008666'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/lunar-laya-from-state-and-text-to.html' title='Lunar Laya: From State and Text to Decisions on a Mac'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-4250535266415240014</id><published>2026-09-21T10:19:15.079-04:00</published><updated>2026-09-21T10:19:15.079-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="GenAI"/><category scheme="http://www.blogger.com/atom/ns#" term="GIS"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Nano FEL: GPT-2 and nanoGPT on the North Sea</title><content type='html'>&lt;p&gt;After &lt;a href=&quot;https://thunderheadxpler.blogspot.com/2026/09/feln-lora-teaching-small-model-to-query.html&quot;&gt;LoRA on a 4B model&lt;/a&gt; and &lt;a href=&quot;https://thunderheadxpler.blogspot.com/2026/09/feln-liquid-fine-tuning-gis-model-on-mac.html&quot;&gt;a 1.2B model on the Mac&lt;/a&gt;, I kept asking myself a simpler question. How small can the model be, and how plain can the training loop be, before this North Sea task stops working? So I went back to the basics: GPT-2 medium, 355 million parameters, fully fine-tuned with Andrej Karpathy&#39;s &lt;a href=&quot;https://github.com/karpathy/nanoGPT&quot;&gt;nanoGPT&lt;/a&gt;. No adapters, no chat template, no grammar, no catalog in the prompt. Just a question in, a JSON query out. That is &lt;a href=&quot;https://github.com/mraad/nanofel&quot;&gt;nanofel&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The task is the same FELN translation: a question about wells, pipelines, and discoveries becomes a plan with &lt;code&gt;layers&lt;/code&gt;, &lt;code&gt;where&lt;/code&gt;, and &lt;code&gt;relations&lt;/code&gt;. The twist here is the data format. nanoGPT trains on one flat stream of tokens and samples random windows from it, so each training pair is simply:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Q: Which gas wells in Norway are within 2 kilometers of an oil pipeline?
A: {&quot;layers&quot;:[&quot;Wells&quot;,&quot;Pipelines&quot;],
    &quot;where&quot;:[&quot;content_type = cast(2 as SMALLINT) and (country = &#39;NO&#39;)&quot;,&quot;PipelinesType = cast(4 as SMALLINT)&quot;],
    &quot;relations&quot;:[&quot;withinDistance 2 kilometers&quot;]}&amp;lt;|endoftext|&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pairs are written one after another, and the training script from nanoGPT runs unchanged. The whole project is three vendored files from nanoGPT, a 100-line data preparation script, a normalizer that rewrites generated SQL into the gold surface style, and a 100-line inference script that decodes greedily and stops at the end-of-text token. At inference time the model has no idea a catalog exists; everything it knows about &lt;code&gt;content_type = 2&lt;/code&gt; meaning gas was absorbed during training.&lt;/p&gt;

&lt;p&gt;The gold data is 3,000 records from the regenerated NorthSea project, each with a casual phrasing and a canonical phrasing of the same query. I hold out 300 records, which gives 600 evaluation questions, and keep both phrasings of a record on the same side of the split. On top of the gold, about 12,900 generated pairs from the &lt;code&gt;feln-dsl&lt;/code&gt; grammar sampler go into training only, after the normalizer puts them in gold style. Without that normalization step the model would learn two competing ways to write the same clause and lose exact match on gold.&lt;/p&gt;

&lt;p&gt;Training is short. Block size 256, 32 sequences per iteration, learning rate 3e-5 with cosine decay, dropout 0.1, and 3,000 iterations while keeping the checkpoint with the best validation loss. On two RTX PRO 6000 GPUs that is about five minutes including compile and checkpoint writes. The stripped checkpoint is 1.4 GB and runs on my Mac through MPS.&lt;/p&gt;

&lt;p&gt;Here is how the runs went, all scored by exact JSON match on the held-out questions:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Run&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Training pairs&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Evaluation split&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Exact match&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;v1, gold only&lt;/th&gt;&lt;td&gt;1,800&lt;/td&gt;&lt;td&gt;old gold, 200 questions&lt;/td&gt;&lt;td&gt;158/200 (79.0%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;v1, gpt2-large&lt;/th&gt;&lt;td&gt;1,800&lt;/td&gt;&lt;td&gt;old gold, 200 questions&lt;/td&gt;&lt;td&gt;158/200 (79.0%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;v2, gold + generated&lt;/th&gt;&lt;td&gt;17,372&lt;/td&gt;&lt;td&gt;old gold, 200 questions&lt;/td&gt;&lt;td&gt;194/200 (97.0%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;v3, new gold only&lt;/th&gt;&lt;td&gt;5,400&lt;/td&gt;&lt;td&gt;new gold, 600 questions&lt;/td&gt;&lt;td&gt;529/600 (88.2%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;v4, new gold + generated (shipped)&lt;/th&gt;&lt;td&gt;18,297&lt;/td&gt;&lt;td&gt;new gold, 600 questions&lt;/td&gt;&lt;td&gt;534/600 (89.0%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;v5, same as v4, twice the steps on three GPUs&lt;/th&gt;&lt;td&gt;18,297&lt;/td&gt;&lt;td&gt;new gold, 600 questions&lt;/td&gt;&lt;td&gt;532/600 (88.7%)&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Two things jumped out at me. First, going from gpt2-medium to gpt2-large on the same 1,800 pairs changed nothing, while adding generated coverage for the sparse patterns took the same small model from 79% to 97%. This task is data-limited, not capacity-limited, and a 355M model has plenty of room for it. Second, the drop from 97% to 89% is not the model getting worse. The gold set was regenerated with three times more records, and the new catalog hints dropped the layer noun from the subtype descriptions. So “List gas/condensate. The returned wells must be within 15 kilometers of oil.” is now gold for wells near oil &lt;em&gt;pipelines&lt;/em&gt;, with nothing in the sentence saying pipelines. 40 of the 66 misses in v4 are that exact situation: the secondary layer is never named, and the model picks discoveries instead of pipelines or the other way around. When every layer is named in the question, v4 scores 95%.&lt;/p&gt;

&lt;p&gt;The v5 run was my attempt to squeeze more out of the same data, since the v4 validation loss looked like it was still falling at the last step. Three GPUs, 1.5 times the batch, twice the iterations. The validation loss bottomed out at step 1,800, drifted up while the training loss kept falling, and the best checkpoint landed two questions below v4. Same data, same result. The “still falling” signal was smaller than the evaluation noise, and I should have looked at the eval jitter before spending the compute. The remaining misses are the catalog&#39;s ambiguity, and the fix is in the data generation, not in the training loop.&lt;/p&gt;

&lt;p&gt;A few caveats. The 89% and the 99.7% from the LoRA post are on different gold sets with different SQL conventions, so there is no head-to-head here. Exact match is strict, and a style-normalized comparison gives the same numbers, so none of the misses are formatting. But the model has no grammar and no output validation. It will happily copy an unfamiliar word into a column name, so a downstream allowlist against the catalog is still needed before anything runs.&lt;/p&gt;

&lt;p&gt;What I like about this experiment is how little there is to it. A few hundred lines you can read in one sitting, a five-minute training run, and a model that answers on a laptop. The &lt;a href=&quot;https://github.com/mraad/nanofel&quot;&gt;code, training notes, and the full miss breakdown&lt;/a&gt; are on GitHub. Next is regenerating the gold with the layer nouns restored in the hints, and masking the question tokens so the loss is spent on the answer only. More to come!&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/4250535266415240014/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/4250535266415240014' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/4250535266415240014'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/4250535266415240014'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/nano-fel-gpt-2-and-nanogpt-on-north-sea.html' title='Nano FEL: GPT-2 and nanoGPT on the North Sea'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-6263359062102713831</id><published>2026-09-17T14:13:00.632-04:00</published><updated>2026-09-17T14:13:00.632-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="GenAI"/><category scheme="http://www.blogger.com/atom/ns#" term="GIS"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>FELN Qwen: Train on RTX, Run on Jetson Orin</title><content type='html'>&lt;p&gt;After &lt;a href=&quot;https://thunderheadxpler.blogspot.com/2026/09/feln-lora-teaching-small-model-to-query.html&quot;&gt;teaching a small model to query the North Sea&lt;/a&gt;, I wanted to take the experiment onto a Jetson. Could I train on a powerful workstation, then move the model, application, and spatial database onto an NVIDIA Jetson Orin? And could it produce the expected FELN JSON accurately enough to be useful?&lt;/p&gt;

&lt;p&gt;That is the experiment behind &lt;a href=&quot;https://github.com/mraad/feln-qwen&quot;&gt;feln-qwen&lt;/a&gt;. The job is specific: translate a question about wells, pipelines, and discoveries into FELN. The JSON identifies the layers, their attribute filters, and the spatial relationships between them. Layer order matters. Finding pipelines near discoveries is a different request from finding discoveries near pipelines!&lt;/p&gt;

&lt;p&gt;A huge thank you to &lt;strong&gt;&lt;a href=&quot;https://www.geodata.no/&quot;&gt;Geodata&lt;/a&gt;&lt;/strong&gt; for the beautiful &lt;strong&gt;NorthSea data&lt;/strong&gt;. It gave this experiment a rich geospatial vocabulary and meaningful relationships to work with. I deeply appreciate the work and care behind this dataset.&lt;/p&gt;

&lt;p&gt;I chose &lt;strong&gt;Qwen3.5-9B&lt;/strong&gt; and first verified stock-model inference on the Orin. The target machine had to support the model before committing to a full training run. Training then used two RTX PRO 6000 Blackwell GPUs, while inference and spatial execution stayed entirely on the Orin. The Mac was the browser and SSH connection; the workstation was not an inference dependency.&lt;/p&gt;

&lt;p&gt;The data builder reads &lt;code&gt;FELN.json&lt;/code&gt;, &lt;code&gt;Layers.json&lt;/code&gt;, and the Open Knowledge Format files. It validates the catalog, removes duplicates, and retains provenance. The final split contains &lt;strong&gt;4,790 training, 673 validation, and 534 test examples&lt;/strong&gt;. Related query families stay together, including literal substitutions and reordered secondary layers, so a close variant cannot slip across the split boundary.&lt;/p&gt;

&lt;p&gt;I used BF16 LoRA with rank-32 adapters, keeping the base weights frozen and applying loss to the expected answer tokens. There was enough measured GPU memory to use this recipe without quantizing the training base. A smoke run preceded the two-epoch run, which took about &lt;strong&gt;58 minutes&lt;/strong&gt; of measured wall time. Validation selected the checkpoint. The &lt;a href=&quot;https://github.com/mraad/feln-qwen/blob/main/docs/TRAINING_AND_DEPLOYMENT.md&quot;&gt;training and deployment walkthrough&lt;/a&gt; records the settings and commands.&lt;/p&gt;

&lt;p&gt;For deployment, the adapter was merged into the base model and exported to a text-only &lt;strong&gt;GGUF Q4_K_M&lt;/strong&gt; file. The resulting file is about &lt;strong&gt;5.63 GB&lt;/strong&gt;, served by llama.cpp with a 4,096-token context and one request at a time. Quantization is a separate deployment decision, so I evaluated the actual exported file as well as the training checkpoint.&lt;/p&gt;

&lt;p&gt;Now the part I was most interested in: the untuned Q4 model on Orin matched &lt;strong&gt;96 of 534&lt;/strong&gt; expected FELNs, or &lt;strong&gt;17.98%&lt;/strong&gt;. The tuned Q4 model matched &lt;strong&gt;530 of 534&lt;/strong&gt;, or &lt;strong&gt;99.25%&lt;/strong&gt;, on the same held-out set and application prompt/grammar path. These results come from the exported GGUF tested on the Orin.&lt;/p&gt;

&lt;p&gt;Here is the matched RTX–Orin comparison using the same tuned Q4 file and all 534 questions. These are measured model-endpoint timings, with requests sent sequentially. RTX inference used &lt;strong&gt;one GPU&lt;/strong&gt;; training used both. Runtime build revisions differ and are recorded with the results.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Measurement&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;RTX&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Orin&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Strict FELN matches&lt;/th&gt;&lt;td&gt;530/534 (99.25%)&lt;/td&gt;&lt;td&gt;530/534 (99.25%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Warm median latency&lt;/th&gt;&lt;td&gt;0.402 seconds&lt;/td&gt;&lt;td&gt;3.221 seconds&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Warm p95 latency&lt;/th&gt;&lt;td&gt;0.644 seconds&lt;/td&gt;&lt;td&gt;5.168 seconds&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Requests per second&lt;/th&gt;&lt;td&gt;2.426&lt;/td&gt;&lt;td&gt;0.303&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;RTX was about eight times faster in this workload, but the Orin retained the same accuracy. The strict comparison uses &lt;code&gt;FELN.same&lt;/code&gt; after schema validation: equivalent SQL formatting is acceptable; a wrong field, enum value, or spatial relationship is not. The &lt;a href=&quot;https://github.com/mraad/feln-qwen/blob/main/README.md#rtx-versus-orin&quot;&gt;comparison and underlying reports&lt;/a&gt; are in the repository.&lt;/p&gt;

&lt;p&gt;The application adds schema compilation and read-only DuckDB spatial execution. A separate 18-question sample covering the nine primary-layer/layer-count strata passed 18 of 18 end-to-end checks, with a warm median of &lt;strong&gt;3.97 seconds&lt;/strong&gt;. Sampled peak system-wide shared RAM usage was about &lt;strong&gt;10.1 GiB&lt;/strong&gt;. That is an application/database measurement and a system-memory reading, distinct from the model-only table above.&lt;/p&gt;

&lt;p&gt;I also deployed FELN Studio on the Orin. Here it is generating the expected JSON for pipelines with a dimension over 20.0 that are within two miles of a condensate discovery. The screenshot was captured from the real application, with an exact-match verdict.&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/mraad/feln-qwen/5e0836a2c826169ce4a1890fb2111b810e27097c/docs/images/feln-studio-orin.png&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/mraad/feln-qwen/5e0836a2c826169ce4a1890fb2111b810e27097c/docs/images/feln-studio-orin.png&quot; alt=&quot;FELN Studio using Qwen3.5-9B on Orin to generate a pipelines-to-discoveries spatial query, with an exact-match verdict&quot; width=&quot;720&quot; style=&quot;max-width:100%;height:auto;&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;
  &lt;figcaption&gt;FELN Studio on Orin, viewed through the Mac browser. The legacy “on this Mac” helper text is inaccurate for this deployment: model computation runs on Orin.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;There are limits. These are held-out synthetic and source query families from one static catalog, not a production user study. Four test answers are still wrong, and complete Orin responses exceed one second. Studio provides generation and inspection; spatial GeoJSON comes from the separate application API. The services were restart-tested and subsequently stopped at my request, so the screenshot documents the verified deployment rather than a live public demo.&lt;/p&gt;

&lt;p&gt;The code is now public under Apache 2.0 in &lt;a href=&quot;https://github.com/mraad/feln-qwen&quot;&gt;feln-qwen&lt;/a&gt;, with data preparation, training, evaluation, serving, and measured results. Third-party models and data retain their own terms. I would like to test broader catalogs and more natural questions next. Fork the repository, explore the workflow, and let me know where you would take this in your GIS work. More to come!&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/6263359062102713831/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/6263359062102713831' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/6263359062102713831'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/6263359062102713831'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/feln-qwen-train-on-rtx-run-on-jetson.html' title='FELN Qwen: Train on RTX, Run on Jetson Orin'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-4630572326466380269</id><published>2026-09-16T16:23:29.775-04:00</published><updated>2026-09-16T16:23:29.775-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="GenAI"/><category scheme="http://www.blogger.com/atom/ns#" term="GIS"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="MLX"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>FELN Liquid: Fine-Tuning a GIS Model on a Mac</title><content type='html'>&lt;p&gt;I have been experimenting with small language models that translate everyday questions into structured GIS queries. With &lt;a href=&quot;https://github.com/mraad/feln-liquid&quot;&gt;FELN Liquid&lt;/a&gt;, I wanted to explore the training side on the Mac itself. Could I take a compact model, teach it the conventions of a North Sea catalog, and complete the experiment locally?&lt;/p&gt;

&lt;p&gt;The first complete run used LiquidAI&#39;s &lt;a href=&quot;https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct&quot;&gt;LFM2.5-1.2B-Instruct&lt;/a&gt; with LoRA through MLX on an Apple M4 Max. Training took 48 minutes and 37 seconds. Including checkpoint evaluation and the final test, the workflow finished in just under an hour. That is a practical turnaround for trying an idea and inspecting what it learned.&lt;/p&gt;

&lt;p&gt;The task is familiar from the other FELN projects: turn a request into a JSON plan containing &lt;code&gt;layers&lt;/code&gt;, &lt;code&gt;where&lt;/code&gt;, and &lt;code&gt;relations&lt;/code&gt;. A compact &lt;code&gt;Layers.json&lt;/code&gt; catalog accompanies each request, supplying the names, types, aliases, codes, and hints that make the data meaningful.&lt;/p&gt;

&lt;p&gt;For example, “Show all wells with water depth &amp;gt; 350 meters” should produce:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  &quot;layers&quot;: [&quot;Wells&quot;],
  &quot;where&quot;: [&quot;\&quot;water_depth\&quot; &amp;gt; 350&quot;],
  &quot;relations&quot;: []
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The measurement belongs to the &lt;code&gt;water_depth&lt;/code&gt; field, and its unit is already understood from the catalog. The SQL predicate needs the numeric threshold. There is no spatial join in this example, so the relations array is empty.&lt;/p&gt;

&lt;p&gt;The completed experiment used 1,000 synthetic examples, split into 796 for training, 100 for validation, and 104 for testing. Related query families stayed together across those splits. I also froze the source files, prompt, split identifiers, and hashes for the run. As the catalog and generated questions evolve, those copies let us identify exactly what produced a result.&lt;/p&gt;

&lt;p&gt;LoRA updated about 11.1 million parameters, roughly 0.95% of the base model, using rank-16 adapters. MLX runs this work on the Mac&#39;s Metal GPU. Peak reported MLX memory was about 30.7 GB, so the memory requirement matters even with a relatively small model. The selected adapter is about 44.5 MB and still needs the base weights to run.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/mraad/feln-liquid/blob/main/LOCAL_TUNING.md&quot;&gt;recorded results&lt;/a&gt; selected epoch two:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;&lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Measurement&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Selected adapter&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Canonical plan matches, validation&lt;/th&gt;&lt;td&gt;98/100&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Canonical plan matches, test&lt;/th&gt;&lt;td&gt;99/104 (95.2%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Strict FELN schema validity, test&lt;/th&gt;&lt;td&gt;104/104&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Identifier and SQL compilation checks, test&lt;/th&gt;&lt;td&gt;104/104&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Observed median generation time, test&lt;/th&gt;&lt;td&gt;0.784 seconds&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Epoch three matched 97 of the 100 validation plans, so the final checkpoint was not the selected one. The test split was evaluated after selection. Canonical comparison normalizes the SQL representation before comparing plans; the stricter raw-output score was 96/104 on the test set. The timing is an observation from this Mac run, not a controlled, repeated inference benchmark.&lt;/p&gt;

&lt;p&gt;Then a short human question exposed a useful problem: “Show all wells with depth &amp;gt; 350 meters.” The original adapter invented a &lt;code&gt;depth&lt;/code&gt; column and left &lt;code&gt;meters&lt;/code&gt; in the SQL expression. The available field describes water depth. A person&#39;s word “depth” could also mean drilled-well depth, which this catalog does not provide.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/mraad/feln-liquid/blob/main/predict_feln.py&quot;&gt;guarded inference command&lt;/a&gt; now asks for clarification on that wording. For an explicit request, it supplies additional instructions and one example, validates the complete response against the catalog and permitted SQL expressions, and allows one retry with feedback. Its result can be &lt;code&gt;clarification_required&lt;/code&gt;, &lt;code&gt;validated&lt;/code&gt;, or &lt;code&gt;rejected&lt;/code&gt;. It does not execute the query.&lt;/p&gt;

&lt;p&gt;That distinction matters. All five errors in the original tuned test set passed the schema and SQL checks. A wrong stored code or an extra condition can still be valid SQL. Likewise, the six targeted checks that passed after adding the guard are development checks, not a new accuracy benchmark. The original scores describe synthetic query families, and execution-result accuracy has not been measured.&lt;/p&gt;

&lt;p&gt;I am excited by how much of the training and inspection cycle can happen locally. The next step is to add more human phrasing while reserving independently written questions for evaluation. A matched RTX training comparison is still future work for this project.&lt;/p&gt;

&lt;p&gt;The scripts, &lt;a href=&quot;https://github.com/mraad/feln-liquid/blob/main/LOCAL_TUNING.md&quot;&gt;run notes&lt;/a&gt;, and &lt;a href=&quot;https://github.com/mraad/feln-liquid/blob/main/DATA_SYNC.md&quot;&gt;data preparation details&lt;/a&gt; are available in &lt;a href=&quot;https://github.com/mraad/feln-liquid&quot;&gt;feln-liquid&lt;/a&gt;. The setup currently uses workspace-specific paths, and the model weights and run artifacts are not included in a clone, so start with the README before reproducing it. Feedback on the questions your own GIS users actually ask would be especially useful. More to come!&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/4630572326466380269/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/4630572326466380269' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/4630572326466380269'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/4630572326466380269'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/feln-liquid-fine-tuning-gis-model-on-mac.html' title='FELN Liquid: Fine-Tuning a GIS Model on a Mac'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-8485272760864896242</id><published>2026-09-16T16:00:28.000-04:00</published><updated>2026-09-16T16:23:18.047-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="DuckDB"/><category scheme="http://www.blogger.com/atom/ns#" term="GenAI"/><category scheme="http://www.blogger.com/atom/ns#" term="GIS"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="WebGPU"/><title type='text'>FELN WebGPU: A Language Model, a Database, and a Map in One Browser Tab</title><content type='html'>&lt;p&gt;I have been exploring how small language models can turn a plain-English question into a useful GIS query. With FELN LoRA, that meant training a model on a GPU and running it locally on my Mac. For &lt;a href=&quot;https://github.com/mraad/feln-webgpu&quot;&gt;feln-webgpu&lt;/a&gt;, I wanted to bring the language model, the database, and the map into the web application itself.&lt;/p&gt;

&lt;p&gt;Type “Give me all oil discoveries,” and the browser generates a query plan, compiles it to SQL, executes it, and draws the results. The example in the repository finds 562 discoveries, with 200 displayed. We can inspect the plan and SQL beside the map. I like being able to see what the model actually asked the database!&lt;/p&gt;

&lt;figure&gt;
  &lt;a href=&quot;https://raw.githubusercontent.com/mraad/feln-webgpu/main/docs/screenshot.png&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/mraad/feln-webgpu/main/docs/screenshot.png&quot; alt=&quot;North Sea Query showing oil discoveries on a map, the generated FELN plan and DuckDB SQL, and a table reporting 562 matches with 200 displayed.&quot; style=&quot;max-width:100%;height:auto;&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;
  &lt;figcaption&gt;The question, query plan, SQL, and resulting features in one browser tab.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The pieces are a fine-tuned &lt;a href=&quot;https://huggingface.co/LiquidAI/LFM2-350M&quot;&gt;LiquidAI LFM2-350M&lt;/a&gt; model, Transformers.js running on WebGPU, DuckDB-Wasm with its spatial extension, and the ArcGIS Maps SDK for JavaScript. Once the assets have loaded, query generation and database execution happen on the user&#39;s machine. There is no inference server or database server behind the interaction.&lt;/p&gt;

&lt;p&gt;There is an initial download: 443 MB for the exported model weights and 5.9 MB of Parquet data for wells, discoveries, and pipelines. The application also fetches libraries, the spatial extension, and basemap tiles from external services. Local query processing is useful, but this version still has network dependencies.&lt;/p&gt;

&lt;p&gt;For the oil-discoveries example, the model produces:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  &quot;layers&quot;: [&quot;Discoveries&quot;],
  &quot;where&quot;: [&quot;discovery_type = cast(3 as INTEGER)&quot;],
  &quot;relations&quot;: []
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A JavaScript port of the FELN compiler turns that plan into DuckDB SQL. DuckDB reads the geometry from the Parquet tables, and the application sends the returned features to an ArcGIS graphics layer. The model&#39;s responsibility is translating the request into FELN. The database performs the selection.&lt;/p&gt;

&lt;p&gt;Getting a 350-million-parameter model to do that required attention to the examples. An early version saw “water depth” during training, but a question using just “depth” could make it invent a column. Adding paraphrases addressed that gap. The current preparation also includes more natural wording and combined constraints, while keeping a query plan and all its wordings on the same side of the training/validation split.&lt;/p&gt;

&lt;p&gt;This project uses full fine-tuning. The latest 350M run took about 31 minutes on one RTX PRO 6000. Its system prompt includes the OKF catalog documentation, roughly 10,900 tokens describing the data. That is a substantial amount of context for a short question.&lt;/p&gt;

&lt;p&gt;The useful trick is to process that common prompt once. At startup, the application spends about seven seconds building a prefix cache, then reuses the model state for subsequent questions. In the recorded comparison, this brought generation from about seven seconds per question to 0.7–1.3 seconds. A separate 210-question browser holdout run averaged 0.46 seconds for generation. Those measurements exclude downloading the model and drawing the result.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/mraad/feln-webgpu#export-to-the-browser&quot;&gt;export experiments&lt;/a&gt; also helped choose the download size. On the earlier 81-question browser evaluation, FP16 and the 8-bit export both matched 74 expected plans. The 8-bit version reduced the weights from 692 MB to 443 MB. The smaller 4-bit exports lost accuracy, so the application stayed with 8-bit weights.&lt;/p&gt;

&lt;p&gt;For the current v4 model, the recorded browser results are:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;&lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Measurement&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;Result&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Exact FELN plans&lt;/th&gt;&lt;td&gt;203/210 (96.7%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Matching result sets&lt;/th&gt;&lt;td&gt;208/210 (99.0%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Mean generation time&lt;/th&gt;&lt;td&gt;0.46 seconds&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Matching rows and matching plans answer different questions. Two different filters can happen to select the same records in this dataset. I want to keep both measurements visible. A larger 1.2B model reached 204/210 exact plans in the Python evaluation, one more than the 350M model, and was not exported into the application.&lt;/p&gt;

&lt;p&gt;There is a spatial issue to fix before relying on distance queries: the current compiler passes metre-valued thresholds to &lt;code&gt;ST_DWithin&lt;/code&gt; while the geometry is stored in WGS84 degrees. Those units do not match. The browser reproduces the Python implementation, so agreement between them does not establish correct distances. The correction belongs in the shared compiler and then in the browser port.&lt;/p&gt;

&lt;p&gt;What interests me here is how much of this workflow fits in a browser tab: language, structured queries, a spatial database, and a map. The dataset is small, the model still makes mistakes, and the initial load matters. Even with those limits, this is an exciting direction for focused GIS applications.&lt;/p&gt;

&lt;p&gt;The code and evaluation tables are available on GitHub in &lt;a href=&quot;https://github.com/mraad/feln-webgpu&quot;&gt;feln-webgpu&lt;/a&gt;. I would like to keep pushing on natural wording and spatial correctness as this develops. More to come!&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/8485272760864896242/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/8485272760864896242' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/8485272760864896242'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/8485272760864896242'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/feln-webgpu-language-model-database-and.html' title='FELN WebGPU: A Language Model, a Database, and a Map in One Browser Tab'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-3669112394555579646</id><published>2026-09-16T16:00:18.000-04:00</published><updated>2026-09-16T16:23:07.775-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="GenAI"/><category scheme="http://www.blogger.com/atom/ns#" term="GIS"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>FELN LoRA: Teaching a Small Model to Query the North Sea</title><content type='html'>&lt;p&gt;In the &lt;a href=&quot;https://thunderheadxpler.blogspot.com/2026/09/feln-rag-five-examples-and-spatial-query.html&quot;&gt;FELN RAG experiment&lt;/a&gt;, I used a few relevant examples to help a language model translate a question into a spatial query. That got me interested in taking the next step: teaching a small model to do this particular job, then running it locally.&lt;/p&gt;

&lt;p&gt;I believe small language models have a big role to play in GIS. We have plenty of tasks with a well-defined vocabulary, a known catalog, and a very specific output. Turning a North Sea question into a query over wells, pipelines, and discoveries is one of them. That is the experiment behind &lt;a href=&quot;https://github.com/mraad/feln-lora&quot;&gt;feln-lora&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The model is NVIDIA&#39;s Nemotron-3-Nano-4B, fine-tuned using LoRA and QLoRA through &lt;a href=&quot;https://github.com/NVIDIA-NeMo/Automodel&quot;&gt;NeMo AutoModel&lt;/a&gt;. Training happens on an NVIDIA GPU. The resulting model runs on my Mac through llama.cpp and Metal, with the question staying on the machine.&lt;/p&gt;

&lt;p&gt;Now the best part: the latest local models produce a query in about 1.2 seconds at the warm median. That is a useful place to start for an interactive GIS workflow :-)&lt;/p&gt;

&lt;p&gt;Consider this question from the &lt;a href=&quot;https://github.com/mraad/feln-lora/blob/main/tests/challenge.json&quot;&gt;challenge set&lt;/a&gt;: “Find gas wells no more than 5 km from oil pipelines.” The expected FELN is:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  &quot;layers&quot;: [&quot;Wells&quot;, &quot;Pipelines&quot;],
  &quot;where&quot;: [&quot;\&quot;content_type\&quot; = 2&quot;, &quot;\&quot;pipelinestype\&quot; = 4&quot;],
  &quot;relations&quot;: [&quot;withinDistance 5 kilometers&quot;]
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There is quite a bit of GIS knowledge hiding in that little object. Gas wells use &lt;code&gt;content_type = 2&lt;/code&gt;, while oil pipelines use &lt;code&gt;pipelinestype = 4&lt;/code&gt;. The first layer is what we want returned. The second supplies the spatial filter. Reverse the layers and we are asking for pipelines instead of wells.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/mraad/layers-json&quot;&gt;layers-json&lt;/a&gt; supplies the catalog, and &lt;a href=&quot;https://github.com/mraad/feln&quot;&gt;feln&lt;/a&gt; supplies the query model and strict comparison. The inference bundle carries the prompt, catalog, and JSON-schema grammar alongside the exported model. The grammar constrains the output structure; getting the right fields, codes, and spatial relationship remains the model&#39;s job. Query execution is a separate step, with a read-only DuckDB path in the repository.&lt;/p&gt;

&lt;p&gt;LoRA trains small adapters while keeping the base model frozen. QLoRA adds a quantized base during training, using 4-bit NF4 here. Both recipes use rank-16 adapters. After training, I merge the adapter into the full-precision base and export a Q8_0 GGUF for local inference. The 4-bit training representation and the 8-bit deployment file are two separate choices.&lt;/p&gt;

&lt;p&gt;For the September 16 retraining, the data preparation produced 3,463 training examples, 410 validation examples, and 419 test examples. A target query and its paraphrases stay in the same split. Otherwise, a reworded training question could make the validation score look more impressive than it deserves. Validation selects the checkpoint; the test and challenge sets are reserved for regression checks.&lt;/p&gt;

&lt;p&gt;Here are the &lt;a href=&quot;https://github.com/mraad/feln-lora/blob/main/TRAINING.md&quot;&gt;v2 results&lt;/a&gt;, with each training run using one RTX PRO 6000:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th scope=&quot;col&quot;&gt;Measurement&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;LoRA&lt;/th&gt;&lt;th scope=&quot;col&quot;&gt;QLoRA&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Merged model, validation&lt;/th&gt;&lt;td&gt;409/410 (99.76%)&lt;/td&gt;&lt;td&gt;409/410 (99.76%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Q8_0 GGUF, Mac validation&lt;/th&gt;&lt;td&gt;408/410 (99.51%)&lt;/td&gt;&lt;td&gt;408/410 (99.51%)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Q8_0 GGUF, Mac challenge set&lt;/th&gt;&lt;td&gt;37/40&lt;/td&gt;&lt;td&gt;36/40&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Warm median query generation, Mac&lt;/th&gt;&lt;td&gt;1.21 seconds&lt;/td&gt;&lt;td&gt;1.16 seconds&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Training wall time&lt;/th&gt;&lt;td&gt;28 minutes 22 seconds&lt;/td&gt;&lt;td&gt;31 minutes 6 seconds&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;th scope=&quot;row&quot;&gt;Peak GPU memory during training&lt;/th&gt;&lt;td&gt;15.6 GiB&lt;/td&gt;&lt;td&gt;18.4 GiB&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The score uses &lt;code&gt;feln.FELN.same&lt;/code&gt;, which normalizes SQL formatting while preserving distinctions such as operators, spatial direction, and an empty string versus &lt;code&gt;NULL&lt;/code&gt;. A query returning some rows is not enough to make it correct. And these timings measure query generation, with database execution still to follow.&lt;/p&gt;

&lt;p&gt;QLoRA gave me the same validation accuracy, but used more memory and took longer in this setup. That is worth measuring on the hardware and model we actually intend to use. The harder, hand-written challenge set also gives a more modest picture than the validation percentage. Prefix matching, category distinctions, and a query combining two spatial relationships still exposed mistakes. I have not measured accuracy against a real-user query log yet.&lt;/p&gt;

&lt;p&gt;There is also the question of maintenance. When the catalog aliases, hints, and question phrasing changed, the original models scored 832/862 on the new geometry-layer questions before retraining. The preparation pipeline excludes standalone tables without geometry, including 138 questions from the regenerated gold set. Supporting those tables is still an open extension.&lt;/p&gt;

&lt;p&gt;RAG makes it convenient to update examples and catalog context. Fine-tuning adds a training and validation cycle, but gives me a local model specialized for this workflow. The recorded RAG and fine-tuning scores use different evaluation sets, so they do not establish a head-to-head winner.&lt;/p&gt;

&lt;p&gt;What excites me is having this translation step close to the data, on the machine in front of me. The code and &lt;a href=&quot;https://github.com/mraad/feln-lora/blob/main/TRAINING.md&quot;&gt;training notes&lt;/a&gt; are available on GitHub in &lt;a href=&quot;https://github.com/mraad/feln-lora&quot;&gt;feln-lora&lt;/a&gt;. One experiment I would like to try next is adding retrieved examples to the fine-tuned model&#39;s prompt. There is room to combine the two approaches. More to come!&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/3669112394555579646/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/3669112394555579646' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3669112394555579646'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3669112394555579646'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/feln-lora-teaching-small-model-to-query.html' title='FELN LoRA: Teaching a Small Model to Query the North Sea'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-5316279532570799272</id><published>2026-09-15T19:19:57.641-04:00</published><updated>2026-09-15T19:19:57.641-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="GenAI"/><category scheme="http://www.blogger.com/atom/ns#" term="GIS"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="RAG"/><title type='text'>FELN RAG: Five Examples and a Spatial Query</title><content type='html'>&lt;p&gt;Ask a GIS person to find oil wells within five kilometers of gas
pipelines and they can start breaking the request into pieces. Which
layer contains the wells? How is “oil” encoded? Which pipelines carry
gas? What spatial operation connects the two?&lt;/p&gt;
&lt;p&gt;A language model needs those same details. “Oil” might be a numeric
subtype. A country might be stored as a two-letter code. And choosing
&lt;code&gt;intersects&lt;/code&gt; when the question calls for
&lt;code&gt;contains&lt;/code&gt; can produce perfectly respectable JSON that asks
the wrong question.&lt;/p&gt;
&lt;p&gt;That is the problem behind &lt;a
href=&quot;https://github.com/mraad/feln-rag&quot;&gt;feln-rag&lt;/a&gt;: give the model a
description of the data and a handful of relevant worked examples, then
ask it to translate a new request into a structured spatial query.&lt;/p&gt;
&lt;p&gt;The interesting part is how little machinery the retrieval needs.&lt;/p&gt;
&lt;h2 id=&quot;start-with-the-query-we-want-to-produce&quot;&gt;Start with the query we
want to produce&lt;/h2&gt;
&lt;p&gt;The output is FELN, a JSON representation with three main pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;layers&lt;/code&gt;: the primary layer to return, followed by any
spatial filter layers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;where&lt;/code&gt;: one SQL filter per layer, in the same
order.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;relations&lt;/code&gt;: the spatial relationship between the primary
layer and each secondary layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is a small example from the &lt;a
href=&quot;https://github.com/mraad/feln-rag/blob/main/feln_rag/data/NorthSea/FELN.json&quot;&gt;bundled
NorthSea corpus&lt;/a&gt;. A request for discoveries in the United Kingdom
whose discovery wellbore name starts with &lt;code&gt;42/30-&lt;/code&gt;
becomes:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &amp;quot;layers&amp;quot;: [&amp;quot;Discoveries&amp;quot;],
  &amp;quot;where&amp;quot;: [&amp;quot;\&amp;quot;country\&amp;quot; = &amp;#39;UK&amp;#39; AND \&amp;quot;discovery_wellbore_name\&amp;quot; LIKE &amp;#39;42/30-%&amp;#39;&amp;quot;],
  &amp;quot;relations&amp;quot;: []
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One layer, two attribute conditions, and no spatial join. Add another
layer and a distance constraint, and the relationship becomes something
like &lt;code&gt;withinDistance 5 kilometers&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The repository generates this representation. Executing it against
geographic data is a separate step. You can explore the translation
without having the NorthSea geodatabase on your machine.&lt;/p&gt;
&lt;p&gt;Two companion projects supply the foundation. &lt;a
href=&quot;https://github.com/mraad/layers-json&quot;&gt;layers-json&lt;/a&gt; describes
the catalog, including field names, coded values, and hints. &lt;a
href=&quot;https://github.com/mraad/feln&quot;&gt;feln&lt;/a&gt; supplies the output
models, validation, and comparison tools.&lt;/p&gt;
&lt;h2 id=&quot;the-vector-store-is-a-numpy-array&quot;&gt;The vector store is a NumPy
array&lt;/h2&gt;
&lt;p&gt;The bundled corpus contains 1,000 question-and-answer pairs. Each
question has a known FELN answer attached to it.&lt;/p&gt;
&lt;p&gt;An embedding model converts the question text into a vector. Those
vectors become rows in a NumPy array, with each row still associated
with its original question and FELN. The new question goes through the
same encoder, and retrieval finds the five closest examples.&lt;/p&gt;
&lt;p&gt;Because the vectors are normalized to unit length, cosine similarity
reduces to this line in &lt;a
href=&quot;https://github.com/mraad/feln-rag/blob/main/feln_rag/index.py&quot;&gt;the
index implementation&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;scores = self.embeddings @ query.reshape(-1)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this corpus size, scanning the array is a straightforward place to
start. There is no separate vector database service to operate. The
embeddings are cached on disk, with the encoder name and ordered
question texts contributing to the cache fingerprint.&lt;/p&gt;
&lt;p&gt;A similarity score tells us which questions resemble the new request.
Whether their answers demonstrate the right spatial logic is something
we still have to inspect and measure.&lt;/p&gt;
&lt;h2 id=&quot;put-the-examples-where-the-model-can-use-them&quot;&gt;Put the examples
where the model can use them&lt;/h2&gt;
&lt;p&gt;The retrieved examples become a short conversation. Each question is
a user message, and its known FELN answer is the following assistant
message. The actual question comes last:&lt;/p&gt;
&lt;pre class=&quot;text&quot;&gt;&lt;code&gt;System:    Translation instructions and layer catalog
User:      Retrieved question 1
Assistant: Known FELN answer 1
           ...four more question/answer pairs...
User:      The new question&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the few-shot prompt. The catalog explains the data; the
examples demonstrate how to translate requests involving it. The &lt;a
href=&quot;https://github.com/mraad/feln-rag/blob/main/feln_rag/rag.py&quot;&gt;generation
code&lt;/a&gt; sends those messages through LiteLLM and attempts to parse the
response as FELN.&lt;/p&gt;
&lt;p&gt;The model has to produce a new answer. Copying the closest example
would be a very efficient way to return somebody else&#39;s query :-)&lt;/p&gt;
&lt;h2 id=&quot;make-the-context-visible&quot;&gt;Make the context visible&lt;/h2&gt;
&lt;p&gt;FELN Studio puts the pieces on one page: the question, five retrieved
examples, an editable system prompt and catalog, and the generated
JSON.&lt;/p&gt;
&lt;p&gt;The useful first click is &lt;strong&gt;Find examples only&lt;/strong&gt;. Expand
a card and inspect the FELN that will become an assistant message. With
the default local encoder, this step does not call the generation model.
&lt;strong&gt;Generate FELN&lt;/strong&gt; then uses those displayed examples in
their displayed order.&lt;/p&gt;
&lt;p&gt;&lt;img
src=&quot;https://raw.githubusercontent.com/mraad/feln-rag/main/docs/screenshots/feln-studio-workspace.jpg&quot;
alt=&quot;FELN Studio showing the NorthSea query workspace&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The repository screenshot demonstrates the workspace and
retrieval; it does not show a completed generation request.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The interface is plain HTML, CSS, and JavaScript served by Python&#39;s
standard-library HTTP server. It runs on loopback. Generation sends the
question, catalog, and examples to the configured model provider, while
credentials stay on the server. The &lt;a
href=&quot;https://github.com/mraad/feln-rag#feln-studio&quot;&gt;README&lt;/a&gt; explains
the workflow and configuration.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;VALID FELN&lt;/strong&gt; label has a precise meaning: the
output passed schema validation. You still need to read the query. Valid
structure cannot tell you whether the model chose the relationship you
intended.&lt;/p&gt;
&lt;h2 id=&quot;count-the-misses-too&quot;&gt;Count the misses, too&lt;/h2&gt;
&lt;p&gt;The evaluation code separates held-out questions from the retrieval
corpus and supports a zero-shot baseline using the same catalog
instructions without examples. It records validity, exact match,
structural and partial scores, and request failures. Studio uses the
full corpus for exploration, so evaluation belongs in that separate
holdout workflow. See &lt;a
href=&quot;https://github.com/mraad/feln-rag/blob/main/feln_rag/eval.py&quot;&gt;eval.py&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The repository reports a September 15 smoke test using MPNet
retrieval and &lt;code&gt;azure/gpt-5.5&lt;/code&gt;. Retrieval was evaluated on ten
held-out questions; generation used the first three:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Reported result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matching layer set among the five retrieved examples&lt;/td&gt;
&lt;td&gt;10/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG exact match&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-shot exact match&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation requests&lt;/td&gt;
&lt;td&gt;Six valid outputs, no request errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The zero-shot miss selected &lt;code&gt;intersects&lt;/code&gt; where the
reference used &lt;code&gt;contains&lt;/code&gt;. All six outputs were valid, which
is a useful illustration of why validation and correctness need separate
columns. These are &lt;a
href=&quot;https://github.com/mraad/feln-rag#northsea-smoke-results&quot;&gt;reported
smoke results&lt;/a&gt;, and three questions are far too few to establish
general accuracy.&lt;/p&gt;
&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;
&lt;p&gt;The current setup requires Python 3.13, &lt;code&gt;uv&lt;/code&gt;, and sibling
checkouts of &lt;code&gt;feln&lt;/code&gt;, &lt;code&gt;layers-json&lt;/code&gt;, and
&lt;code&gt;VectorlessGAIT&lt;/code&gt;. That last dependency is currently required
by the package configuration even when using NumPy retrieval. Follow the
&lt;a href=&quot;https://github.com/mraad/feln-rag#setup&quot;&gt;setup
instructions&lt;/a&gt;, configure &lt;code&gt;LLM_MODEL_NAME&lt;/code&gt; and the provider
credentials, then run from the repository:&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;uv sync
uv run --no-sync python -m feln_rag.web&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:8765/&lt;/code&gt;. The default embedding model
may download its weights on first use.&lt;/p&gt;
&lt;p&gt;Start with a question whose answer you understand. Inspect the
retrieved examples. Generate the FELN. Then change the distance, reverse
the requested relationship, or add an attribute condition and see what
moves.&lt;/p&gt;
&lt;p&gt;For me, the appeal is being able to follow the translation all the
way through: the catalog, the examples, the prompt, and the resulting
query. When something goes wrong, there are concrete pieces to
examine.&lt;/p&gt;
&lt;p&gt;The next useful experiment would be a larger held-out comparison,
especially on questions that differ by one spatial relationship or one
coded value. Those small differences are where a convincing answer has
to become a correct query.&lt;/p&gt;
&lt;p&gt;More to come :-)&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/5316279532570799272/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/5316279532570799272' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/5316279532570799272'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/5316279532570799272'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/feln-rag-five-examples-and-spatial-query.html' title='FELN RAG: Five Examples and a Spatial Query'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-3648070290143247639</id><published>2026-09-15T12:27:54.402-04:00</published><updated>2026-09-15T12:27:54.403-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI"/><category scheme="http://www.blogger.com/atom/ns#" term="Technology"/><title type='text'>FELN: Find Existing Location with N Layers</title><content type='html'>&lt;p&gt;
  In the &lt;a href=&quot;https://github.com/mraad/layers-json&quot;&gt;layers-json&lt;/a&gt; post, I argued
  that the model needs to know what our data means before it can write a useful query.
  A field called &lt;code&gt;STATUS&lt;/code&gt; with a value of &lt;code&gt;3&lt;/code&gt; is not self-explanatory,
  and the aliases, domains, and subtypes already sitting in an ArcGIS Pro project are the
  cheapest explanation we will ever get. That post ended with a catalog, a
  &lt;code&gt;Layers.json&lt;/code&gt; file, and a promise to do something with it.
&lt;/p&gt;

&lt;p&gt;
  This is the something. &lt;a href=&quot;https://github.com/mraad/feln&quot;&gt;FELN&lt;/a&gt; stands for
  &lt;em&gt;Find Existing Location with N layers&lt;/em&gt;, and it is a small Python library and CLI
  that turns a plain-language request into a structured spatial query, compiles that query
  to DuckDB SQL, and, the part I actually care about, measures how close one query is to
  another.
&lt;/p&gt;

&lt;h2&gt;The shape of a question&lt;/h2&gt;

&lt;p&gt;
  Most of the spatial questions people ask me over the years have the same skeleton. Find
  the things in one layer, optionally filtered, that stand in some spatial relationship to
  the things in another layer, also optionally filtered. Wells near pipelines. Parcels
  inside a flood zone. Counties that contain at least one suspended well. The layers change,
  the predicates change, the relationship changes, but the skeleton does not.
&lt;/p&gt;

&lt;p&gt;So a FELN is exactly that skeleton, written down as three parallel lists:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;“Find suspended wells within 5 kilometers of oil pipelines.”

{
  &quot;layers&quot;:    [&quot;Wells&quot;, &quot;Pipelines&quot;],
  &quot;where&quot;:     [&quot;STATUS = 2&quot;, &quot;MEDIUM = &#39;OIL&#39;&quot;],
  &quot;relations&quot;: [&quot;withinDistance 5 kilometers&quot;]
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  &lt;code&gt;layers[0]&lt;/code&gt; is what comes back. &lt;code&gt;where[i]&lt;/code&gt; filters
  &lt;code&gt;layers[i]&lt;/code&gt;, and an empty string means no filter. &lt;code&gt;relations[i]&lt;/code&gt;
  relates the primary layer to &lt;code&gt;layers[i+1]&lt;/code&gt;. One layer, no relations. Three
  layers, two relations, both anchored on the primary. The pipelines constrain the answer
  but are not returned, and the &lt;code&gt;2&lt;/code&gt; in &lt;code&gt;STATUS = 2&lt;/code&gt; comes straight
  from the catalog&#39;s coded-value domain, which is the whole reason the catalog exists.
&lt;/p&gt;

&lt;p&gt;
  One thing to say early, because it shapes everything that follows: the WHERE clauses
  are simple on purpose. &lt;code&gt;STATUS = 2&lt;/code&gt;, &lt;code&gt;DEPTH &amp;gt; 1000&lt;/code&gt;,
  &lt;code&gt;NAME LIKE &#39;Alpha%&#39;&lt;/code&gt;, a &lt;code&gt;BETWEEN&lt;/code&gt;, a few of those joined with
  &lt;code&gt;AND&lt;/code&gt;. That is the vocabulary, and it was never meant to be exhaustive. This
  is a starting point for the find-existing-location process, not a SQL grammar. The
  generator emits simple predicates so a small model has something tractable to learn,
  and the comparison scores them per predicate for the same reason. Nothing stops you
  from elaborating on it. The &lt;code&gt;where&lt;/code&gt; entries are strings, the compiler passes
  them through a guard rather than a parser of its own, and a richer generator or a
  hand-written query can put more in them. But start simple. The interesting question is
  whether the loop closes at all, and simple predicates are enough to find out.
&lt;/p&gt;

&lt;p&gt;
  There is nothing clever in that JSON, and that is deliberate. I wanted something a model
  can emit, a person can read, and a program can compare. The cleverness, such as it is,
  lives on either side of it.
&lt;/p&gt;

&lt;h2&gt;Why structure instead of SQL&lt;/h2&gt;

&lt;p&gt;
  My first instinct, back when I started this thread, was to have the model write the SQL
  directly. It works often enough to be seductive and fails in ways that are very hard to
  score. Two SQL strings that select the same rows can look nothing alike. Two that look
  almost identical can differ by one &lt;code&gt;&amp;lt;&lt;/code&gt; versus &lt;code&gt;&amp;lt;=&lt;/code&gt; and return
  different wells. If I cannot measure the output, I cannot improve the thing producing it.
&lt;/p&gt;

&lt;p&gt;
  The FELN structure makes the answer measurable in more than one way, and the library
  keeps those ways separate on purpose:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;strong&gt;Exact match.&lt;/strong&gt; &lt;code&gt;FELN.same()&lt;/code&gt; normalizes the WHERE clauses
    with sqlglot in DuckDB dialect, so &lt;code&gt;cast(2 as SMALLINT)&lt;/code&gt; equals
    &lt;code&gt;2&lt;/code&gt;, identifiers are case-insensitive, and conjuncts are sorted before
    comparison. Secondary layers match by name, not position.
  &lt;/li&gt;
  &lt;li&gt;
    &lt;strong&gt;Graded similarity.&lt;/strong&gt; &lt;code&gt;FELNCompare.partial&lt;/code&gt; gives credit per
    predicate. A dropped conjunct is worth about 0.67, an &lt;code&gt;OR&lt;/code&gt; written as
    &lt;code&gt;AND&lt;/code&gt; about 0.75, a swapped primary layer keeps half credit. Distances are
    compared in meters, so &lt;code&gt;5 miles&lt;/code&gt; and &lt;code&gt;8.05 kilometers&lt;/code&gt; can agree.
  &lt;/li&gt;
  &lt;li&gt;
    &lt;strong&gt;Execution.&lt;/strong&gt; When you have result sets, &lt;code&gt;FELNCompare.cost&lt;/code&gt;
    blends a 70% Jaccard on the returned OBJECTIDs with 30% of the partial score, and
    precision and recall fall out of the ID sets in the usual way.
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
  That last one is where the structure pays for itself twice. The same FELN can be compiled
  for DuckDB, or ArcPy, or Spark, each preserving its own units, coordinate systems, and
  boundary semantics. The generation side improves against a measurable target; the
  execution side is chosen for the data. Today only the DuckDB compiler exists. ArcPy and
  Spark are possible backends, not integrations I have written, and I would rather say that
  plainly than let a README imply otherwise.
&lt;/p&gt;

&lt;h2&gt;What the compiler emits&lt;/h2&gt;

&lt;p&gt;
  &lt;code&gt;FELNToDuckDB&lt;/code&gt; builds a chain of CTEs: one filtered &lt;code&gt;L0..Ln&lt;/code&gt; per
  layer, one join CTE per spatial relation, and a final select that keeps a primary row
  only if it appears in every join. &lt;code&gt;withinDistance&lt;/code&gt; becomes
  &lt;code&gt;ST_DWithin&lt;/code&gt;, which includes the boundary. &lt;code&gt;notWithinDistance&lt;/code&gt;
  becomes &lt;code&gt;NOT EXISTS&lt;/code&gt;, because “more than 500 meters from every pipeline”
  checks the absence of any nearby pipeline, and finding one distant pipeline is not
  sufficient. I got that wrong once with a plain join and a &lt;code&gt;&amp;gt;&lt;/code&gt;, and the
  result looked perfectly reasonable until I counted the rows.
&lt;/p&gt;

&lt;p&gt;
  The text side has the same boundary discipline. “No more than” and “at most” generate
  &lt;code&gt;&amp;lt;=&lt;/code&gt;. “Under” and “less than” generate strict &lt;code&gt;&amp;lt;&lt;/code&gt;. Distance
  wording only uses “within” and “no more than”, because the relation behind it is
  inclusive. An earlier version mapped “no more than” to &lt;code&gt;&amp;lt;&lt;/code&gt;; it is fixed,
  the fix does not rewrite old datasets, and seeded output changed. If you generated
  examples before, regenerate them.
&lt;/p&gt;

&lt;h2&gt;Generating the training pairs&lt;/h2&gt;

&lt;p&gt;
  The part that consumes the catalog is the generator. Point it at a
  &lt;code&gt;Layers.json&lt;/code&gt; and it samples layers, fields, coded values, subtypes, and
  spatial relations into synthetic text/FELN pairs, one JSON object per line:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;uv run feln generate &quot;$HOME/Documents/ArcGIS/Projects/NorthSea/Layers.json&quot; \
  -n 20 --seed 0 --normalize --sql --alias-suffix -o northsea-examples.jsonl&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  When a layer carries subtype labels, they supply the noun in the text, so a subtype
  &lt;code&gt;Oil wells&lt;/code&gt; yields “Show oil wells” with &lt;code&gt;kind = 1&lt;/code&gt; in the WHERE,
  while &lt;code&gt;meta.layers&lt;/code&gt; keeps the catalog layer name. &lt;code&gt;--alias-suffix&lt;/code&gt;
  appends the layer alias to disambiguate labels shared across layers.
  &lt;code&gt;--layer-only 0.25&lt;/code&gt; phrases a quarter of the subtyped layers by alias alone,
  so the subtype column has to compete as an ordinary condition. These are synthetic
  pairs from a template, not a natural-language parser, and I want to be clear about
  that: the generator produces what a model should learn to emit, it does not read what
  a person typed.
&lt;/p&gt;

&lt;p&gt;
  On the NorthSea project, five catalog layers including two tables, I ran 1,000 samples
  in each of five generation modes, then ran them again to check reproducibility. Every
  record validated against the model, referenced only layers and columns in the catalog,
  parsed as a single DuckDB statement, and compared to its own normalized form with a
  structural score of 1 and a cost of 0. Profiling put nearly all of the generation time
  in SQL normalization, so a bounded &lt;code&gt;lru_cache&lt;/code&gt; from the standard library
  took a cold run from 0.84 seconds to 0.29 for a thousand records. No new dependency.
&lt;/p&gt;

&lt;h2&gt;What it is not&lt;/h2&gt;

&lt;p&gt;
  FELN generates SQL; it does not execute it and it is not a sandbox. The WHERE guard
  rejects some unsafe syntax, but it does not stop subqueries or database functions that
  read files. Catalog metadata, WHERE expressions, geometry and CRS options are trusted
  inputs. If you are going to run model-generated queries, enforce the allowed tables and
  columns in your application and lock the database process down; DuckDB has
  &lt;a href=&quot;https://duckdb.org/docs/current/operations_manual/securing_duckdb/overview&quot;&gt;good guidance&lt;/a&gt;
  on that. Structural similarity and a successful parse are not security checks.
&lt;/p&gt;

&lt;p&gt;
  And no accuracy claim yet. The smoke tests cover generation, compilation, and
  comparison. DuckDB was not installed in the environment I tested from, so result IDs,
  CRS correctness, and runtime types were not validated against real features. Precision
  and recall need reference queries and execution results, and that benchmark is the next
  thing on the list, not something this post can report.
&lt;/p&gt;

&lt;p&gt;
  What interests me here is the loop. Catalog explains the data. Generator produces
  measurable pairs from that explanation. Compiler runs them. Compare scores the result.
  Close the loop with a small model and the numbers tell you whether supplying the meaning
  of a field actually helps, rather than whether it feels like it should. That is the
  experiment I set out to make possible, and it is now possible.
&lt;/p&gt;

&lt;p&gt;
  The source, the CLI reference, the worked examples, and the dated review notes are in
  the &lt;a href=&quot;https://github.com/mraad/feln&quot;&gt;feln repository&lt;/a&gt;. It depends on
  &lt;a href=&quot;https://github.com/mraad/layers-json&quot;&gt;layers-json&lt;/a&gt; for the catalog model,
  pydantic, sqlglot, and numpy, and it does not need GDAL. Try it on a project whose data
  you know, read the generated pairs, and tell me where the text and the SQL disagree.
  Those are the bugs I want.
&lt;/p&gt;

&lt;p&gt;More to come :-)&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/3648070290143247639/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/3648070290143247639' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3648070290143247639'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3648070290143247639'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/feln-find-existing-location-with-n.html' title='FELN: Find Existing Location with N Layers'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-7045433040179134990</id><published>2026-09-15T05:59:11.232-04:00</published><updated>2026-09-15T05:59:11.232-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ArcGIS"/><category scheme="http://www.blogger.com/atom/ns#" term="DuckDB"/><category scheme="http://www.blogger.com/atom/ns#" term="GenAI"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Layers JSON: Giving LLMs the GIS Context They Need</title><content type='html'>&lt;p&gt;When I &lt;a href=&quot;https://thunderheadxpler.blogspot.com/2024/01/back-in-action-genai-meets-geospatial.html&quot;&gt;started writing about GenAI and geospatial analysis&lt;/a&gt;, the attraction was being able to ask questions in plain language and let the tools do the spatial work. There is a very practical detail behind that interaction, though: the model needs to know what our data means.&lt;/p&gt;

&lt;p&gt;Take a field called &lt;code&gt;STATUS&lt;/code&gt; with a value of &lt;code&gt;3&lt;/code&gt;. Is that an active well? An abandoned one? A record waiting for review? A model can write perfectly valid SQL around that number and still give us the wrong answer. The database will happily execute it, too :-)&lt;/p&gt;

&lt;p&gt;That is the problem I want to address with &lt;a href=&quot;https://github.com/mraad/layers-json&quot;&gt;layers-json&lt;/a&gt;: give applications access to the knowledge already captured in an ArcGIS Pro project and its supporting data sources.&lt;/p&gt;

&lt;h2&gt;The map already carries a lot of the explanation&lt;/h2&gt;

&lt;p&gt;If you have spent time giving fields useful aliases, defining domains, and organizing layers in Pro, you have already done some of the hard work. A field alias turns a storage name into something a person recognizes. A coded-value domain explains what those numbers mean. Subtypes tell us which kind of feature a row represents. Metadata descriptions and sample values fill in more of the picture.&lt;/p&gt;

&lt;p&gt;I like the idea of putting that work to use beyond the map. Someone took the trouble to explain the data once; we should be able to carry that explanation into the next application.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;layers-json&lt;/code&gt; command reads the project&#39;s layer configuration together with its referenced File Geodatabase or PostgreSQL data and writes a &lt;code&gt;Layers.json&lt;/code&gt; catalog. It brings together aliases, field types, domains, subtypes, sample values, and query hints. A consuming application can supply the relevant parts to an LLM when translating a question into SQL.&lt;/p&gt;

&lt;p&gt;For our hypothetical well-status domain, that means the application can supply the actual mapping between “active” and its stored code. Now the model has something concrete to work with.&lt;/p&gt;

&lt;h2&gt;From an APRX to JSON, or something we can read&lt;/h2&gt;

&lt;p&gt;Once the package and its GDAL dependencies are installed, a catalog command looks like this. Replace the example path and map name with your own:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;layers-json /data/NorthSea/NorthSea.aprx --map Map -o ./catalog&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This writes &lt;code&gt;catalog/Layers.json&lt;/code&gt;. The command runs outside ArcGIS Pro, without ArcPy. It needs Python 3.11 or newer, system GDAL, and matching GDAL Python bindings. The &lt;a href=&quot;https://github.com/mraad/layers-json#install-from-source&quot;&gt;README has the installation steps&lt;/a&gt;. If you prefer working in Pro, the bundled &lt;code&gt;Layers.pyt&lt;/code&gt; toolbox provides a Prepare Metadata tool for the active map.&lt;/p&gt;

&lt;p&gt;There is also a format I can open and read directly:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;layers-okf /data/NorthSea/NorthSea.aprx --map Map -o ./knowledge&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This uses the same reader to produce an OKF knowledge bundle: an &lt;code&gt;index.md&lt;/code&gt; and Markdown concept files for the tables. The bundle includes schema tables, decoded domains, query hints, and source references, with structured metadata in the index. It gives us a readable way to inspect the context before handing it to an application.&lt;/p&gt;

&lt;p&gt;There are some useful boundaries here. Sampling defaults to at most 20,000 rows per layer and 20 sampled distinct values per field. Columns without sampled or domain values are pruned. I would inspect the result before treating it as the context for a query; it is a catalog built to help querying, and sampling can miss values.&lt;/p&gt;

&lt;h2&gt;And the actual rows?&lt;/h2&gt;

&lt;p&gt;The catalog describes the data. To work with the rows in a SQL database, the repository also includes export tools. From an ArcGIS Pro Python environment with the DuckDB extra installed, we can export a project&#39;s layers:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;layers-duckdb &quot;C:\Projects\NorthSea\NorthSea.aprx&quot; --map Map&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That exporter writes a sibling DuckDB database, preserves hidden attributes and GlobalIDs, honors definition queries, and converts geometry to EPSG:4326. Separate GDAL-based Bash loaders handle File Geodatabases going into DuckDB, PostGIS, or Oracle Spatial. The &lt;a href=&quot;https://github.com/mraad/layers-json/blob/main/scripts/README.md&quot;&gt;script reference&lt;/a&gt; explains their requirements and replacement behavior.&lt;/p&gt;

&lt;p&gt;What interests me most is what this could do for smaller language models. Supplying the meaning of a field leaves less for a model to infer. That is the motivation; this project does not yet establish an accuracy improvement through model benchmarks. The application still has to select the context, generate the query, and check the answer.&lt;/p&gt;

&lt;p&gt;Try it with a small project whose data you know well. Open the catalog, look at the aliases and coded values, and see whether it contains the explanation you would give a colleague asking about that dataset. That is a useful place to start.&lt;/p&gt;

&lt;p&gt;The source is on &lt;a href=&quot;https://github.com/mraad/layers-json&quot;&gt;GitHub&lt;/a&gt;. I am interested in which pieces of GIS context make the biggest difference for your queries. Feedback and questions are welcome. More to come!&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/7045433040179134990/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/7045433040179134990' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/7045433040179134990'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/7045433040179134990'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/layers-json-giving-llms-gis-context.html' title='Layers JSON: Giving LLMs the GIS Context They Need'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-3538722285488546855</id><published>2026-09-11T09:16:03.000-04:00</published><updated>2026-09-12T17:27:52.742-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Computer Vision"/><category scheme="http://www.blogger.com/atom/ns#" term="Jetson"/><category scheme="http://www.blogger.com/atom/ns#" term="OpenCV"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><title type='text'>Nano Face: A Mac Camera, a Jetson, and One USB Cable</title><content type='html'>&lt;p&gt;
  Let me get the obvious out of the way first. Face detection on a Jetson is not new.
  NVIDIA ships samples for it, half the tutorials on the internet do it, and I am fairly
  sure somebody has done it on a Raspberry Pi wearing a HAT. So no, this is not a post
  about discovering face detection.
&lt;/p&gt;

&lt;p&gt;
  This is a post about my two Jetsons, an Orin Nano Super and an AGX Orin, that live
  headless on a shelf with no monitor, no keyboard, and no camera plugged into either of
  them. I had just finished moving the Nano to JetPack 7.2.1 and pruning both boards down
  to a lean server install (no desktop, no Docker, nothing starting at boot), and I
  wanted a small, honest, end-to-end test that they were alive and doing real work. A
  face detector is a good smoke test. It either draws a box on your nose or it does not.
&lt;/p&gt;

&lt;p&gt;
  The twist, and the part I actually cared about, is where the camera lives. It is the
  one in my MacBook Pro. The Jetson never sees a camera device. Frames leave the Mac&#39;s
  browser, ride over the USB-C cable that was already plugged in, and land on the Jetson
  as plain HTTP. The Jetson runs the model and sends back coordinates. That is the whole
  trick, and it is the reason I bothered.
&lt;/p&gt;

&lt;h2&gt;USB as a network, not as a webcam&lt;/h2&gt;

&lt;p&gt;
  Plug a Jetson developer kit into a Mac over USB-C and it does not show up as a camera
  or a disk. It shows up as a network interface. NVIDIA&#39;s device-mode service brings up
  a small bridge on the board with a fixed private address, and macOS gets a matching
  &amp;ldquo;Linux for Tegra&amp;rdquo; interface on the same tiny subnet. So I can SSH to the
  board with no Wi-Fi, no router, and no idea which DHCP address it picked up today.
&lt;/p&gt;

&lt;p&gt;
  Once SSH works, the rest is old plumbing. A launcher on the Mac copies the app to the
  board, starts a tiny Python HTTP server there that listens only on the Jetson&#39;s own
  loopback, and opens an &lt;code&gt;ssh -L&lt;/code&gt; tunnel so that
  &lt;code&gt;http://127.0.0.1:8765&lt;/code&gt; on the Mac &lt;em&gt;is&lt;/em&gt; that server. The browser
  opens the page, and here is the part I like: because the page comes from localhost,
  the browser treats it as a secure context and happily hands over
  &lt;code&gt;getUserMedia&lt;/code&gt;. The camera stays a Mac camera. The HTML just happens to be
  served by a Jetson.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Mac camera
  -&amp;gt; browser (getUserMedia, canvas, JPEG at quality 0.8, fit within 640x480)
    -&amp;gt; POST /api/detect to http://127.0.0.1:8765
      -&amp;gt; ssh -L tunnel over the USB-C cable
        -&amp;gt; Jetson loopback server (OpenCV decode, longest side resized to 320)
          -&amp;gt; YuNet through cv2.FaceDetectorYN on two CPU threads
        &amp;lt;- JSON: boxes, five landmarks, confidence, timings
  &amp;lt;- browser draws the result on the exact frame it sent&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  Nothing is queued. The browser sends one frame, waits for the answer, draws the boxes
  on that same frame, and only then grabs the next one, capped at 20 fps. If the board
  gets slow, the picture gets slower; it does not fall behind. A second tab hitting a
  busy detector gets an HTTP 429 and a polite message. I have seen too many
  &amp;ldquo;live&amp;rdquo; previews with boxes floating three frames behind the face to build
  another one.
&lt;/p&gt;

&lt;h2&gt;The model, and why the CPU&lt;/h2&gt;

&lt;p&gt;
  The detector is
  &lt;a href=&quot;https://github.com/opencv/opencv_zoo/tree/47534e27c9851bb1128ccc0102f1145e27f23f98/models/face_detection_yunet&quot;&gt;YuNet&lt;/a&gt;,
  the 2023 ONNX model from the OpenCV Zoo. It is a 232 KB file that returns a rectangle,
  five landmarks (eyes, nose, mouth corners), and a score, and it runs through OpenCV&#39;s
  DNN module on the CPU. Yes, the CPU, on a board with a GPU and TensorRT sitting right
  there. That was deliberate. The Nano ships with NVIDIA&#39;s OpenCV 4.8.0 already
  installed, YuNet is tiny, and I wanted to see what the plain path gives me before
  touching CUDA. The server pins the model by SHA-256 and refuses to start on a
  mismatch, which sounds paranoid until the day a Git LFS pointer file shows up where
  the model should be.
&lt;/p&gt;

&lt;p&gt;
  On the Orin Nano Super, with the browser capturing at 640x480 and inference at
  320x240, I watched 19.7 completed frames per second, 13 to 19 ms of inference on the
  board, and 27 to 43 ms round trips measured from the browser. That live run was on
  JetPack 6.2; after the 7.2.1 upgrade the fixture tests landed in the same place, with a
  median of 16.1 ms inference. On the AGX Orin over the same USB tunnel, inference came
  in at 12.9 to 16 ms with 24 to 28 ms round trips, though that was a fixture run, not
  the live camera. These are readings off my desk, not a benchmark, and I would be lying
  if I said the AGX was breaking a sweat. For a 320-pixel face detector on the CPU, the
  GPU is simply not the bottleneck. JPEG encoding and the trip over the cable account
  for most of the rest.
&lt;/p&gt;

&lt;p&gt;
  &lt;img src=&quot;https://raw.githubusercontent.com/mraad/nano-face/main/NanoFace.png&quot;
       alt=&quot;Nano Face in Chrome: one detected face with a box, a 92% confidence label, and five landmarks; the sidebar shows 16.9 fps, 18 ms Nano inference, and a 121 ms round trip&quot;
       style=&quot;max-width:100%;height:auto;&quot; /&gt;
&lt;/p&gt;

&lt;p&gt;
  One face, five dots, and the numbers I actually care about in the sidebar. The
  kitchen is not part of the model.
&lt;/p&gt;

&lt;h2&gt;Two Jetsons, two surprises&lt;/h2&gt;

&lt;p&gt;
  The Nano was the easy one. The AGX Orin on JetPack 7 (L4T R39.2.1) had two surprises
  waiting. First, it ships with no Python OpenCV at all, and the apt package is 4.6, too
  old for &lt;code&gt;FaceDetectorYN&lt;/code&gt;. A pip &lt;code&gt;opencv-python-headless&lt;/code&gt; 4.11
  wheel in &lt;code&gt;~/.local&lt;/code&gt;, pinned with &lt;code&gt;numpy&amp;lt;2&lt;/code&gt; so it does not
  fight the system NumPy, took care of that.
&lt;/p&gt;

&lt;p&gt;
  The second one was less obvious. macOS binds the AGX&#39;s USB gadget through NCM, which
  shows up on the board as &lt;code&gt;usb1&lt;/code&gt;. NVIDIA&#39;s device-mode start script
  hardcodes &lt;code&gt;usb0&lt;/code&gt; in its ECM/NCM block, so &lt;code&gt;usb1&lt;/code&gt; never joins the
  bridge and the Mac&#39;s &amp;ldquo;Linux for Tegra&amp;rdquo; port sits there saying
  &lt;code&gt;inactive&lt;/code&gt;. One line in that script, reading the interface name from the
  gadget&#39;s configfs instead of assuming it, and the port came alive. I keep the original
  next to it, because a JetPack update will happily overwrite the fix.
&lt;/p&gt;

&lt;p&gt;
  And one rule I learned by doing it wrong: never plug both Jetsons into the Mac at the
  same time. They both answer at the same fixed USB address. SSH host key aliases keep
  their trust records apart, but they cannot make two boards share one address.
&lt;/p&gt;

&lt;h2&gt;What it is not&lt;/h2&gt;

&lt;p&gt;
  It is a detector, not a recognizer. There are no identities, no embeddings, no
  database, and nothing is written to disk on either machine; a frame lives in memory
  for the length of one request. The server binds only to loopback on the Jetson, the
  tunnel binds only to loopback on the Mac, and the server checks Host and Origin, caps
  bodies at 1 MB and decoded frames at 1280x720, and closes the connection after each
  request. It installs no service and touches nothing at boot. Close the launcher and
  the Jetson process exits with it, because the server watches its SSH stdin for EOF.
  That last one is my favorite line in the whole project.
&lt;/p&gt;

&lt;p&gt;
  What excites me here is not the box around my face. It is that a headless board on a
  shelf became a useful sidekick for my laptop over the one cable that was already
  plugged in, with no camera purchase, no Wi-Fi setup, and no cloud in the loop. The
  same shape, Mac captures, Jetson computes, browser shows, should work for anything
  that fits in a JPEG and comes back as JSON.
&lt;/p&gt;

&lt;p&gt;
  The code, the API table, the test suite, and the dated verification notes are in the
  &lt;a href=&quot;https://github.com/mraad/nano-face&quot;&gt;nano-face repository&lt;/a&gt;. Next on my
  list is to move inference to TensorRT just to see whether it matters at this size (I
  suspect not), and then to try something heavier than a face detector now that the
  plumbing is boring and the cable is proven.
&lt;/p&gt;

&lt;p&gt;More to come :-)&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/3538722285488546855/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/3538722285488546855' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3538722285488546855'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3538722285488546855'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/nano-face-mac-camera-jetson-and-one-usb.html' title='Nano Face: A Mac Camera, a Jetson, and One USB Cable'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-875081500410751900</id><published>2026-09-08T19:04:46.027-04:00</published><updated>2026-09-08T19:04:46.027-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ArcGIS Pro"/><category scheme="http://www.blogger.com/atom/ns#" term="ArcPy"/><category scheme="http://www.blogger.com/atom/ns#" term="Claude Code"/><category scheme="http://www.blogger.com/atom/ns#" term="Generative AI"/><category scheme="http://www.blogger.com/atom/ns#" term="GeoAI"/><category scheme="http://www.blogger.com/atom/ns#" term="MCP"/><title type='text'>ArcGIS Pro, Claude Code, and a Loopback Bridge</title><content type='html'>&lt;p&gt;
  Continuing the GenAI-with-a-GeoSpatial-twist thread I started a while back. Back
  then, I had a language model &lt;em&gt;reason&lt;/em&gt; about geospatial logic. This time, I
  wanted it to actually &lt;em&gt;do&lt;/em&gt; the work—on my open project, inside my running
  &lt;a href=&quot;https://www.esri.com/en-us/arcgis/products/arcgis-pro/overview&quot;&gt;ArcGIS Pro&lt;/a&gt;
  session, while I watch.
&lt;/p&gt;

&lt;p&gt;
  The result is &lt;a href=&quot;https://github.com/mraad/ProCowork&quot;&gt;ProCowork&lt;/a&gt;, an
  experimental native ArcGIS Pro add-in that embeds the
  &lt;a href=&quot;https://www.claude.com/product/claude-code&quot;&gt;Claude Code&lt;/a&gt; engine in a
  dockable chat panel. I can ask it to list layers, add and calculate fields, select
  features, run a buffer, or write a more specialized ArcPy script. Claude generates
  the code, the add-in runs it against the live project, and the code and results come
  back into the same panel.
&lt;/p&gt;

&lt;p&gt;For example, I can type:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;“List the layers in the current map.”&lt;/li&gt;
  &lt;li&gt;“Add a DOUBLE field POP_DEN to Parcels and set it to POP / AREASQMI.”&lt;/li&gt;
  &lt;li&gt;“Select parcels where POP_DEN &amp;gt; 5000 and zoom to them.”&lt;/li&gt;
  &lt;li&gt;“Buffer Roads by 100 meters and add the result to the map.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
  The panel is useful, but the interesting part is not the chat UI. It is getting an
  external AI coding agent safely and reliably close enough to a live ArcGIS Pro
  project to be useful.
&lt;/p&gt;

&lt;h2&gt;The constraint that shaped the design&lt;/h2&gt;

&lt;p&gt;
  &lt;code&gt;arcpy.mp.ArcGISProject(&quot;CURRENT&quot;)&lt;/code&gt; only resolves inside ArcGIS Pro&#39;s
  own Python execution context on the foreground geoprocessing thread. Claude Code,
  however, runs as a headless child process. If it starts Python outside Pro, that
  process can work with datasets on disk, but it cannot see the open project as
  &lt;code&gt;CURRENT&lt;/code&gt;.
&lt;/p&gt;

&lt;p&gt;
  My first design kept a long-running Python daemon inside Pro. It worked, and then it
  went stale in the ways long-running bridges tend to go stale. So I flipped the
  ownership around. A persistent C# bridge now lives inside ArcGIS Pro for the whole
  session. For each request it either uses the ArcGIS Pro .NET SDK directly or launches
  one fresh foreground ArcPy geoprocessing tool. The tool resolves
  &lt;code&gt;CURRENT&lt;/code&gt;, performs the operation, returns JSON, and goes away. There is no
  Python daemon left behind to outlive its host.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ProCowork panel (WPF, inside ArcGIS Pro)
    -&amp;gt; Claude Code (headless child process)
        -&amp;gt; MCP over HTTP on 127.0.0.1:&amp;lt;ephemeral-port&amp;gt;
            -&amp;gt; C# BridgeService (inside ArcGIS Pro)
                -&amp;gt; .NET SDK on the CIM thread
                -&amp;gt; or one fresh RunScript.pyt call
                    -&amp;gt; live project, map, and data&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;MCP is the small door into the live map&lt;/h2&gt;

&lt;p&gt;
  The bridge exposes 14 tools through the
  &lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;Model Context Protocol (MCP)&lt;/a&gt;. There
  are focused tools such as &lt;code&gt;list_layers&lt;/code&gt;, &lt;code&gt;get_field_list&lt;/code&gt;,
  &lt;code&gt;feature_count&lt;/code&gt;, &lt;code&gt;select_by_attribute&lt;/code&gt;,
  &lt;code&gt;add_field&lt;/code&gt;, and &lt;code&gt;run_geoprocessing&lt;/code&gt;. The centerpiece is
  &lt;code&gt;run_python_current(code)&lt;/code&gt;, which gives Claude a general ArcPy path when
  the curated tools are not enough.
&lt;/p&gt;

&lt;p&gt;
  Inspection calls and lightweight map interactions use the .NET SDK on Pro&#39;s CIM
  thread, without ArcPy. Data updates and arbitrary Python go through the fresh
  geoprocessing call. Those calls are serialized behind a semaphore because
  geoprocessing does not appreciate re-entrancy. Request and response data travel as
  geoprocessing string parameters, so there is no file-polling spool between C# and
  Python.
&lt;/p&gt;

&lt;p&gt;
  Everything stays local except the normal Claude model traffic. The MCP server binds
  only to loopback, chooses an ephemeral port at startup, and requires a new bearer
  token for the session. The add-in writes those details into a generated
  &lt;code&gt;.mcp.json&lt;/code&gt; and starts Claude Code in streaming JSON mode. It can use the
  user&#39;s existing Claude Code login, an OAuth token, or an Anthropic API key; stored
  secrets are protected with Windows DPAPI.
&lt;/p&gt;

&lt;p&gt;
  There is also a very practical UI detail. Claude answers in Markdown, while ArcGIS
  Pro hosts a native WPF interface. ProCowork converts the Markdown with Markdig and
  renders headings, code, lists, links, and real tables using themed WPF controls. Tool
  calls can be shown or hidden, but I like keeping them visible: if an agent is changing
  my map, I want to see the code it ran.
&lt;/p&gt;

&lt;h2&gt;A word of caution&lt;/h2&gt;

&lt;p&gt;
  By default, ProCowork runs in what I call YOLO mode. Generated code executes on the
  open project without an approval prompt. That immediacy is the point, and it is also
  exactly as dangerous as it sounds. ArcPy can edit or delete real data. The bundled
  instructions tell Claude to inspect fields first, use edit sessions, create backups
  before destructive operations, and report affected row counts. Those are useful
  working rules; they are not a security boundary.
&lt;/p&gt;

&lt;p&gt;
  So this is an MVP and an experiment, not an Esri product or a supported Esri
  solution. It currently requires Windows, ArcGIS Pro 3.7, the ArcGIS Pro SDK for .NET,
  and Claude Code. Keep backups and do not point it at the only copy of important data.
  The architecture can support an approval step, but the proper approval card is still
  future work.
&lt;/p&gt;

&lt;p&gt;
  What excites me here is not only that an LLM can write ArcPy. We already knew that.
  It is that a small, local bridge can give an agent useful context from a live desktop
  GIS, let it act through the correct execution path, and return the evidence to the
  same place where the request started.
&lt;/p&gt;

&lt;p&gt;
  The source code, build instructions, architecture notes, and current limitations are
  available in the
  &lt;a href=&quot;https://github.com/mraad/ProCowork&quot;&gt;ProCowork repository&lt;/a&gt;. The next step
  is the one I would want before using it on serious work: a clear review-and-approve
  experience for generated operations.
&lt;/p&gt;

&lt;p&gt;More to come :-)&lt;/p&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/875081500410751900/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/875081500410751900' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/875081500410751900'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/875081500410751900'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/arcgis-pro-claude-code-and-loopback.html' title='ArcGIS Pro, Claude Code, and a Loopback Bridge'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-3207394641017657153</id><published>2026-09-08T17:39:06.677-04:00</published><updated>2026-09-08T17:39:06.677-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Control Systems"/><category scheme="http://www.blogger.com/atom/ns#" term="LunarLander"/><category scheme="http://www.blogger.com/atom/ns#" term="Model Predictive Control"/><category scheme="http://www.blogger.com/atom/ns#" term="Quadratic Programming"/><category scheme="http://www.blogger.com/atom/ns#" term="Reinforcement Learning"/><title type='text'>One Lander, Three Controllers: Reinforcement Learning, MPC, and Quadratic Programming</title><content type='html'>&lt;p&gt;I was always interested in the Lunar Lander game and suddenly I heard about model predictive control and quadratic programming. I want to see if the Lunar Lander RL can be adapted to that one in here.&lt;/p&gt;

&lt;p&gt;
  That question led me to build three related experiments around Gymnasium&#39;s
  &lt;code&gt;LunarLander-v3&lt;/code&gt;:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mraad/lunar-rl&quot;&gt;&lt;strong&gt;Lunar RL&lt;/strong&gt;&lt;/a&gt;: a causal-transformer PPO policy trained through reinforcement learning.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mraad/lunar-mpc&quot;&gt;&lt;strong&gt;Lunar MPC&lt;/strong&gt;&lt;/a&gt;: adaptive model predictive control using discrete beam search.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mraad/lunar-qp&quot;&gt;&lt;strong&gt;Lunar QP&lt;/strong&gt;&lt;/a&gt;: model predictive control whose plan is solved as a convex quadratic program.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
  All three control the same lander, but they answer very different questions. RL asks,
  “Can experience produce a policy that maps observations directly to actions?” MPC asks,
  “Given an approximate physics model, which short action sequence looks best from the
  current state?” QP asks, “Can that short-horizon plan be expressed as a fast convex
  optimization problem with explicit constraints?”
&lt;/p&gt;

&lt;p&gt;
  The progression is not a conversion of one trained checkpoint into another controller.
  It is an adaptation of the &lt;em&gt;problem&lt;/em&gt;: the observation interface, action space,
  randomized starts, disturbance scenarios, evaluation rules, and replay tools carry
  forward, while the decision-making mechanism changes completely.
&lt;/p&gt;

&lt;h2&gt;One lander, three ways to decide&lt;/h2&gt;

&lt;p&gt;
  LunarLander exposes an eight-dimensional state: horizontal and vertical position,
  horizontal and vertical velocity, angle, angular velocity, and two leg-contact flags.
  At every simulator step, a controller chooses one of four legal actions: coast, fire
  the left orientation engine, fire the main engine, or fire the right orientation
  engine.
&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:left&quot;&gt;Question&lt;/th&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:left&quot;&gt;Lunar RL&lt;/th&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:left&quot;&gt;Lunar MPC&lt;/th&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:left&quot;&gt;Lunar QP&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;&lt;strong&gt;Decision&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;One policy inference&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Beam search over discrete action sequences&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Convex optimization of fractional engine duties&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;&lt;strong&gt;Knowledge&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Learned from simulated experience&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Explicit approximate dynamics&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Local affine dynamics and linear constraints&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;&lt;strong&gt;Adaptation&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Requires learning or retraining&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Replans and estimates engine response in flight&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Re-solves and estimates engine response in flight&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;&lt;strong&gt;Action handling&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Outputs one legal discrete action&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Searches legal discrete actions&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Allocates fractional requests into discrete pulses&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;&lt;strong&gt;Primary cost&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Expensive training, cheap inference&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Online combinatorial search&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;Online numerical optimization&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2&gt;1. Lunar RL: learn the behavior before flight&lt;/h2&gt;

&lt;p&gt;
  &lt;a href=&quot;https://github.com/mraad/lunar-rl&quot;&gt;Lunar RL&lt;/a&gt; uses Proximal Policy
  Optimization (PPO). During training, many simulated landers collect trajectories. PPO
  adjusts an actor that selects actions and a critic that estimates future return. After
  training, control is simple: encode the latest context, evaluate the policy once, and
  execute the selected discrete action.
&lt;/p&gt;

&lt;p style=&quot;text-align:center&quot;&gt;
  &lt;a href=&quot;https://github.com/mraad/lunar-rl/blob/main/docs/showcase.mp4&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/mraad/lunar-rl/main/docs/showcase.gif&quot;
         alt=&quot;Transformer PPO policy landing LunarLander from an off-pad tilted start&quot;
         width=&quot;420&quot; style=&quot;max-width:100%;height:auto&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align:center&quot;&gt;&lt;em&gt;The recorded Lunar RL policy landing from an off-pad, tilted start.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;
  The network can process either the normal eight-value state or rendered pixels. Each
  timestep becomes a token containing the current state, previous action, previous
  reward, and optionally an Impala-CNN image embedding. Three GTrXL blocks process a
  causal window of tokens. The actor emits four logits; the critic represents value with
  41 two-hot bins in symlog space.
&lt;/p&gt;

&lt;p&gt;
  There is an honest architectural lesson in the repository: the standard eight-value
  observation is already Markov. A small multilayer perceptron can solve it, so a
  transformer is unnecessary in vector mode. Memory becomes useful in pixel mode because
  a single image shows pose but does not directly reveal velocity or angular rate. A
  sequence of frames lets attention recover part of that hidden state.
&lt;/p&gt;

&lt;p&gt;
  The advantage of RL is that the policy can absorb behavior that is difficult to write
  as equations. The cost is paid during training, and the result is tied to the
  distribution it experienced. A center-trained checkpoint landed only six of eight
  randomized off-pad approaches. Training with the same position and tilt randomization
  produced a robust checkpoint that landed all eight in that experiment.
&lt;/p&gt;

&lt;p&gt;
  The repository reports a historical mean return of +282.6 for its centered checkpoint
  and documents stronger robust-policy evaluations. It also states an important current
  limitation: the shipped metrics predate a correction that made the PPO update context
  exactly match the context used while acting. The first retrained candidate after that
  correction failed the strict promotion checks, so the historical checkpoint remains in
  place while a corrected baseline is validated. That kind of disclosure matters more
  than a single high score.
&lt;/p&gt;

&lt;h2&gt;2. Lunar MPC: predict, act once, and predict again&lt;/h2&gt;

&lt;p&gt;
  &lt;a href=&quot;https://github.com/mraad/lunar-mpc&quot;&gt;Lunar MPC&lt;/a&gt; moves the computation
  from training time to control time. It starts with an approximate six-state physics
  model for position, velocity, angle, and angular velocity. At each observation, it
  searches possible future action sequences, chooses the lowest-cost plan, applies only
  its first action, observes what actually happened, and repeats.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;observe current state
update the engine model
predict candidate action sequences
keep the best beam
execute the first action
repeat from the next observation&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  This repeated loop is the defining idea of model predictive control. A prediction does
  not have to remain accurate for the entire landing. It needs to be useful long enough
  to choose the next action, because the controller replans after 0.02 simulated seconds.
  The default horizon contains 16 blocks of four physics steps, or 1.28 seconds of
  foresight, with a beam width of 32.
&lt;/p&gt;

&lt;p style=&quot;text-align:center&quot;&gt;
  &lt;a href=&quot;https://github.com/mraad/lunar-mpc/blob/main/docs/showcase.mp4&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/mraad/lunar-mpc/main/docs/showcase.gif&quot;
         alt=&quot;Adaptive beam-search MPC landing after a main-engine power loss&quot;
         width=&quot;420&quot; style=&quot;max-width:100%;height:auto&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align:center&quot;&gt;&lt;em&gt;Adaptive MPC replans after an unannounced 30% main-engine power loss.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;
  Exhaustively expanding four actions for 16 blocks would require examining
  &lt;code&gt;4**16&lt;/code&gt; sequences. Beam search avoids that explosion. At each depth, it
  expands the current candidates with all four actions and keeps only the 32 plans with
  the lowest accumulated cost. The cost penalizes horizontal error, velocity error,
  vertical descent error, tilt, angular motion, unsafe approach geometry, and excessive
  main-engine use.
&lt;/p&gt;

&lt;p&gt;
  The model also learns during the flight. Bounded recursive least squares compares the
  predicted velocity change with the observed change and updates estimates of main-engine
  acceleration, side-engine acceleration, translational bias, angular response, and
  angular bias. Contact impulses and extreme accelerations are excluded because they are
  poor evidence of airborne engine performance.
&lt;/p&gt;

&lt;p&gt;
  This creates two forms of adaptation. Replanning reacts to state error even when the
  model remains fixed. Parameter estimation tries to correct the model itself. The
  distinction is visible in the fault experiment: immediately before action 100, the
  evaluator reduces main-engine power by 30%. The controller is not told the fault time
  or the new multiplier; it sees only the resulting motion.
&lt;/p&gt;

&lt;p&gt;
  On historical evaluation seeds 200–249, adaptive MPC passed 49 of 50 strict landing
  checks both normally and under the fault. The fixed-model controller also passed 49 of
  50 normally but fell to 42 of 50 under the fault. This is evidence that online
  identification helped in that specific experiment, while still leaving a known
  horizontal-recovery failure and no safety certificate.
&lt;/p&gt;

&lt;h2&gt;3. Lunar QP: make the plan a convex optimization&lt;/h2&gt;

&lt;p&gt;
  &lt;a href=&quot;https://github.com/mraad/lunar-qp&quot;&gt;Lunar QP&lt;/a&gt; is still MPC. The change
  is the planner inside the repeated loop. Instead of beam-searching discrete sequences,
  it linearizes the approximate dynamics around the current state and asks OSQP to choose
  fractional engine duties over the horizon.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;minimize    1/2 z&#39; P z + q&#39; z
subject to  lower &amp;lt;= A z &amp;lt;= upper&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  The predicted dynamics have the local affine form
  &lt;code&gt;x[k+1] = A&lt;sub&gt;d&lt;/sub&gt;x[k] + B&lt;sub&gt;d&lt;/sub&gt;u[k] + c&lt;/code&gt;. The implementation
  condenses the state sequence into &lt;code&gt;X = base + response*U&lt;/code&gt;, leaving the
  engine requests and slack variables as optimization decisions. With 16 prediction
  blocks, the QP has 96 variables: 48 engine duties and 48 nonnegative slacks.
&lt;/p&gt;

&lt;p&gt;
  Its quadratic objective penalizes deviation from desired position, velocity, descent
  rate, tilt, and angular velocity. Linear constraints bound every duty between zero and
  one and require the three engine fractions to sum to at most one. Soft approach limits
  encourage clearance, bounded tilt, and remaining inside the viewport. Explicit slack
  variables keep a difficult state mathematically feasible while exposing how much the
  preferred envelope was relaxed.
&lt;/p&gt;

&lt;p style=&quot;text-align:center&quot;&gt;
  &lt;a href=&quot;https://github.com/mraad/lunar-qp/blob/main/docs/showcase.mp4&quot;&gt;
    &lt;img src=&quot;https://raw.githubusercontent.com/mraad/lunar-qp/main/docs/showcase.gif&quot;
         alt=&quot;Quadratic-programming MPC landing after a main-engine power loss&quot;
         width=&quot;420&quot; style=&quot;max-width:100%;height:auto&quot; /&gt;
  &lt;/a&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align:center&quot;&gt;&lt;em&gt;QP-based MPC uses fractional plans and executes discrete engine pulses.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;
  The most interesting mismatch is at the actuator. The Gymnasium environment accepts
  one discrete action, but the convex QP may request something like 60% main engine, 10%
  left engine, and 30% coast. A pulse-density allocator accumulates these fractions over
  time, executes whichever action has the largest balance, and subtracts one from that
  balance. Over repeated decisions, the discrete pulse counts approximate the requested
  fractions.
&lt;/p&gt;

&lt;p&gt;
  This is a convex relaxation followed by discrete allocation, not mixed-integer
  optimization. The predicted fractional path and the path produced by real pulses can
  diverge. Replanning corrects part of that error, but a solved QP is only a numerical
  statement about the approximate optimization problem. It is not proof of a safe
  landing in the nonlinear Box2D simulator.
&lt;/p&gt;

&lt;p&gt;
  Lunar QP uses the same style of bounded online engine estimation as Lunar MPC. On its
  separate evaluation seeds 300–319, both adaptive and fixed QP passed 20 of 20 normal
  flights. Under the 30% engine fault, adaptive QP passed 15 of 20 and fixed QP passed 16
  of 20. The result does &lt;em&gt;not&lt;/em&gt; demonstrate an adaptation advantage for QP. It also
  shows why successful development seeds are not enough: all four controller/scenario
  combinations had passed the eight development starts.
&lt;/p&gt;

&lt;h2&gt;Can the RL solution be adapted to MPC and QP?&lt;/h2&gt;

&lt;p&gt;
  Yes, if “adapted” means preserving the experimental system and replacing the decision
  rule. The reusable parts are substantial:
&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The same eight controller-visible observations and four legal actions.&lt;/li&gt;
  &lt;li&gt;The same randomized off-pad and tilted starting-pose wrapper.&lt;/li&gt;
  &lt;li&gt;The same separation between controller information and evaluator-only simulator truth.&lt;/li&gt;
  &lt;li&gt;The same disturbance idea: reduce engine power without telling the controller.&lt;/li&gt;
  &lt;li&gt;Strict landing checks based on termination, settling, leg contact, and foot position.&lt;/li&gt;
  &lt;li&gt;Recorded trajectories and self-contained browser replays for inspecting failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
  What does not transfer is the trained policy itself. PPO weights encode a nonlinear
  mapping learned from data; an MPC controller needs explicit transition equations and a
  cost; a QP additionally needs dynamics and constraints that remain affine or quadratic
  after local approximation. Trying to reinterpret neural-network weights as those
  matrices would solve a different and much harder identification problem.
&lt;/p&gt;

&lt;p&gt;
  A future hybrid could combine them more directly. RL could learn a residual correction
  to the MPC physics model, tune terminal costs, propose a warm-start plan, or provide a
  recovery policy when the optimizer fails. MPC could supervise safer exploration or
  generate demonstrations for an RL policy. Those designs require new matched
  experiments; none of the three current repositories claims to implement them.
&lt;/p&gt;

&lt;h2&gt;Reading the results without fooling ourselves&lt;/h2&gt;

&lt;p&gt;
  The reported numbers are evidence about each repository, not a leaderboard. Lunar RL,
  Lunar MPC, and Lunar QP use different training histories, seed ranges, costs, horizons,
  discretization choices, and evaluation sample sizes. The MPC and QP controllers even
  optimize different cost functions. Comparing their pass counts directly would not
  isolate whether policy learning, beam search, or QP is better.
&lt;/p&gt;

&lt;p&gt;
  A fair head-to-head study would freeze the controller-visible state, starting poses,
  terrain seeds, engine faults, landing definition, time limit, compute budget, and
  reporting format. It would measure pass rate, worst-case behavior, return distribution,
  decision latency, deadline misses, and sensitivity to model error. For QP, it should
  also report slack and the error introduced by fractional-to-discrete allocation. For
  RL, it should report variation across training seeds and hardware backends.
&lt;/p&gt;

&lt;h2&gt;Run the three experiments&lt;/h2&gt;

&lt;p&gt;Each repository is standalone and uses &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;Reinforcement learning&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;git clone https://github.com/mraad/lunar-rl.git
cd lunar-rl
uv sync --locked
uv run lunar-rl-view --ckpt lunar_agent_robust.pt --seed 0 --greedy&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Adaptive beam-search MPC&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;git clone https://github.com/mraad/lunar-mpc.git
cd lunar-mpc
uv sync --locked
uv run lunar-mpc --episodes 8 --seed 0 --thrust-scale 0.7 \
  --out dist/fault-adaptive.json --replay dist/fault-adaptive.html&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;QP-based MPC&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;git clone https://github.com/mraad/lunar-qp.git
cd lunar-qp
uv sync --locked
uv run lunar-qp --episodes 8 --seed 0 --thrust-scale 0.7 \
  --out dist/qp/fault-adaptive.json --replay dist/qp/fault-adaptive.html&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  The generated MPC and QP replay pages expose more than the final score. They show the
  actual trajectory, predicted path, chosen plan, engine estimates, and decision timing.
  The QP replay adds fractional requests, executed pulses, solver status, residuals, and
  slack. The RL viewer shows policy probabilities, critic values, and attention over the
  recent context. Looking inside a failed landing is often more useful than adding another
  decimal place to an average.
&lt;/p&gt;

&lt;h2&gt;What the three controllers taught me&lt;/h2&gt;

&lt;p&gt;
  RL can discover a capable policy without a hand-written flight model, but its behavior
  depends on training coverage and careful validation. MPC makes assumptions visible and
  can react immediately when observed motion differs from prediction, but it pays for
  planning at every step. QP makes objectives and constraints especially inspectable and
  solves the relaxed plan efficiently, while introducing local-linearization and actuator-
  allocation errors.
&lt;/p&gt;

&lt;p&gt;
  The most useful outcome of adapting LunarLander across these methods is not choosing one
  universal winner. It is seeing the trade clearly: learned behavior, discrete online
  search, and constrained convex optimization each fail in different ways. Keeping the
  environment, disturbances, evidence, and replay tools consistent turns those failures
  into something we can study rather than hide behind a score.
&lt;/p&gt;

&lt;h2&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mraad/lunar-rl&quot;&gt;Lunar RL repository&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mraad/lunar-mpc&quot;&gt;Lunar MPC repository&lt;/a&gt; and its &lt;a href=&quot;https://github.com/mraad/lunar-mpc/blob/main/docs/adaptive-mpc.md&quot;&gt;adaptive MPC introduction&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/mraad/lunar-qp&quot;&gt;Lunar QP repository&lt;/a&gt; and its &lt;a href=&quot;https://github.com/mraad/lunar-qp/blob/main/docs/quadratic-programming.md&quot;&gt;QP introduction&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gymnasium.farama.org/environments/box2d/lunar_lander/&quot;&gt;Gymnasium LunarLander documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://osqp.org/docs/&quot;&gt;OSQP documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1707.06347&quot;&gt;Proximal Policy Optimization Algorithms&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/3207394641017657153/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/3207394641017657153' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3207394641017657153'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3207394641017657153'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/one-lander-three-controllers.html' title='One Lander, Three Controllers: Reinforcement Learning, MPC, and Quadratic Programming'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-8675663881419372638</id><published>2026-09-08T17:30:39.672-04:00</published><updated>2026-09-08T17:30:39.672-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Deep Reinforcement Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="DQN"/><category scheme="http://www.blogger.com/atom/ns#" term="Gymnasium"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="PyTorch"/><title type='text'>Teaching a Lunar Lander to Stop Hovering: DQN, Prioritized Replay, and n-Step Returns</title><content type='html'>&lt;p&gt;
  LunarLander looks simple: observe eight numbers, choose one of four engine actions,
  and guide a small craft between two flags. In practice, it exposes several failure
  modes that make Deep Q-Learning interesting. My first promising agents learned to
  approach the landing pad, then hovered above it until the episode timed out. Other
  runs climbed past a score of 100 and later collapsed.
&lt;/p&gt;

&lt;p&gt;
  I built &lt;a href=&quot;https://github.com/mraad/LunarLanderDRL&quot;&gt;LunarLanderDRL&lt;/a&gt; to work
  through those problems in a compact PyTorch implementation. The final agent combines
  a Deep Q-Network (DQN), proportional prioritized experience replay, Double DQN,
  Huber loss, Polyak target updates, and three-step returns. Across three training seeds,
  the resulting checkpoints averaged &lt;strong&gt;265.7&lt;/strong&gt; on 90 held-out episodes and
  recorded &lt;strong&gt;zero crashes&lt;/strong&gt;.
&lt;/p&gt;

&lt;p style=&quot;text-align:center&quot;&gt;
  &lt;img
    src=&quot;https://raw.githubusercontent.com/mraad/LunarLanderDRL/main/docs/lunarlander.gif&quot;
    alt=&quot;A trained DQN agent landing the Gymnasium LunarLander&quot;
    width=&quot;320&quot;
    style=&quot;max-width:100%;height:auto&quot;
  /&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align:center&quot;&gt;&lt;em&gt;Two consecutive rollouts of the shipped policy, scoring 308.1 and 272.1.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;The problem&lt;/h2&gt;

&lt;p&gt;
  Gymnasium&#39;s &lt;code&gt;LunarLander-v3&lt;/code&gt; returns an eight-dimensional observation:
  horizontal and vertical position, horizontal and vertical velocity, angle, angular
  velocity, and two leg-contact flags. The discrete action space contains four choices:
  do nothing, fire the left orientation engine, fire the main engine, or fire the right
  orientation engine.
&lt;/p&gt;

&lt;p&gt;
  A DQN approximates the action-value function &lt;code&gt;Q(s, a)&lt;/code&gt;. Given a state, the
  network emits one value for each action, and a greedy policy selects the largest one.
  The model in this project is deliberately small:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;8 inputs -&amp;gt; 256 -&amp;gt; 128 -&amp;gt; 64 -&amp;gt; 4 Q-values&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  The hidden layers use ReLU activations and Xavier initialization. During training,
  an epsilon-greedy policy sometimes takes a random action, with epsilon decaying from
  exploration toward exploitation. The network architecture is the easy part. Stable
  learning depends on what transitions we replay, how we calculate targets, and how we
  decide that one checkpoint is better than another.
&lt;/p&gt;

&lt;h2&gt;Prioritized experience replay&lt;/h2&gt;

&lt;p&gt;
  Uniform replay treats every stored transition as equally informative. Prioritized
  experience replay (PER) focuses training on transitions where the current prediction
  is most wrong. This implementation assigns each transition the priority
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;p(i) = (|TD error(i)| + epsilon) ** alpha&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  &lt;code&gt;alpha&lt;/code&gt; controls how strongly priority affects sampling. A value of zero reduces
  PER to uniform replay; the default is 0.6. A new transition has no TD error yet, so it
  enters with the highest priority seen so far. This ensures that it is replayed once
  before being assigned a data-driven priority.
&lt;/p&gt;

&lt;p&gt;
  The replay buffer stores states, actions, rewards, terminal flags, next states, and
  bootstrap discounts in flat NumPy arrays. Priorities live in a binary sum tree. Each
  parent stores the sum of its children, which lets the agent update a priority or locate
  a sampled leaf in &lt;code&gt;O(log N)&lt;/code&gt; time. A batch is sampled from equal-mass segments
  of the total priority, reducing variance compared with drawing every item independently.
&lt;/p&gt;

&lt;p&gt;
  Prioritized sampling changes the training distribution, so each sampled loss receives
  an importance-sampling correction:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;w(i) = (N * P(i)) ** -beta&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  The weights are normalized by the maximum weight in the batch. Beta is annealed from
  0.4 to 1.0, making the correction stronger as the policy approaches convergence.
&lt;/p&gt;

&lt;h2&gt;Stopping Q-value overestimation from taking over&lt;/h2&gt;

&lt;p&gt;
  A vanilla DQN uses a maximum over estimated values when it constructs the bootstrap
  target. Estimation noise makes that maximum optimistic. Because the target is itself
  used to train future estimates, the bias can compound until a policy that appeared to
  be improving suddenly collapses.
&lt;/p&gt;

&lt;p&gt;
  Double DQN separates action selection from action evaluation. The online network picks
  the next action, while the target network evaluates it:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;next_action = online(next_state).argmax()
next_value  = target(next_state)[next_action]
target_q    = reward + discount * next_value&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  The target network is not copied in occasional hard jumps. It is updated with Polyak
  averaging on every training step:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;target = (1 - tau) * target + tau * online&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  The default &lt;code&gt;tau&lt;/code&gt; is &lt;code&gt;1e-3&lt;/code&gt;. This gives the target a slowly moving
  reference point while the online network continues to learn.
&lt;/p&gt;

&lt;p&gt;
  The project also uses Huber loss instead of squared error. This choice matters when it
  is paired with PER: the replay sampler intentionally selects transitions with large TD
  errors, and squaring an outlier can let one sample dominate the gradient. Huber loss
  is quadratic close to zero and linear for large errors. Gradient norms are clipped at
  10 as a final guard against unstable updates.
&lt;/p&gt;

&lt;h2&gt;Why the one-step agent learned to hover&lt;/h2&gt;

&lt;p&gt;
  The most revealing failure was not a crash. A one-step agent often descended near the
  pad, stabilized, and hovered until Gymnasium&#39;s 1,000-step limit. That behavior makes
  sense from the agent&#39;s temporary view of the world. LunarLander provides shaping reward
  for remaining close to the pad at low velocity, while the large touchdown reward is
  still hundreds of actions away. With one-step bootstrapping, that delayed reward moves
  backward through the value function slowly.
&lt;/p&gt;

&lt;p&gt;
  Three-step returns propagate the landing signal more quickly. Instead of storing only
  one immediate reward, the buffer stores a folded return:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;R(t) = r(t) + gamma*r(t+1) + gamma**2*r(t+2)
target_q = R(t) + gamma**k * Q(next_state, next_action)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  Two implementation details are essential. First, the bootstrap multiplier must be
  &lt;code&gt;gamma**k&lt;/code&gt;, where &lt;code&gt;k&lt;/code&gt; is the number of rewards actually folded into
  that transition. The buffer therefore stores a discount beside each transition.
  Reusing a single &lt;code&gt;gamma&lt;/code&gt; would silently produce incorrect targets.
&lt;/p&gt;

&lt;p&gt;
  Second, the pending reward window must be flushed at every episode boundary. Otherwise,
  the last &lt;code&gt;n_step - 1&lt;/code&gt; transitions disappear. Those are often the exact
  transitions containing touchdown, so dropping them removes the examples the agent most
  needs. If a real terminal occurs inside the window, the fold stops there. A time-limit
  truncation ends the rollout but does not zero a state value that could continue beyond
  the artificial cutoff.
&lt;/p&gt;

&lt;h2&gt;Evaluate the policy you will actually deploy&lt;/h2&gt;

&lt;p&gt;
  Early versions saved checkpoints using the moving average of training rewards. That
  metric is contaminated by epsilon-greedy exploration. A lucky random action can improve
  a score, and a useful exploratory action can also lower it. The checkpoint should be
  selected using the deterministic policy that will run after training.
&lt;/p&gt;

&lt;p&gt;
  The training loop now evaluates a fully greedy policy every 25 episodes on a fixed set
  of seeds. Evaluation uses a separate Gymnasium environment, so resetting it never
  changes the training environment&#39;s random-number stream. This produced a striking
  example: one checkpoint had a training average of 239.8 but scored only 160.6 under
  greedy evaluation. In another run, the training average was just 78.2 while the greedy
  policy scored 269.1. Selecting on training reward would have preserved the first and
  discarded the second.
&lt;/p&gt;

&lt;h2&gt;Measured results&lt;/h2&gt;

&lt;p&gt;I trained three seeds with the following configuration:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;uv run main.py --n_episodes 4000 --eps_ratio 0.35 --eps_final 0.01 \
  --learning_rate 5e-4 --warm_start 5000 --n_step 3 \
  --eval_episodes 20 --solved_score 275&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
  The runs stopped between episodes 474 and 999 and took roughly ten minutes each on an
  M-series CPU. I evaluated every checkpoint on the same 30 seeds, numbered 1000 through
  1029. Those held-out seeds are disjoint from the seeds used to select checkpoints.
&lt;/p&gt;

&lt;table style=&quot;border-collapse:collapse;width:100%&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:left&quot;&gt;Training seed&lt;/th&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;Mean&lt;/th&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;Median&lt;/th&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;Solved&lt;/th&gt;
      &lt;th style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;Crashed&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;42&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;280.6&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;281.2&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;30/30&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;43&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;243.3&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;278.4&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;24/30&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;44 (shipped model)&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;273.1&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;286.5&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;28/30&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px&quot;&gt;&lt;strong&gt;Pooled&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;&lt;strong&gt;265.7&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;&lt;strong&gt;282.1&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;&lt;strong&gt;82/90&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #bbb;padding:6px;text-align:right&quot;&gt;&lt;strong&gt;0/90&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
  The comparison that changed the design was the one-step run. With otherwise matching
  settings, it averaged 220.3, solved 25 of 30 held-out episodes, and crashed twice. The
  three-step runs produced no crashes in 90 episodes. Eight episodes finished below 200,
  but these were soft or slow landings scoring between 21 and 46, rather than destructive
  impacts.
&lt;/p&gt;

&lt;p&gt;
  I ship the seed-44 checkpoint even though seed 42 scored higher. Seed 44 lies closer to
  the pooled result, so it is a more representative artifact than the luckiest run.
&lt;/p&gt;

&lt;h2&gt;Run it yourself&lt;/h2&gt;

&lt;p&gt;
  The project uses &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt;, pins dependencies in
  &lt;code&gt;uv.lock&lt;/code&gt;, and includes a trained model. Clone the repository and install the
  environment:
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;git clone https://github.com/mraad/LunarLanderDRL.git
cd LunarLanderDRL
uv sync&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Replay the shipped policy into an MP4:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;uv run render.py

# Scores only, without creating video
uv run render.py --no-save&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Train a new policy and monitor it with TensorBoard:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;uv run main.py --n_episodes 4000 --learning_rate 5e-4 \
  --warm_start 5000 --n_step 3
uv run tensorboard --logdir logs&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The automated checks cover schedules, both replay buffers, the sum tree, n-step folding and flushing, and a short end-to-end training run against the real environment:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;uv run pytest test_rl.py&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;What I learned&lt;/h2&gt;

&lt;p&gt;
  The useful lesson was not that one algorithmic trick solves LunarLander. The pieces
  address different sources of failure. PER spends more updates on surprising
  transitions. Importance weights correct its sampling bias. Double DQN reduces optimistic
  targets. Huber loss and gradient clipping keep high-error examples from destabilizing
  an update. Three-step returns move delayed touchdown information backward faster. Greedy,
  isolated evaluation selects the policy that will actually be used.
&lt;/p&gt;

&lt;p&gt;
  The project remains intentionally small enough to read end to end. The next practical
  improvements would be training every few environment steps instead of every step,
  scheduling epsilon by step rather than episode, annealing PER beta across the full run,
  and adding a dueling value/advantage head. The current implementation and trained
  weights are available under the Apache 2.0 license at
  &lt;a href=&quot;https://github.com/mraad/LunarLanderDRL&quot;&gt;github.com/mraad/LunarLanderDRL&lt;/a&gt;.
&lt;/p&gt;

&lt;h2&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://gymnasium.farama.org/environments/box2d/lunar_lander/&quot;&gt;Gymnasium LunarLander documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.nature.com/articles/nature14236&quot;&gt;Human-level control through deep reinforcement learning&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1511.05952&quot;&gt;Prioritized Experience Replay&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1509.06461&quot;&gt;Deep Reinforcement Learning with Double Q-learning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/8675663881419372638/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/8675663881419372638' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/8675663881419372638'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/8675663881419372638'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2026/09/teaching-lunar-lander-to-stop-hovering.html' title='Teaching a Lunar Lander to Stop Hovering: DQN, Prioritized Replay, and n-Step Returns'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-1687137542363741386</id><published>2024-02-03T01:10:00.000-05:00</published><updated>2026-09-12T17:27:54.383-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AutoGluon"/><category scheme="http://www.blogger.com/atom/ns#" term="AutoML"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="NYC"/><category scheme="http://www.blogger.com/atom/ns#" term="Spark"/><title type='text'>On Using AutoML for NYC Taxi Trip Duration Prediction</title><content type='html'>&lt;p&gt;&lt;span style=&quot;background-color: white; font-family: arial;&quot;&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;While explaining&lt;/span&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://optuna.org/&quot; rel=&quot;nofollow&quot; style=&quot;box-sizing: border-box; font-size: 16px; text-underline-offset: 0.2rem;&quot;&gt;Optuna&lt;/a&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;to a client in the context of hyperparameter tuning, and performing more research on the topic, I came across&lt;/span&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://auto.gluon.ai/stable/index.html&quot; rel=&quot;nofollow&quot; style=&quot;box-sizing: border-box; font-size: 16px; text-underline-offset: 0.2rem;&quot;&gt;AutoGluon&lt;/a&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;to perform &quot;&lt;/span&gt;&lt;a href=&quot;https://www.automl.org/automl/&quot; rel=&quot;nofollow&quot; style=&quot;box-sizing: border-box; font-size: 16px; text-underline-offset: 0.2rem;&quot;&gt;AutoML&lt;/a&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-size: 16px;&quot;&gt;for images, text, and tabular data&quot;. After a quick scan of the documentation, I decided to give it a try and see how it performs on a simple project.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;p dir=&quot;auto&quot; style=&quot;box-sizing: border-box; font-size: 16px; margin-bottom: 16px; margin-top: 0px;&quot;&gt;&lt;span style=&quot;background-color: white; font-family: arial;&quot;&gt;I always loved the Kaggle competition NYC Taxi Trip Duration, as the data has spatial, temporal, and other traditional attribute information and is a great dataset to test various models and feature engineering techniques.&lt;/span&gt;&lt;/p&gt;&lt;p dir=&quot;auto&quot; style=&quot;box-sizing: border-box; font-size: 16px; margin-bottom: 16px; margin-top: 0px;&quot;&gt;&lt;span style=&quot;background-color: white; font-family: arial;&quot;&gt;I used a local&amp;nbsp;&lt;a href=&quot;https://spark.apache.org/&quot; rel=&quot;nofollow&quot; style=&quot;box-sizing: border-box; text-underline-offset: 0.2rem;&quot;&gt;Apache Spark&lt;/a&gt;&amp;nbsp;instance (as it is my go-to ETL engine) to perform some feature engineering before letting AutoGluon do its magic. As a quick proof of concept, the results are quite impressive, and &lt;a href=&quot;https://github.com/mraad/auto_ml_nyc&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt; are the steps to reproduce the project and visualize the result.&lt;/span&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/1687137542363741386/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/1687137542363741386' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/1687137542363741386'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/1687137542363741386'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2024/02/on-using-automl-for-nyc-taxi-trip.html' title='On Using AutoML for NYC Taxi Trip Duration Prediction'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-6524804252522085352</id><published>2024-01-28T11:16:00.000-05:00</published><updated>2026-09-12T17:27:55.986-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Spark"/><title type='text'>Arabic SDK For Apache Spark</title><content type='html'>&lt;p&gt;&lt;span style=&quot;color: #0e101a;&quot;&gt;I recently attended the &lt;a href=&quot;https://www.esrisaudiarabia.com/en-sa/about/events/esrisaudi-uc2024/overview&quot; target=&quot;_blank&quot;&gt;Esri Saudi Arabia User Conference&lt;/a&gt; and was amazed by the changes in the Kingdom. The capital city of Riyadh is booming and proliferating. During the conference, I presented on integrating GenerativeAI and GIS in the plenary session and led a session on BigData and &lt;a href=&quot;https://developers.arcgis.com/geoanalytics/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;GeoAnalytics Engine&lt;/a&gt;. GeoAnalytics Engine, based on &lt;a href=&quot;https://spark.apache.org/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Apache Spark&lt;/a&gt;, allows spatial operations on Spark data frames. We showcased a project called &quot;A Day in the Life,&quot; which used historical traffic data from &lt;a href=&quot;https://www.here.com/learn/blog/traffic-analytics-speed-data&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;HERE&lt;/a&gt; to demonstrate traffic congestion during peak hours. Traffic is notoriously bad in the city, so this was a fitting example. My colleague Mahmoud H. presented a traditional workflow process in a Jupyter Notebook off a &lt;a href=&quot;https://cloud.google.com/dataproc?hl=en&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Google Cloud DataProc&lt;/a&gt; cluster, efficiently processing over 300 million records (this is relatively &quot;small&quot;). The processed traffic information was then displayed in &lt;a href=&quot;https://www.esri.com/en-us/arcgis/products/arcgis-pro/overview&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;ArcGIS Pro&lt;/a&gt; in a time-aware layer to reflect the congestion visually while activating a time slider. At the end of the presentation, we surprised the audience by using &lt;b&gt;ChatGPT&lt;/b&gt; to translate &lt;b&gt;Arabic&lt;/b&gt; sentences to SparkSQL code, and Azure OpenAI GPT4 handled the translation very well. Look &lt;a href=&quot;https://github.com/mraad/GeoLLM/blob/main/PySparkAI.ipynb&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt; for code snippets. This form of interaction &lt;b&gt;IS&lt;/b&gt; the future, and I am excited to invest more in this technology and in the following areas:&lt;/span&gt;&lt;/p&gt;&lt;ol style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: decimal; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Enhanced Visualization and Real-time Data Integration:&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Dynamic Visualization:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Integrating real-time traffic data feeds into existing models. This will not only show historical congestion but also provide live updates. Dynamic heatmaps can be particularly effective in visualizing the intensity of traffic at different times.&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;3D Modeling:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Utilize ArcGIS&#39;s 3D scene capabilities to give a more immersive view of traffic congestion and urban planning scenarios.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;ol style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: decimal; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Improved Data Analysis through Machine Learning:&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Predictive Analytics:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Integrate machine learning models to predict future traffic patterns based on historical data, weather conditions, events, and other variables.&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Anomaly Detection:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Implement anomaly detection algorithms to identify unusual traffic patterns, which can be crucial for incident response and urban planning.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;ol style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: decimal; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Enhancing User Interaction and Accessibility:&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Multilingual Support:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;While we showcased the translation of Arabic sentences to SparkSQL code, we should consider expanding this feature to include more languages, making your tool more accessible to a global audience.&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Voice Commands and Chatbots:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Integrate voice command functionality and develop a chatbot using Azure OpenAI GPT4 for querying and controlling the GeoAnalytics Engine, making the system more interactive and user-friendly.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;ol style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: decimal; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Scalability and Performance Optimization:&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Optimization for Large Datasets:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Continue to refine the efficiency of processing large datasets. Explore the latest advancements in distributed computing and in-memory processing to handle even larger datasets more efficiently.&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Cloud Integration:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Ensure the solutions are cloud-agnostic and can be deployed on any public or private cloud provider, enhancing the system&#39;s scalability and reliability.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;ol style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: decimal; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Collaboration and Sharing:&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Collaborative Features:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Develop features that allow multiple users to work on the same project simultaneously, including version control and change tracking for shared projects.&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Export and Sharing Options:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Enhance the ability to export results and visualizations in various formats and share them across different platforms, facilitating easier collaboration and reporting.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;ol style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: decimal; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Ethical Considerations and Transparency:&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;ul style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Data Privacy:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Address data privacy concerns by implementing robust data encryption and anonymization techniques, ensuring that individual privacy is respected while analyzing traffic patterns.&lt;/span&gt;&lt;/li&gt;&lt;li class=&quot;ql-indent-1&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; list-style-type: disc; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;strong style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;Algorithm Transparency:&lt;/strong&gt;&lt;span data-preserver-spaces=&quot;true&quot; style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&amp;nbsp;Provide clear documentation and explanations of the algorithms used, promoting transparency and trust in your system.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;p style=&quot;background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-position: initial; background-repeat: initial; background-size: initial; color: #0e101a; margin-bottom: 0pt; margin-top: 0pt;&quot;&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/6524804252522085352/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/6524804252522085352' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/6524804252522085352'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/6524804252522085352'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2024/01/arabic-sdk-for-apache-spark.html' title='Arabic SDK For Apache Spark'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-4654680844677249602</id><published>2024-01-27T14:40:00.000-05:00</published><updated>2026-09-12T17:27:57.626-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Generative AI"/><category scheme="http://www.blogger.com/atom/ns#" term="langchain"/><category scheme="http://www.blogger.com/atom/ns#" term="ollama"/><title type='text'>Back in Action: GenAI Meets GeoSpatial</title><content type='html'>&lt;p&gt;&amp;nbsp;Hello, everyone. It has been a while since my last post, and I wanted to explain my absence. I have been working on demanding client projects requiring confidentiality, so I couldn&#39;t share anything.&lt;/p&gt;&lt;p&gt;But now I&#39;m back and excited to dive into something new and exciting.&amp;nbsp;&lt;/p&gt;&lt;p&gt;Generative AI (GenAI) has gained much attention lately, but I&#39;m taking it to a different level by merging Large Language Models (LLMs) with insights from geospatial analysis. It&#39;s GenAI with a GeoSpatial twist.&lt;/p&gt;&lt;p&gt;I want to introduce a simple project, &quot;&lt;a href=&quot;https://github.com/mraad/GeoLLM&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;ReAct geospatial logic with Ollama&lt;/a&gt;,&quot; which uses resources such as the Python &lt;a href=&quot;https://www.langchain.com/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Langchain&lt;/a&gt; and &lt;a href=&quot;https://ollama.ai/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Ollama&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I&#39;m thrilled to be back and can&#39;t wait to start this new journey with you. Keep an eye on this space for future updates, tips, and unique code snippets. Your feedback and questions are valuable, so please don&#39;t hesitate to reach out.&lt;/p&gt;&lt;p&gt;I&#39;ll see you in the next post, and as usual, you can check out the source code &lt;a href=&quot;https://github.com/mraad/GeoLLM&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/4654680844677249602/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/4654680844677249602' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/4654680844677249602'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/4654680844677249602'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2024/01/back-in-action-genai-meets-geospatial.html' title='Back in Action: GenAI Meets GeoSpatial'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-378498455952580356</id><published>2020-08-24T08:28:00.000-04:00</published><updated>2026-09-12T17:27:59.131-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ArcGIS"/><category scheme="http://www.blogger.com/atom/ns#" term="GeoAI"/><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><category scheme="http://www.blogger.com/atom/ns#" term="Spark"/><title type='text'>On Machine Learning in ArcGIS and Data Preparation using Spark</title><content type='html'>&lt;span style=&quot;font-family: arial;&quot;&gt;Artificial Intelligence / Machine Learning implementations have been part of Esri software in ArcGIS for a very long time. &lt;a href=&quot;https://pro.arcgis.com/en/pro-app/tool-reference/spatial-statistics/geographically-weighted-regression.htm&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Geographic Weighted Regression (GWR)&lt;/a&gt; and &lt;a href=&quot;https://pro.arcgis.com/en/pro-app/tool-reference/spatial-statistics/hot-spot-analysis.htm&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Hot Spot Analysis&lt;/a&gt; &lt;b&gt;are&lt;/b&gt; Machine Learning algorithms. ArcGIS users have been utilizing &lt;a href=&quot;https://en.wikipedia.org/wiki/Supervised_learning&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;supervised&lt;/a&gt; &lt;b&gt;and&lt;/b&gt;&amp;nbsp;&lt;a href=&quot;https://en.wikipedia.org/wiki/Unsupervised_learning&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;unsupervised&lt;/a&gt; learning like &lt;a href=&quot;https://pro.arcgis.com/en/pro-app/tool-reference/spatial-statistics/multivariate-clustering.htm&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;clustering&lt;/a&gt;&amp;nbsp;to solve a myriad of problems and gain geospatial insight from their data.&amp;nbsp; We just did not call these learning algorithms AI/ML back then.&amp;nbsp; Not until the recent popularity of &lt;a href=&quot;https://en.wikipedia.org/wiki/Deep_learning&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;DeepLearning&lt;/a&gt; that finally blossomed from the &lt;a href=&quot;https://en.wikipedia.org/wiki/AI_winter&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;AI Winter&lt;/a&gt;&amp;nbsp;and made AI a household name. And guess what? Esri software has now &lt;a href=&quot;https://pro.arcgis.com/en/pro-app/help/analysis/image-analyst/deep-learning-in-arcgis-pro.htm&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;DeepLearning implementations&lt;/a&gt;!&lt;/span&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;It is important to understand the difference between AI, ML, and DL.&amp;nbsp; I came across this very insightful &lt;a href=&quot;https://www.7wdata.be/big-data/what-is-the-difference-between-ai%EF%BB%BF-machine-learning-and-deep-learning/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;article&lt;/a&gt; that analogizes the relationship to &lt;a href=&quot;https://en.wikipedia.org/wiki/Matryoshka_doll&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Russian dolls&lt;/a&gt;.&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;Typically, collected data is very &quot;dirty&quot;, and a lot of cleaning has to performed on it before processing it through a Machine Learning algorithm.&amp;nbsp; The bigger the data, the harder is the process especially when pruning noisy outliers and anomalies.&amp;nbsp; But then, that could be what you are looking for, outliers and anomalies. That is why &quot;Data Janitor&quot; is a new job title, as a majority of your time when dealing with data is cleaning it!&amp;nbsp; That is why I love to use &lt;a href=&quot;https://spark.apache.org/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Apache Spark&lt;/a&gt; for this task. It can handle BigData very efficiently in a distributed, parallel share-nothing environment, and the usage of the &lt;a href=&quot;https://spark.apache.org/docs/latest/sql-programming-guide.html&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;DataFrame API and SQL&lt;/a&gt; makes the maniplation a breeze. Utilizing the latter two in an interactive environment like a &lt;a href=&quot;https://jupyter.org/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Jupyter&lt;/a&gt; Notebook enables quick cleaning and more importantly insightful explorations.&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;Now the best part, we can have all 3, Jupyter, Spark and ML, in one enviroment; &lt;a href=&quot;https://www.esri.com/en-us/arcgis/products/arcgis-pro/resources&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;ArcGIS Pro&lt;/a&gt;!&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;&lt;a href=&quot;https://github.com/mraad/spark-esri/blob/master/taxi_trips_duration_train.ipynb&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;This&lt;/a&gt; notebook and &lt;a href=&quot;https://github.com/mraad/spark-esri/blob/master/taxi_trips_duration_error.ipynb&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;that&lt;/a&gt; notebook demonstrate the usage of Spark in Notebook in Pro to clean and explore the data, and then prepare it for Machine Learning.&amp;nbsp; We are using the built-in &lt;a href=&quot;https://pro.arcgis.com/en/pro-app/tool-reference/spatial-statistics/forestbasedclassificationregression.htm&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Forest Based Regression&lt;/a&gt; to predict (or attempt to predict) the &lt;a href=&quot;https://www.kaggle.com/c/nyc-taxi-trip-duration&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;trip duration of a New York City taxi&lt;/a&gt; given a pickup and dropoff location.&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0xgyKAYwn235jabKI6olLt0aP4h_gHbH87bGx4UvO6u8rAGhXx11-S8vbl-vGshkv6Tch4bMyW-SAt8GQylTcY8ik01Nbt-vK7SvjVnkiR9kNTUJAuKFplPcK_wSEfxMiQSZe13Jj1Oo/s768/PickupBins200x200.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em; text-align: center;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;580&quot; data-original-width=&quot;768&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0xgyKAYwn235jabKI6olLt0aP4h_gHbH87bGx4UvO6u8rAGhXx11-S8vbl-vGshkv6Tch4bMyW-SAt8GQylTcY8ik01Nbt-vK7SvjVnkiR9kNTUJAuKFplPcK_wSEfxMiQSZe13Jj1Oo/s640/PickupBins200x200.png&quot; width=&quot;640&quot; /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;The last notebook, enables the user to select pickup locations (in the below case, about JFK) and &quot;see&quot; the errors of the model on a map.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8-LFCNF0vEAxkj02AjyuNcjXyZysGGmKG23T6CesDzeEhNgJmSt_RptyAco_Bar309_LxOzmzuidrR1l70Vy6DusRq0yCbIaM3hRKGQLyilhHsJ9AVE-bFSZiXSppylB8OzDpqQQR0VI/s768/TripErrors.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;580&quot; data-original-width=&quot;768&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8-LFCNF0vEAxkj02AjyuNcjXyZysGGmKG23T6CesDzeEhNgJmSt_RptyAco_Bar309_LxOzmzuidrR1l70Vy6DusRq0yCbIaM3hRKGQLyilhHsJ9AVE-bFSZiXSppylB8OzDpqQQR0VI/s640/TripErrors.png&quot; width=&quot;640&quot; /&gt;&lt;/span&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;font-family: arial;&quot;&gt;Some will argue that all this could have been done using &lt;a href=&quot;https://pandas.pydata.org/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Pandas&lt;/a&gt;, and that is true. But that is not the point of this demonstration and this assumes that all the data could be held in memory which is true in this case (1.45) but will fail when you have 100&#39;s of million of rows and you have to process all this data on one machine with limited resources.&amp;nbsp; If you like Pandas, I recommend that you check out &lt;a href=&quot;https://koalas.readthedocs.io/en/latest/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Koalas&lt;/a&gt;.&lt;/span&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/378498455952580356/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/378498455952580356' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/378498455952580356'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/378498455952580356'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2020/08/on-machine-learning-in-arcgis-and-data.html' title='On Machine Learning in ArcGIS and Data Preparation using Spark'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi0xgyKAYwn235jabKI6olLt0aP4h_gHbH87bGx4UvO6u8rAGhXx11-S8vbl-vGshkv6Tch4bMyW-SAt8GQylTcY8ik01Nbt-vK7SvjVnkiR9kNTUJAuKFplPcK_wSEfxMiQSZe13Jj1Oo/s72-c/PickupBins200x200.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-3115456126051289011</id><published>2020-08-03T21:48:00.000-04:00</published><updated>2026-09-12T17:28:00.710-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ArcGIS"/><category scheme="http://www.blogger.com/atom/ns#" term="ArcGIS Pro"/><category scheme="http://www.blogger.com/atom/ns#" term="Jupyter"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="Spark"/><title type='text'>ArcGIS Pro, Jupyter Notebook and Databricks¶</title><content type='html'>&lt;font face=&quot;verdana&quot;&gt;Yet another post in the continuing saga of the usage of &lt;a href=&quot;https://spark.apache.org/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Apache Spark&lt;/a&gt; from a &lt;a href=&quot;https://www.esri.com/arcgis-blog/products/arcgis-pro/analytics/introducing-arcgis-notebooks-in-arcgis-pro/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Jupyter notebook within ArcGIS Pro&lt;/a&gt;.&lt;/font&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;In the &lt;a href=&quot;http://thunderheadxpler.blogspot.com/2020/07/on-arcgis-pro-notebook-and-apache-spark.html&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;previous posts&lt;/a&gt;, the execution was always within the ArcGIS Pro environment on a single machine, albeit taking advantage of all the cores of that machine.&amp;nbsp; Here, we take a different angle, the execution is performed on a remote cluster of machines in the cloud.&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;So, we author the notebook locally, but we execute it remotely.&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;In &lt;a href=&quot;https://github.com/mraad/spark-esri/blob/master/spark_dbconnect.ipynb&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;this&lt;/a&gt; notebook, we demonstrate the spatial binning of AIS broadcast points on a &lt;a href=&quot;https://databricks.com/product/azure&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Databricks cluster on Azure&lt;/a&gt;. In addition, to colocate the data storage with the execution engine for performance purposes, we converted the local feature class of the AIS broadcast points to a &lt;a href=&quot;https://parquet.apache.org/&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;parquet&lt;/a&gt; file and placed it in the &lt;a href=&quot;https://docs.databricks.com/data/databricks-file-system.html&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;Databricks distributed file system&lt;/a&gt;.&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;More to come :-)&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/3115456126051289011/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/3115456126051289011' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3115456126051289011'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/3115456126051289011'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2020/08/arcgis-pro-jupyter-notebook-and.html' title='ArcGIS Pro, Jupyter Notebook and Databricks¶'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-5436394102935036543</id><published>2020-08-02T16:58:00.001-04:00</published><updated>2020-08-02T16:58:20.460-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ArcGIS Pro"/><category scheme="http://www.blogger.com/atom/ns#" term="Jupyter"/><category scheme="http://www.blogger.com/atom/ns#" term="Spark"/><title type='text'>Virtual Gate Crossing</title><content type='html'>&lt;font face=&quot;verdana&quot;&gt;Yet another continuation post regarding Pro, Notebook, and Spark :-). In &lt;a href=&quot;https://github.com/mraad/spark-esri/blob/master/virtual_gates.ipynb&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;this&lt;/a&gt; notebook, we will demonstrate&amp;nbsp;&lt;span style=&quot;background-color: white; text-align: justify;&quot;&gt;a parallel, distributed, share-nothing spatial join between a relatively large dataset and a small dataset.&lt;/span&gt;&lt;/font&gt;&lt;div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;span style=&quot;background-color: white;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;span style=&quot;background-color: white;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;In this case, virtual gates are defined at various locations in a port, and the outcome is an account of the number of crossings of these gates by ships using their AIS target positions.&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;p style=&quot;background-color: white; margin: 1em 0px 0px; padding: 0px;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;Note that the join is to a &quot;small&quot; spatial dataset that we can:&lt;/font&gt;&lt;/p&gt;&lt;ul style=&quot;background-color: white; list-style-image: initial; list-style-position: initial; margin: 1em 2em 0px; padding: 0px; text-align: start;&quot;&gt;&lt;li style=&quot;line-height: 20px; margin: 0px; padding: 0px;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;a href=&quot;https://spark.apache.org/docs/latest/rdd-programming-guide.html#broadcast-variables&quot; rel=&quot;nofollow&quot; style=&quot;color: #0088cc; margin: 0px; padding: 0px;&quot; target=&quot;_blank&quot;&gt;Broadcast&lt;/a&gt;&amp;nbsp;to all the spark workers.&lt;/font&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 20px; margin: 0px; padding: 0px;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;Brutly traverse it on each worker, as it is cheaper and faster to do so that spatially index it.&lt;/font&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: white; text-align: justify;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;span style=&quot;background-color: white;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;The following are sample gates:&lt;/font&gt;&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgxYnFr2AvzQ6OkVkBSxNysKhVsiyikpdVIJsmIpFkfAKBmyqK4nK9hTVNwkMuK6h-oz7boKSlCCF8nmJ5nMz07oyZXYswoAmMVd6ryFjC9N2FP7Wcy1uB_laMwIPP-hhbJsK1fCGPsC5w/s768/Gates1.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;575&quot; data-original-width=&quot;768&quot; height=&quot;383&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgxYnFr2AvzQ6OkVkBSxNysKhVsiyikpdVIJsmIpFkfAKBmyqK4nK9hTVNwkMuK6h-oz7boKSlCCF8nmJ5nMz07oyZXYswoAmMVd6ryFjC9N2FP7Wcy1uB_laMwIPP-hhbJsK1fCGPsC5w/w512-h383/Gates1.png&quot; width=&quot;512&quot; /&gt;&lt;/font&gt;&lt;/a&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;And the following is a sample processed output:&lt;/font&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjX0O-nIeyK7SVxj-gh3LEkOsyJVRFYbmbLkcJcw2K7VdUdLmT37YNhoN6fQx5Ib4JgqCTeppxgRABylCNxKwkSjeBW4WuXX7A0kUfGLbgLw-1liblPW6znaydSy2kebBv7lyFHARQbPyQ/s768/Gates2.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;480&quot; data-original-width=&quot;768&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjX0O-nIeyK7SVxj-gh3LEkOsyJVRFYbmbLkcJcw2K7VdUdLmT37YNhoN6fQx5Ib4JgqCTeppxgRABylCNxKwkSjeBW4WuXX7A0kUfGLbgLw-1liblPW6znaydSy2kebBv7lyFHARQbPyQ/w512-h320/Gates2.png&quot; width=&quot;512&quot; /&gt;&lt;/font&gt;&lt;/a&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;font face=&quot;verdana&quot;&gt;More to come...&lt;/font&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/5436394102935036543/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/5436394102935036543' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/5436394102935036543'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/5436394102935036543'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2020/08/virtual-gate-crossing.html' title='Virtual Gate Crossing'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgxYnFr2AvzQ6OkVkBSxNysKhVsiyikpdVIJsmIpFkfAKBmyqK4nK9hTVNwkMuK6h-oz7boKSlCCF8nmJ5nMz07oyZXYswoAmMVd6ryFjC9N2FP7Wcy1uB_laMwIPP-hhbJsK1fCGPsC5w/s72-w512-h383-c/Gates1.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-6962131783439800471</id><published>2020-08-01T11:52:00.001-04:00</published><updated>2020-08-01T11:56:10.163-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ArcGIS Pro"/><category scheme="http://www.blogger.com/atom/ns#" term="Jupyter"/><category scheme="http://www.blogger.com/atom/ns#" term="Spark"/><title type='text'>MicroPath Reconstruction of AIS Broadcast Points</title><content type='html'>&lt;font face=&quot;helvetica&quot;&gt;This is a continuation of the last post regarding ArcGIS Pro, Jupyter Notebook, and Spark. And, this is a rehash of an &lt;a href=&quot;http://thunderheadxpler.blogspot.com/2018/05/on-patterns-of-life-from-macrodata-to.html&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;older post&lt;/a&gt; in a more &quot;modern&quot; way.&lt;/font&gt;&lt;div&gt;&lt;font face=&quot;helvetica&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;p class=&quot;p1&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;Micropathing is the construction of a target&#39;s path from a limited set of a consecutive sequence of target points. Typically, the sequence is time-based, and the collection is limited to 2 or 3 target points.&lt;span class=&quot;Apple-converted-space&quot;&gt;&amp;nbsp; &lt;/span&gt;The following is an illustration of 2 micropaths derived from 3 target points:&lt;/font&gt;&lt;/p&gt;&lt;p class=&quot;p2&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px; min-height: 14px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ6JGi7a46HwF77b_JFhU0iFHCj9OoYWiWbdTLhZiEPMDIs6waNaOu1S3IrXvYRcinp1eCUYT7lka7wY1GH5N8id9sPtI38i2UdHYiXJlVJ_cnNM1Ag-7nwDezu47DPR7YfNd81pIvU5c/s320/Micropath0.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;162&quot; data-original-width=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ6JGi7a46HwF77b_JFhU0iFHCj9OoYWiWbdTLhZiEPMDIs6waNaOu1S3IrXvYRcinp1eCUYT7lka7wY1GH5N8id9sPtI38i2UdHYiXJlVJ_cnNM1Ag-7nwDezu47DPR7YfNd81pIvU5c/s0/Micropath0.png&quot; /&gt;&lt;/font&gt;&lt;/a&gt;&lt;/div&gt;&lt;p class=&quot;p1&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/p&gt;&lt;p class=&quot;p1&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;Micropathing is different than path reconstruction, in such that the latter produced one polyline for the path of a target. Path reconstruction losses insightful in-path behavior, as a large number of attributes cannot be associated with the path parts. Some can argue that the points along the path can be enriched with these attributes. However, with the current implementations of Point objects, we are limited to only the extra &lt;i&gt;M&lt;/i&gt;&amp;nbsp;and &lt;i&gt;Z&lt;/i&gt; to the necessary &lt;i&gt;X&lt;/i&gt; and &lt;i&gt;Y&lt;/i&gt;. You can also join the &lt;i&gt;PathID&lt;/i&gt; and &lt;i&gt;M&lt;/i&gt; to a lookup table and gain back that insight, but that joining is typically expensive and is difficult to derive from it the &quot;expression&quot; of the path using traditional mapping. A micropath overcomes today&#39;s limitations with today&#39;s traditional means to express the path insight better.&lt;/font&gt;&lt;/p&gt;&lt;p class=&quot;p2&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px; min-height: 14px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/p&gt;&lt;p class=&quot;p1&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;So, a micropath is a line composed of typically 2 points only and is associated with a set of attributes that describe that line.&lt;span class=&quot;Apple-converted-space&quot;&gt;&amp;nbsp; &lt;/span&gt;These attributes are typical enrichment metrics derived from its two ends. An attribute can be, for example, the traveled distance, time, or speed.&lt;/font&gt;&lt;/p&gt;&lt;p class=&quot;p2&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px; min-height: 14px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/p&gt;&lt;p class=&quot;p1&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;In &lt;a href=&quot;https://github.com/mraad/spark-esri/blob/master/micro_path.ipynb&quot; rel=&quot;nofollow&quot; target=&quot;_blank&quot;&gt;this&lt;/a&gt; notebook, we will construct &quot;clean&quot; micropaths using SparkSQL.&lt;span class=&quot;Apple-converted-space&quot;&gt;&amp;nbsp; &lt;/span&gt;What do I mean by clean? As we all know, emitted target points are notoriously affected by noise, so using SparkSQL, we will eliminate that noise during the micropath construction.&lt;/font&gt;&lt;/p&gt;&lt;p class=&quot;p1&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;&lt;br /&gt;&lt;/font&gt;&lt;/p&gt;&lt;p class=&quot;p1&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;Here is a result:&lt;/font&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdEHQvgRrkyFnkI9A9a2XqmC5NUOb0fpxmnWXShyphenhyphenwUdpQ42Zi98W1AUDiaNS9PYkBc63ijbMzAmMVtaHaypwVbzv2nXk92ftItwoncoc87o0BT0vwHhA734h-0v3_tqpd4pTU8n7x3od4/s768/Micropath1.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;480&quot; data-original-width=&quot;768&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdEHQvgRrkyFnkI9A9a2XqmC5NUOb0fpxmnWXShyphenhyphenwUdpQ42Zi98W1AUDiaNS9PYkBc63ijbMzAmMVtaHaypwVbzv2nXk92ftItwoncoc87o0BT0vwHhA734h-0v3_tqpd4pTU8n7x3od4/w512-h320/Micropath1.png&quot; width=&quot;512&quot; /&gt;&lt;/font&gt;&lt;/a&gt;&lt;/div&gt;&lt;p class=&quot;p1&quot; style=&quot;font-stretch: normal; font-variant-east-asian: normal; font-variant-numeric: normal; line-height: normal; margin: 0px;&quot;&gt;&lt;font face=&quot;helvetica&quot;&gt;More to come...&lt;/font&gt;&lt;/p&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/6962131783439800471/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/6962131783439800471' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/6962131783439800471'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/6962131783439800471'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2020/08/micropath-reconstruction-from-ais.html' title='MicroPath Reconstruction of AIS Broadcast Points'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ6JGi7a46HwF77b_JFhU0iFHCj9OoYWiWbdTLhZiEPMDIs6waNaOu1S3IrXvYRcinp1eCUYT7lka7wY1GH5N8id9sPtI38i2UdHYiXJlVJ_cnNM1Ag-7nwDezu47DPR7YfNd81pIvU5c/s72-c/Micropath0.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-1537575203541375292</id><published>2020-07-31T09:47:00.003-04:00</published><updated>2020-07-31T09:52:00.354-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="ArcGIS Pro"/><category scheme="http://www.blogger.com/atom/ns#" term="Jupyter"/><category scheme="http://www.blogger.com/atom/ns#" term="Spark"/><title type='text'>On ArcGIS Pro, Jupyter Notebook and Apache Spark</title><content type='html'>Been a while since I posted something, and thank you, faithful reader, for coming back :-)&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;I&#39;m intending to writing a series of posts on how to use &lt;a href=&quot;https://spark.apache.org/&quot; target=&quot;_blank&quot;&gt;Apache Spark&lt;/a&gt; and Machine Learning within a jupyter notebook &lt;b&gt;within&lt;/b&gt; ArcGIS Pro.&amp;nbsp; Yes, you can now start a jupyter notebook instance in ArcGIS Pro to create an amazing data science and data exploration experience. Check out &lt;a href=&quot;https://pro.arcgis.com/en/pro-app/arcpy/get-started/pro-notebooks.htm&quot; target=&quot;_blank&quot;&gt;this link&lt;/a&gt; to see how to get started with a Jupyter notebook in Pro.&amp;nbsp; But...my favorite hidden &quot;GeoGem&quot;, is that Pro comes with built-in Apache Spark, and y&#39;all know how much I love Spark. People think that Spark is intended for only BigData analytics.&amp;nbsp; That is so far from the truth. What I love about it, is the frictionless movement of data and analysis locally or remotely and the language fusion.&amp;nbsp; In my case, I&#39;m using Python, SQL, and Scala.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;The usage of Apache Spark in Pro was demonstrated in the publically shared &lt;a href=&quot;https://www.esri.com/about/newsroom/blog/introducing-community-contact-tracing/&quot; target=&quot;_blank&quot;&gt;Covid-19 Contact Tracing Application&lt;/a&gt;&amp;nbsp;and the &lt;a href=&quot;https://www.esri.com/arcgis-blog/products/arcgis-pro/health/use-proximity-tracing-to-identify-possible-contact-events/&quot; target=&quot;_blank&quot;&gt;Proximity Tracing Application&lt;/a&gt;.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;In this first &lt;a href=&quot;https://github.com/mraad/spark-esri/blob/master/spark_esri.ipynb&quot; target=&quot;_blank&quot;&gt;notebook&lt;/a&gt;, we will start by loading selected features into a Spark dataframe from a local feature class, process the dataframe using Spark SQL, and write the result back to an ephemeral feature class that will be displayed on the map.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLzWiYWaccCyJt3-QVXyUoViO5JUjKIcIi8y02JAnZeF1Tgu5IazRzegxLrNaqX4W3ESfe4lf4AQktgKKEumNyRFrNKc8XUaxr3CAm_I021tS9Ys5r_kgXV5DpRngZUHXBooZvIDi_BDk/s768/Notebook.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;480&quot; data-original-width=&quot;768&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLzWiYWaccCyJt3-QVXyUoViO5JUjKIcIi8y02JAnZeF1Tgu5IazRzegxLrNaqX4W3ESfe4lf4AQktgKKEumNyRFrNKc8XUaxr3CAm_I021tS9Ys5r_kgXV5DpRngZUHXBooZvIDi_BDk/w512-h320/Notebook.png&quot; width=&quot;512&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Like usual, all the source code can be found &lt;a href=&quot;https://github.com/mraad/spark-esri&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://thunderheadxpler.blogspot.com/feeds/1537575203541375292/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment/fullpage/post/4442573390356643522/1537575203541375292' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/1537575203541375292'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/1537575203541375292'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2020/07/on-arcgis-pro-notebook-and-apache-spark.html' title='On ArcGIS Pro, Jupyter Notebook and Apache Spark'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgLzWiYWaccCyJt3-QVXyUoViO5JUjKIcIi8y02JAnZeF1Tgu5IazRzegxLrNaqX4W3ESfe4lf4AQktgKKEumNyRFrNKc8XUaxr3CAm_I021tS9Ys5r_kgXV5DpRngZUHXBooZvIDi_BDk/s72-w512-h320-c/Notebook.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-1272198304004993885</id><published>2018-05-27T13:31:00.000-04:00</published><updated>2026-09-12T17:28:02.233-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><title type='text'>On Patterns Of Life: From MacroData to PicoData</title><content type='html'>&lt;p&gt;Insight (or GeoInsight in our case) is lost in the deluge of data that we are acquiring today from everyday sensors that are machine or humanly generated. &lt;a href=&quot;https://github.com/mraad/spark-pico-path&quot;&gt;This&lt;/a&gt; project is a set of heuristic &lt;a href=&quot;https://spark.apache.org/&quot;&gt;Spark&lt;/a&gt; based implementations to reveal signals from the movement of ships in and out of the Port of Miami.&lt;/p&gt;&lt;p&gt;The idea is to extract small clean data (PicoData) from the overlap of a massive amount of data (MacroData). The aggregation of &quot;clean&quot; PicoData derived from MacroData trust into the forefront patterns of life.&lt;/p&gt;&lt;p&gt;For example, given the following display of AIS broadcasts:&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoehkFtzYW3BCH0DkwPhZVFLbQXuEO8sXdb7q0C99GQQXV4wkauZrH-9EOAGs3riTlW85JlxRGL3YvAoDsz6aNH68TuJtimyMxkkrbkJJ2MlUTYTPUmC5fcvf3wLP2syJsHIyswnw0F0E/s9999/1527442048.png&quot; alt=&quot;img-alternative-text&quot; style=&quot;max-width: 100%;&quot;&gt;&lt;/div&gt;&lt;p&gt;We can mutate the data to reveal the &quot;clean&quot; influx of ships into the harbor at high tide:&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDPvmC12tYiQBfZ0QzepzhVvIjIkM0sdgafMWY1idjCRb-XCPg54JrfOJe5BsxIlaCrLJEn7iGWZkM_2TAqG9U4zfDzPhbHCrG1DLEvVx8LiFVzxP1M9sW3YF9cVkL9Npg8ky8Zy93b3s/s9999/1527442066.png&quot; alt=&quot;img-alternative-text&quot; style=&quot;max-width: 100%;&quot;&gt;&lt;/div&gt;&lt;p&gt;Like usual, you can download all the source code from &lt;a href=&quot;https://github.com/mraad/spark-pico-path&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/1272198304004993885'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/1272198304004993885'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2018/05/on-patterns-of-life-from-macrodata-to.html' title='On Patterns Of Life: From MacroData to PicoData'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoehkFtzYW3BCH0DkwPhZVFLbQXuEO8sXdb7q0C99GQQXV4wkauZrH-9EOAGs3riTlW85JlxRGL3YvAoDsz6aNH68TuJtimyMxkkrbkJJ2MlUTYTPUmC5fcvf3wLP2syJsHIyswnw0F0E/s72-c/1527442048.png" height="72" width="72"/></entry><entry><id>tag:blogger.com,1999:blog-4442573390356643522.post-595205747816996903</id><published>2018-01-01T11:06:00.000-05:00</published><updated>2026-09-12T17:28:03.860-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Machine Learning"/><title type='text'>On ML and Elastic Principle Graphs</title><content type='html'>&lt;p&gt;Happy 2018 all. It has been a while since my last post. Thank you for your patience dear reader. Like usual, the perpetual resolutions for every year in addition to blogging more are to eat well, often exercise and climb &lt;a href=&quot;https://en.wikipedia.org/wiki/Mont_Ventoux&quot;&gt;Ventoux&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Onward.&lt;/p&gt;&lt;p&gt;I genuinely believe that 2018 will be the year of the ubiquity of Geo-AI. It will be the year when Machine Learning and Spatial Awareness will blossom inside and mostly outside the GIS community.&lt;/p&gt;&lt;p&gt;We at Esri have had Machine Learning based tools in our &quot;shed&quot; for a long time. Every time an ArcGIS user performs a &lt;a href=&quot;http://pro.arcgis.com/en/pro-app/tool-reference/spatial-statistics/geographically-weighted-regression.htm&quot;&gt;graphically weighted regression&lt;/a&gt;, trains a &lt;a href=&quot;http://pro.arcgis.com/en/pro-app/tool-reference/spatial-analyst/train-random-trees-classifier.htm&quot;&gt;random trees classifier&lt;/a&gt; or detects an &lt;a href=&quot;http://pro.arcgis.com/en/pro-app/tool-reference/space-time-pattern-mining/emerginghotspots.htm&quot;&gt;emerging hot spot&lt;/a&gt;, that user is using a form of Machine Learning without knowing it!&lt;/p&gt;&lt;p&gt;So one of my &quot;missions&quot; for 2018, it to make this knowledge more explicit to our users and non-traditional GIS users. Also, start to implement new forms of Machine Learning.&lt;/p&gt;&lt;p&gt;Machine Learning (ML), a branch of Artificial Intelligence (AI), is a disruptive force that is changing how today&#39;s industries are gaining new insight from their data. ML uses math, statistics and probability to find hidden patterns and make predictions from the data without being explicitly programmed. It is this last statement that is disruptive, &quot;No explicit programming&quot;! An ML algorithm iterates &quot;intelligently&quot; over the data, and the patterns emerge. Being iterative, the more data an ML algorithm is exposed to, the more refined the output becomes. Thus the coupling of BigData and ML is a perfect marriage fueled by cheap storage, ever more powerful computational power (think GPU) and faster networking.&lt;/p&gt;&lt;p&gt;This reemergence of this &quot;No Explicit Programming&quot; paradigm such as &lt;a href=&quot;https://en.wikipedia.org/wiki/Deep_learning&quot;&gt;Deep Learning&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/Reinforcement_learning&quot;&gt;Reinforcement Learning&lt;/a&gt;, and &lt;a href=&quot;https://en.wikipedia.org/wiki/Self-organization#In_learning&quot;&gt;Self Organization&lt;/a&gt; is skyrocketing the likes of Google&#39;s &lt;a href=&quot;https://en.wikipedia.org/wiki/AlphaGo_Zero&quot;&gt;AlphaGo-Zero&lt;/a&gt;, Facebook, and Uber.&lt;/p&gt;&lt;p&gt;So, I am starting this launch with something I have been fascinated by for quite some time, and that is &quot;&lt;a href=&quot;https://github.com/auranic/Elastic-principal-graphs/wiki&quot;&gt;Elastic Principle Graphs&lt;/a&gt;.&quot;&lt;/p&gt;&lt;p&gt;It is a &quot;deep&quot; extension of &lt;a href=&quot;https://en.wikipedia.org/wiki/Principal_component_analysis&quot;&gt;PCA&lt;/a&gt; that I came across it during my research of mapping noisy 2D data to a curve and was fascinated by its self-organization.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgACQn2zSQLUx_vDTaoHaRrTUBvh0T4BZ2bFveUdyXPdNV7KveHiWgmi8vS8k44DhCiHylE0rfZkovCjaltX2OL1j3_k-3902YCAEZdlD_q6Ms3RqBaJx3x8cISTuI8u_xbyxsRnXS-1fo/s9999/1514821804.gif&quot; alt=&quot;img-alternative-text&quot; style=&quot;max-width: 100%;&quot;&gt;&lt;/div&gt;&lt;p&gt;After reading, (and rereading for the nth time) &lt;a href=&quot;https://github.com/auranic/Elastic-principal-graphs/blob/master/ElPiGraph_Methods.pdf&quot;&gt;this&lt;/a&gt; paper, &lt;a href=&quot;https://github.com/mraad/elastic-graph&quot;&gt;this&lt;/a&gt; GitHub repo is a minimalist implementation in Scala.&lt;/p&gt;&lt;p&gt;Happy New Year All.&lt;/p&gt;</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/595205747816996903'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/4442573390356643522/posts/default/595205747816996903'/><link rel='alternate' type='text/html' href='http://thunderheadxpler.blogspot.com/2018/01/on-ml-and-elastic-principle-graphs.html' title='On ML and Elastic Principle Graphs'/><author><name>thunderhead</name><uri>http://www.blogger.com/profile/09200852299600047243</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='https://img1.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgACQn2zSQLUx_vDTaoHaRrTUBvh0T4BZ2bFveUdyXPdNV7KveHiWgmi8vS8k44DhCiHylE0rfZkovCjaltX2OL1j3_k-3902YCAEZdlD_q6Ms3RqBaJx3x8cISTuI8u_xbyxsRnXS-1fo/s72-c/1514821804.gif" height="72" width="72"/></entry></feed>