<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mustavi-99.github.io/Mining_research/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mustavi-99.github.io/Mining_research/" rel="alternate" type="text/html" /><updated>2026-10-23T12:30:24-04:00</updated><id>https://mustavi-99.github.io/Mining_research/feed.xml</id><title type="html">Mining Research Log</title><subtitle>A daily research log on open-pit mine production scheduling, MILP, LLM-guided simulation and exploratory drilling.</subtitle><author><name>Mustavi Ibne Masum</name></author><entry><title type="html">Simulator + LLM. Does the shaft help, and why the LLM loses track</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/22/shaft-test-and-live-state/" rel="alternate" type="text/html" title="Simulator + LLM. Does the shaft help, and why the LLM loses track" /><published>2026-10-22T00:00:00-04:00</published><updated>2026-10-22T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/22/shaft-test-and-live-state</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/22/shaft-test-and-live-state/"><![CDATA[<div class="callout">

  <p><strong>Today in short</strong></p>

  <ul>
    <li><strong>The shaft test.</strong> On the six mines where the shaft helped the MILP most, the MILP goes from <strong>82% → 95% → 100%</strong> of full-information NPV (blind → partial → full). The LLM stays at <strong>83% → 81% → 86%</strong>, below greedy (87%) in every row. More information did not help it.</li>
    <li><strong>Why the paper looked closer.</strong> The paper’s 27-block mine is easy for greedy (greedy reaches 93 to 98% of that MILP), the MILP there was solved to a 5% gap, and the simulator has changed since. Today’s mines were built to be hard for greedy.</li>
    <li><strong>What the logs show.</strong> The LLM does use the shaft data, but it <strong>forgets what it has mined</strong> (it keeps only the last 3 calls), <strong>misreads the precedence rule</strong>, and after a rejected reply <strong>falls back to 1 t steps</strong>.</li>
    <li><strong>Fix.</strong> A compact <strong>live mine state</strong> in every prompt, precedence written out, and one line explaining that partial extraction saves nothing.</li>
    <li><strong>Result on decoy.</strong> The nibbling is gone (21 actions instead of 61). Blind and partial still equal greedy, but <strong>full reaches 9,793, 93% of the MILP and 42% above greedy</strong>.</li>
  </ul>

</div>

<h2 id="the-shaft-test">The shaft test</h2>

<p>The question was simple. If the LLM gets the shaft’s estimated grades (partial) or every grade (full), does it get closer to the MILP?</p>

<p>For the test set I used the six mines where the shaft helped the MILP most: waste cover, decoy, three random mines and the 3D flat seam. On the six 27-block 3D mines from the earlier runs the shaft added almost nothing even for the MILP (gains of 0.01 to 11), so they could not show anything.</p>

<p>The shaft data reaches the LLM by coordinates. Each extraction action carries its block’s <code class="language-plaintext highlighter-rouge">(x, y, z)</code>, and the system prompt lists the drilled blocks the same way, for example <code class="language-plaintext highlighter-rouge">(2, 0, 1) ~0.85</code>. In full, the prompt lists every block’s grade instead.</p>

<h3 id="totals-over-the-six-mines--of-full-information-milp">Totals over the six mines (% of full-information MILP)</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th style="text-align: right">Greedy</th>
      <th style="text-align: right">MILP</th>
      <th style="text-align: right">Sim+LLM</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Blind</strong></td>
      <td style="text-align: right">52,840 (87.4%)</td>
      <td style="text-align: right">49,673 (82.1%)</td>
      <td style="text-align: right">50,290 (83.1%)</td>
    </tr>
    <tr>
      <td><strong>Partial</strong></td>
      <td style="text-align: right">52,840 (87.4%)</td>
      <td style="text-align: right">57,697 (95.4%)</td>
      <td style="text-align: right">49,182 (81.3%)</td>
    </tr>
    <tr>
      <td><strong>Full</strong></td>
      <td style="text-align: right">52,840 (87.4%)</td>
      <td style="text-align: right">60,487 (100%)</td>
      <td style="text-align: right">51,814 (85.7%)</td>
    </tr>
  </tbody>
</table>

<p>Greedy is the same in every row because it only ever chooses among blocks it can mine now, and those are always revealed.</p>

<h3 id="per-mine">Per mine</h3>

<table>
  <thead>
    <tr>
      <th>Mine</th>
      <th style="text-align: right">Greedy</th>
      <th>MILP blind / partial / full</th>
      <th>LLM blind / partial / full</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Waste cover</td>
      <td style="text-align: right">7,180</td>
      <td>3,946 / 6,898 / 7,834</td>
      <td>7,176 / 5,687 / 7,020</td>
    </tr>
    <tr>
      <td>Decoy</td>
      <td style="text-align: right">6,899</td>
      <td>8,416 / 10,306 / 10,500</td>
      <td>6,899 / 6,899 / 6,748</td>
    </tr>
    <tr>
      <td>Random seed 6</td>
      <td style="text-align: right">13,876</td>
      <td>13,457 / 14,376 / 14,462</td>
      <td>13,876 / 13,876 / 13,320</td>
    </tr>
    <tr>
      <td>3D flat seam</td>
      <td style="text-align: right">−170</td>
      <td>795 / 1,666 / 1,676</td>
      <td>−2,654 / −1,648 / −170</td>
    </tr>
    <tr>
      <td>Random seed 5</td>
      <td style="text-align: right">11,383</td>
      <td>9,591 / 10,367 / 11,822</td>
      <td>11,320 / 11,394 / 11,270</td>
    </tr>
    <tr>
      <td>Random seed 1</td>
      <td style="text-align: right">13,672</td>
      <td>13,467 / 14,083 / 14,192</td>
      <td>13,672 / 12,975 / 13,625</td>
    </tr>
  </tbody>
</table>

<p>The MILP turns the shaft into 13 points of NPV. The LLM stays at greedy’s level, and in partial it is even slightly worse.</p>

<h2 id="waste-cover-step-by-step">Waste cover, step by step</h2>

<p>Waste cover shows how information can hurt. The surface is all 0.1 grade, with richer ore under it (0.6 at the edges, up to 0.9 in the middle).</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th>Blind (7,176)</th>
      <th>Partial (5,687)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>surface x = 0, 1</td>
      <td>surface x = 2, then x = 0</td>
    </tr>
    <tr>
      <td>2</td>
      <td>finishes x = 1, so the edge ore opens and it processes 33 t</td>
      <td>more surface, processes nothing</td>
    </tr>
    <tr>
      <td>3</td>
      <td>more ore</td>
      <td>the middle ore finally opens, processes 17 t</td>
    </tr>
  </tbody>
</table>

<p>In partial the LLM followed the drill data toward the 0.85 ore in the middle. But a middle block needs three surface blocks removed where an edge block needs two, and it also stripped a block that did not help. Revenue started one period later, which costs more than the higher grade earns back. The blind run took the first block in the list, which happened to be the cheapest way in.</p>

<h2 id="why-the-paper-got-close-to-the-milp">Why the paper got close to the MILP</h2>

<p>My earlier paper (Sim2Schedule) reported the LLM at 94.5% of the MILP. Three things explain the difference.</p>

<table>
  <thead>
    <tr>
      <th>Paper mine (27 blocks, 10 periods)</th>
      <th style="text-align: right">NPV</th>
      <th style="text-align: right">% of paper’s MILP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>MILP (5% gap)</td>
      <td style="text-align: right">20,579</td>
      <td style="text-align: right">100%</td>
    </tr>
    <tr>
      <td>LLM with context</td>
      <td style="text-align: right">19,456</td>
      <td style="text-align: right">94.5%</td>
    </tr>
    <tr>
      <td>Greedy, paper</td>
      <td style="text-align: right">19,062</td>
      <td style="text-align: right">92.6%</td>
    </tr>
    <tr>
      <td>Greedy, today’s simulator</td>
      <td style="text-align: right">20,277</td>
      <td style="text-align: right">98.5%</td>
    </tr>
  </tbody>
</table>

<ul>
  <li><strong>The mine is easy for greedy.</strong> Almost every top-layer grade is rich, so following the immediate value already gets close to the optimum.</li>
  <li><strong>The MILP was solved to a 5% gap</strong>, so the true optimum could be up to 5% higher.</li>
  <li><strong>The simulator changed after the paper</strong> (the greedy rule and proportional processing). Greedy on the same mine went from 19,062 to 20,277.</li>
</ul>

<p>Today’s mines were designed so that greedy fails. On those, an LLM that behaves like greedy is far from the MILP. That matches the paper’s own observation that without context the LLM falls back to greedy-like behaviour.</p>

<h2 id="what-the-logs-show">What the logs show</h2>

<p>I read the waste-cover partial run call by call. The LLM clearly uses the drill data, since every call reasons about reaching the 0.85 block. Then it goes wrong in four ways.</p>

<ol>
  <li><strong>It forgets what it has mined.</strong> In call 1 it mines block 3 fully. In call 3 it chooses block 3 again: <em>“we should extract block 3 first”</em>. The reply is rejected.</li>
  <li><strong>It misreads the precedence rule, differently each time.</strong> Call 2 gets it right. Call 7 decides <em>“each block at z = 1 touches the block directly above at same x”</em>. Call 8 looks for blocks above a surface block.</li>
  <li><strong>After a rejection it takes 1 t.</strong> <em>“we want to minimize amount extracted to reduce cost… choose 1 ton. That is best.”</em> Every extraction shows a loss per tonne, so the smallest amount looks best, even though it only postpones the cost and leaves blocks half-mined.</li>
  <li><strong>It does not know what ore is worth.</strong> <em>“We don’t know processing revenue… maybe processing revenue per ton is high.”</em></li>
</ol>

<h3 id="how-much-it-remembers">How much it remembers</h3>

<p>Each call sends the system prompt, the last 3 prompts with the LLM’s 3 replies, and the new prompt. Anything older is dropped. Retries take history slots too, so after two invalid replies only one real decision is left in view. The model’s own reasoning is never sent back. So “which blocks are done” survived at most three calls.</p>

<p>Two more things in the prompt add to the confusion. <code class="language-plaintext highlighter-rouge">max_extractable_tons</code> drops to the capacity left in the period (for example from 50 to 20 for every block), which the LLM reads as every block shrinking. And the original prompt line <em>“a block is only available for processing after it has been fully extracted”</em> is not true in the current simulator, which processes the mined share of a block straight away.</p>

<h2 id="the-fix-live-mine-state">The fix. Live mine state</h2>

<p>Three changes, as a new prompt version.</p>

<ul>
  <li><strong>Live mine state in every prompt</strong>, rebuilt from the simulator at each step. Only blocks whose grade is known are listed (in blind, unknown blocks are left out, since the mine size is already given). Each entry is <code class="language-plaintext highlighter-rouge">(block, grade, tons left to extract, tons left to process)</code>. For example, in partial, <code class="language-plaintext highlighter-rouge">(12, '~0.87', 50.0, 43)</code>.</li>
  <li><strong>Precedence written out.</strong> In the coordinate version, <em>“a block at (x, y, z) can only be extracted after every block at (x + dx, y + dy, z − 1), with dx and dy each −1, 0 or +1 and inside the mine, has been fully extracted.”</em> I checked that this rule matches the simulator’s precedence for every block on four mines. The block-number version keeps the general sentence.</li>
  <li><strong>One line on partial extraction.</strong> <em>“Extracting part of a block costs the same per tonne as extracting all of it, but only a fully extracted block unlocks the blocks below it. A period ends only when its mining capacity is used, so extracting less now does not avoid the cost.”</em></li>
</ul>

<p>To see whether naming blocks by number or by coordinates matters, I built both versions and ran them on decoy.</p>

<h2 id="decoy-results-block-number-version">Decoy results (block-number version)</h2>

<table>
  <thead>
    <tr>
      <th>Decoy</th>
      <th style="text-align: right">Greedy</th>
      <th style="text-align: right">MILP</th>
      <th>LLM before (coordinates, no state)</th>
      <th>LLM now (block numbers + live state)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Blind</td>
      <td style="text-align: right">6,899</td>
      <td style="text-align: right">8,416</td>
      <td>6,899, 61 actions</td>
      <td>6,899, <strong>21 actions</strong></td>
    </tr>
    <tr>
      <td>Partial</td>
      <td style="text-align: right">6,899</td>
      <td style="text-align: right">10,306</td>
      <td>6,899, 24 actions</td>
      <td>6,899, <strong>17 actions</strong></td>
    </tr>
    <tr>
      <td>Full</td>
      <td style="text-align: right">6,899</td>
      <td style="text-align: right">10,500</td>
      <td>6,748, 22 actions</td>
      <td><strong>9,793, 25 actions</strong></td>
    </tr>
  </tbody>
</table>

<p><strong>The nibbling is gone.</strong> The same mine takes about a third of the actions, with almost no invalid replies.</p>

<p><strong>Full reaches 93% of the MILP.</strong> This is the first time the LLM moved clearly beyond greedy. Its schedule:</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th>What it did</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>mines rich surface blocks 1 and 2, processes 1 (like greedy)</td>
    </tr>
    <tr>
      <td>2</td>
      <td>finishes 2 and <strong>starts stripping waste block 5</strong></td>
    </tr>
    <tr>
      <td>3</td>
      <td>mines 3 and 8, processes 3</td>
    </tr>
    <tr>
      <td>4</td>
      <td><strong>finishes waste blocks 4, 5 and 6, with no revenue this period</strong></td>
    </tr>
    <tr>
      <td>5</td>
      <td><strong>mines block 12 (0.9 ore)</strong> and processes it</td>
    </tr>
    <tr>
      <td>6</td>
      <td>more of 12, plus 8, 10 and 11</td>
    </tr>
  </tbody>
</table>

<p>Block 12 sits under surface blocks 4, 5 and 6. With the true grade in front of it and the live state showing progress, the LLM paid for that waste in periods 2 to 4 to open 0.9 ore in periods 5 and 6. Greedy never makes that trade.</p>

<p><strong>Partial tried the same and failed on geometry.</strong> It went for block 12 too, and even mined part of block 5 in period 1. Then:</p>

<ul>
  <li>it worked out the layers upside down: <em>“top layer is z = 1… block 2’s above block is block 9, so block 2 cannot be extracted until block 9 is fully extracted”</em>,</li>
  <li>it thought only block 5 sits above block 12, when 4, 5 and 6 all have to go,</li>
  <li>it misread the state and decided block 12 was already fully extracted.</li>
</ul>

<p>So in partial the drill data changed its intent, but without block positions it could not work out which blocks to remove. In full, with every block’s grade listed, it got the layout right.</p>

<h2 id="what-i-take-from-today">What I take from today</h2>

<ul>
  <li><strong>On these mines the gap to the MILP is about planning, not information.</strong> The MILP turns a shaft into 13% more NPV because it plans the whole stripping sequence.</li>
  <li><strong>The LLM failed mainly because it lost track of the mine.</strong> Once it can see the live state, it stops nibbling and, with full information, makes a real multi-period trade-off.</li>
  <li><strong>Partial information still needs the geometry to be clear.</strong> Five estimated blocks named only by number were not enough for it to plan the route.</li>
</ul>

<h2 id="next">Next</h2>

<ul>
  <li>Run the <strong>coordinate version</strong> on decoy (blind, partial, full). If partial with coordinates reaches block 12 in time, block naming is what held it back.</li>
  <li><strong>Repeat the full run</strong> with other seeds before treating the 93% as solid.</li>
  <li>Then run the better of the two versions on the other five shaft mines.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="llm" /><category term="simulator" /><category term="prompts" /><category term="shaft" /><category term="partial" /><category term="full" /><category term="greedy" /><category term="milp" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Simulator + LLM. The value fix and five models</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/21/value-fix-and-model-comparison/" rel="alternate" type="text/html" title="Simulator + LLM. The value fix and five models" /><published>2026-10-21T00:00:00-04:00</published><updated>2026-10-21T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/21/value-fix-and-model-comparison</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/21/value-fix-and-model-comparison/"><![CDATA[<div class="callout">

  <p><strong>Today in short</strong></p>

  <ul>
    <li><strong>New prompt (<code class="language-plaintext highlighter-rouge">actions_value</code>).</strong> Same as yesterday’s structure prompt, but each extraction’s value now <strong>includes the revenue of the ore it frees</strong> this period. That is the same number greedy ranks by.</li>
    <li><strong>The waste-cover failure is fixed.</strong> It went from −2,943 yesterday to <strong>2,500 or better for every model</strong>. No model walks past the ore any more.</li>
    <li><strong>Five models, 24 runs.</strong> Three local (gpt-oss 20B, qwen3 14B, deepseek-r1 14B) and two on Groq (gpt-oss-120b, qwen3.8-27b). <strong>20 of 24 runs land within 0.5% of greedy</strong>, 2 are clearly better and 2 clearly worse.</li>
    <li><strong>A bigger model doesn’t help.</strong> gpt-oss-120b made the same mistake as the 20B model on yesterday’s prompt, and with the new prompt it matches greedy like the others.</li>
    <li><strong>The LLM has become greedy.</strong> Given greedy’s own number, it mostly follows it. Beating greedy will need the LLM to plan beyond one step.</li>
  </ul>

</div>

<h2 id="what-changed-in-the-prompt">What changed in the prompt</h2>

<p>Yesterday’s diagnosis was that the value shown for every extraction (<code class="language-plaintext highlighter-rouge">npv_impact_per_unit</code>) was the mining cost alone, <code class="language-plaintext highlighter-rouge">−10 / 1.1^t</code> per tonne, identical for ore and waste. Greedy ranks by a different number, <code class="language-plaintext highlighter-rouge">npv_delta</code>, which adds the discounted revenue of the ore that extraction makes processable this period (within processing capacity and the blend limits).</p>

<p>The new prompt keeps everything from the structure version and changes only that value. The extraction description gets one extra sentence:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Context: This action incurs a mining cost. Its npv_impact_per_unit also includes the revenue of the ore it makes ready to process in the current period.
</code></pre></div></div>

<p>The difference on the first decision of the decoy mine:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>before: (1, 1, 0.8, 50, -9.09), (1, 2, 0.6, 50, -9.09), (1, 3, 0.0, 50, -9.09), ...
after:  (1, 1, 0.8, 50, 63.64), (1, 2, 0.6, 50, 45.45), (1, 3, 0.0, 50, -9.09), ...
</code></pre></div></div>

<p>Ore blocks now look worth mining, and waste still shows the bare cost.</p>

<h2 id="testing-a-bigger-model-first">Testing a bigger model first</h2>

<p>Before changing the prompt, I checked whether a larger model would see past the misleading number on its own. I sent the exact waste-cover decision from yesterday (period 4, ore block 10 next to four waste blocks, all at −6.83) to <strong>gpt-oss-120b</strong> on Groq, three times at medium effort and once at low effort, and to gpt-oss-20b on Groq for comparison.</p>

<p>All five calls chose <strong>waste block 6, amount 1</strong>. The 120B model’s reasoning:</p>

<blockquote>
  <p><em>“block 10 has ore_concentration 0.6 vs 0.1 for others. However npv impact per unit is same −6.83 regardless of concentration… choose smallest amount: 1.”</em></p>
</blockquote>

<p>So the problem was never the model’s size. The model trusts the number it is given. It also explains the 1 t nibbles seen yesterday. With every extraction showing a loss, “take the smallest amount” looks like the sensible choice.</p>

<h2 id="results-actions_value-prompt-full-information">Results (actions_value prompt, full information)</h2>

<table>
  <thead>
    <tr>
      <th>Mine</th>
      <th style="text-align: right">gpt-oss 20B</th>
      <th style="text-align: right">qwen3 14B</th>
      <th style="text-align: right">deepseek-r1 14B</th>
      <th style="text-align: right">gpt-oss-120b (Groq)</th>
      <th style="text-align: right">qwen3.8-27b (Groq)</th>
      <th style="text-align: right">Greedy</th>
      <th style="text-align: right">Full MILP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3D waste cover</td>
      <td style="text-align: right">2,500</td>
      <td style="text-align: right">2,500</td>
      <td style="text-align: right"><strong>2,562</strong></td>
      <td style="text-align: right">2,500</td>
      <td style="text-align: right">2,500</td>
      <td style="text-align: right">2,500</td>
      <td style="text-align: right">3,277</td>
    </tr>
    <tr>
      <td>3D decoy</td>
      <td style="text-align: right">6,598</td>
      <td style="text-align: right">6,598</td>
      <td style="text-align: right">6,598</td>
      <td style="text-align: right">6,598</td>
      <td style="text-align: right">6,598</td>
      <td style="text-align: right">6,598</td>
      <td style="text-align: right">9,074</td>
    </tr>
    <tr>
      <td>3D central cone</td>
      <td style="text-align: right"><strong>2,027</strong></td>
      <td style="text-align: right">898</td>
      <td style="text-align: right">334</td>
      <td style="text-align: right">673</td>
      <td style="text-align: right">898</td>
      <td style="text-align: right">898</td>
      <td style="text-align: right">4,673</td>
    </tr>
    <tr>
      <td>3D central pipe</td>
      <td style="text-align: right">−594</td>
      <td style="text-align: right">−594</td>
      <td style="text-align: right">−594</td>
      <td style="text-align: right">–</td>
      <td style="text-align: right">–</td>
      <td style="text-align: right">−594</td>
      <td style="text-align: right">1,488</td>
    </tr>
    <tr>
      <td>Dipping orebody</td>
      <td style="text-align: right">13,622</td>
      <td style="text-align: right">13,651</td>
      <td style="text-align: right">13,629</td>
      <td style="text-align: right">–</td>
      <td style="text-align: right">–</td>
      <td style="text-align: right">13,651</td>
      <td style="text-align: right">13,681</td>
    </tr>
    <tr>
      <td>3D blend lower limit</td>
      <td style="text-align: right">14,372</td>
      <td style="text-align: right">14,372</td>
      <td style="text-align: right">14,372</td>
      <td style="text-align: right">–</td>
      <td style="text-align: right">–</td>
      <td style="text-align: right">14,372</td>
      <td style="text-align: right">15,329</td>
    </tr>
  </tbody>
</table>

<p>The missing Groq runs hit Groq’s free daily limit (see below).</p>

<ul>
  <li><strong>20 of 24 runs are within 0.5% of greedy</strong>, most of them to the cent.</li>
  <li><strong>2 runs beat greedy.</strong> gpt-oss on central cone (+126%) and deepseek-r1 on waste cover (+2.5%). The central cone run took 80 actions, more than any other, so it needs repeating before I read much into it.</li>
  <li><strong>2 runs fall clearly below greedy</strong>, both on central cone. deepseek-r1 (334, −63%) and gpt-oss-120b (673, −25%).</li>
  <li><strong>No model gets near the MILP</strong> where planning ahead matters. Central cone, central pipe and waste cover all stay at greedy’s level, well below the MILP.</li>
</ul>

<p>For comparison, the same two mines across all of the prompts so far (gpt-oss 20B):</p>

<table>
  <thead>
    <tr>
      <th>Mine</th>
      <th style="text-align: right">Mine state (baseline)</th>
      <th style="text-align: right">Actions + grade</th>
      <th style="text-align: right">+ structure</th>
      <th style="text-align: right">+ value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3D decoy</td>
      <td style="text-align: right">6,576</td>
      <td style="text-align: right">5,524</td>
      <td style="text-align: right">6,582</td>
      <td style="text-align: right"><strong>6,598</strong></td>
    </tr>
    <tr>
      <td>3D waste cover</td>
      <td style="text-align: right">1,773</td>
      <td style="text-align: right">242</td>
      <td style="text-align: right">−2,943</td>
      <td style="text-align: right"><strong>2,500</strong></td>
    </tr>
  </tbody>
</table>

<h2 id="speed">Speed</h2>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Where</th>
      <th style="text-align: right">Seconds per action</th>
      <th style="text-align: right">Actions per run (average)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>gpt-oss 20B</td>
      <td>local</td>
      <td style="text-align: right">11.5</td>
      <td style="text-align: right">66.5</td>
    </tr>
    <tr>
      <td>gpt-oss-120b</td>
      <td>Groq</td>
      <td style="text-align: right">12.1</td>
      <td style="text-align: right">43.3</td>
    </tr>
    <tr>
      <td>qwen3.8-27b</td>
      <td>Groq</td>
      <td style="text-align: right">10.7</td>
      <td style="text-align: right">20.3</td>
    </tr>
    <tr>
      <td>deepseek-r1 14B</td>
      <td>local</td>
      <td style="text-align: right">24.4</td>
      <td style="text-align: right">26.5</td>
    </tr>
    <tr>
      <td>qwen3 14B</td>
      <td>local</td>
      <td style="text-align: right">46.2</td>
      <td style="text-align: right">27.2</td>
    </tr>
  </tbody>
</table>

<ul>
  <li><strong>Groq answers in under a second</strong>, but the free tier allows only 8,000 tokens per minute, so the scheduler has to pace itself. That is why Groq ends up at about the same seconds per action as my GPU.</li>
  <li><strong>gpt-oss takes the most actions</strong> (66 on average, against 20 to 27 for the others), because it still splits blocks into small pieces. The other models more often take a whole block at once.</li>
  <li><strong>qwen3 14B is the slowest</strong> at about 46 seconds per action, because of its long reasoning.</li>
</ul>

<h2 id="groqs-daily-limit">Groq’s daily limit</h2>

<p>The free tier allows <strong>200,000 tokens per model per day</strong>. The gpt-oss-120b run on central pipe used it up partway through, then got one call through roughly every 12.5 minutes. By step 112 it was still extracting <strong>1 t at a time</strong> from the same block, where the local models finished the whole mine in 20 to 30 steps. I stopped it there. On the three mines it didn’t reach, every local model already matches greedy within 0.2%, so the missing runs are unlikely to change the picture.</p>

<h2 id="moving-machines">Moving machines</h2>

<p>My supervisor’s machine went offline today, so I moved to another Ubuntu machine. Everything was in GitHub apart from the run logs. Two small changes made the move painless.</p>

<ul>
  <li><strong>Finished runs are now recognised from <code class="language-plaintext highlighter-rouge">results/</code></strong>, which is in git, instead of from the log files, which are not. The batch picked up exactly where it stopped.</li>
  <li><strong>Gurobi is switched off</strong> for the LLM runs, since this machine has no academic licence. The LLM and greedy only use the simulator, and the MILP numbers come from my earlier results, so nothing is lost. Greedy reproduces the old numbers exactly, which confirms the simulator is unchanged.</li>
</ul>

<h2 id="what-i-take-from-today">What I take from today</h2>

<ul>
  <li><strong>The right number matters more than the right model.</strong> One changed value fixed a failure that neither more prompt text nor a model six times larger could fix.</li>
  <li><strong>Once the LLM is given greedy’s number, it behaves like greedy.</strong> That is a sensible floor, but it is also a ceiling for a one-step decision.</li>
  <li><strong>The mines where the MILP wins</strong> (central cone, central pipe, waste cover) need several steps of planning, such as stripping waste now for ore two periods later. A value that only looks one step ahead can’t show that.</li>
</ul>

<h2 id="next">Next</h2>

<ul>
  <li><strong>Let the simulator take the full amount</strong>, so the LLM only chooses the block. This removes the nibbling and should cut the calls, and the tokens, sharply.</li>
  <li><strong>Plan once per period instead of once per step.</strong> The LLM ranks blocks or sets a target for the period, and the simulator carries it out. That gives it a chance to plan beyond one step, which is what greedy can’t do.</li>
  <li><strong>Repeat the two runs that beat greedy</strong> with other seeds before treating them as real.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="llm" /><category term="simulator" /><category term="prompts" /><category term="greedy" /><category term="models" /><category term="groq" /><category term="ollama" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Simulator + LLM. Baseline and first prompt changes</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/20/llm-baseline-and-prompt-changes/" rel="alternate" type="text/html" title="Simulator + LLM. Baseline and first prompt changes" /><published>2026-10-20T00:00:00-04:00</published><updated>2026-10-20T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/20/llm-baseline-and-prompt-changes</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/20/llm-baseline-and-prompt-changes/"><![CDATA[<div class="callout">

  <p><strong>Today in short</strong></p>

  <ul>
    <li><strong>Baseline (18 runs).</strong> With the current prompt (whole mine state plus action list), the LLM <strong>behaves like greedy but is much slower</strong>. 9 of 18 runs land within 0.5% of greedy, 8 are clearly worse, and only 1 is clearly better. It never comes close to the MILP with the same information.</li>
    <li><strong>More information doesn’t help it.</strong> Full, blind and partial give similar results, and sometimes full is the worst of the three.</li>
    <li><strong>Prompt change 1. Actions only, grade on each extraction.</strong> Each call has about 2.6 times fewer tokens and each decision is <strong>1.7 to 2.9 times faster</strong> (about 10 s instead of 17 to 30 s). But NPV dropped, badly on the waste-cover mine (−90% vs greedy).</li>
    <li><strong>Prompt change 2. Add a short, general description of the mine structure.</strong> The decoy mine recovered fully to greedy level. The waste-cover mine got even worse and finished with a <strong>negative NPV</strong>.</li>
    <li><strong>The cause.</strong> For every extraction, the number the LLM is given (<code class="language-plaintext highlighter-rouge">npv_impact_per_unit</code>) is the <strong>mining cost only</strong>, the same for every block. Greedy ranks by a different number that also includes the revenue of the ore the extraction frees up. The LLM noticed the rich block, then trusted the number and mined waste.</li>
  </ul>

</div>

<h2 id="setup">Setup</h2>

<p>The loop is <strong>simulator → possible actions → LLM picks one → simulator applies it</strong>, repeated until the end. All runs used the six 27-block mines from my MILP experiments, with the same shafts, drilling noise and costs, and greedy reproduces my earlier numbers exactly on all six. The model is <code class="language-plaintext highlighter-rouge">gpt-oss</code> (20B) running locally, at temperature 0 with a fixed seed.</p>

<h2 id="what-the-llm-was-given-in-the-baseline">What the LLM was given in the baseline</h2>

<p>Every call contained:</p>

<ol>
  <li><strong>A system prompt</strong> (the same every call). Its role (“maximize NPV”), what each mine-state entry means <code class="language-plaintext highlighter-rouge">(block, grade, tons to extract, tons to process)</code>, the two action formats and their amount rules, three rules (“process only after full extraction”, “maximize NPV”, “you do not need to track time”), and the JSON reply format.</li>
  <li><strong>The current step.</strong> Last action, current NPV, period number, the <strong>whole mine state</strong> and the <strong>list of possible actions</strong>.</li>
  <li><strong>History.</strong> The last 3 prompts and the LLM’s 3 replies.</li>
</ol>

<p>What it was <strong>never</strong> told: revenue and cost per tonne, capacities, the number of periods, the discount rate, the blend limits, and which blocks sit above which. Its own reasoning says so: <em>“processing revenue per ton? Not given… So we cannot compute NPV.”</em></p>

<h2 id="baseline-results-current-prompt-18-runs">Baseline results (current prompt, 18 runs)</h2>

<table>
  <thead>
    <tr>
      <th>Mine</th>
      <th style="text-align: right">LLM full</th>
      <th style="text-align: right">LLM blind</th>
      <th style="text-align: right">LLM partial</th>
      <th style="text-align: right">Greedy</th>
      <th style="text-align: right">Full MILP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3D blend lower limit</td>
      <td style="text-align: right">14,372</td>
      <td style="text-align: right">14,372</td>
      <td style="text-align: right">14,372</td>
      <td style="text-align: right">14,372</td>
      <td style="text-align: right">15,329</td>
    </tr>
    <tr>
      <td>3D central cone</td>
      <td style="text-align: right">726</td>
      <td style="text-align: right"><strong>2,574</strong></td>
      <td style="text-align: right">717</td>
      <td style="text-align: right">898</td>
      <td style="text-align: right">4,673</td>
    </tr>
    <tr>
      <td>3D central pipe</td>
      <td style="text-align: right">−594</td>
      <td style="text-align: right">−594</td>
      <td style="text-align: right">−594</td>
      <td style="text-align: right">−594</td>
      <td style="text-align: right">1,488</td>
    </tr>
    <tr>
      <td>3D decoy</td>
      <td style="text-align: right">6,576</td>
      <td style="text-align: right">5,546</td>
      <td style="text-align: right">6,598</td>
      <td style="text-align: right">6,598</td>
      <td style="text-align: right">9,074</td>
    </tr>
    <tr>
      <td>3D waste cover</td>
      <td style="text-align: right">1,773</td>
      <td style="text-align: right">1,693</td>
      <td style="text-align: right">1,693</td>
      <td style="text-align: right">2,500</td>
      <td style="text-align: right">3,277</td>
    </tr>
    <tr>
      <td>Dipping orebody</td>
      <td style="text-align: right">13,618</td>
      <td style="text-align: right">12,311</td>
      <td style="text-align: right">12,486</td>
      <td style="text-align: right">13,651</td>
      <td style="text-align: right">13,681</td>
    </tr>
  </tbody>
</table>

<ul>
  <li><strong>It mostly copies greedy.</strong> 9 of 18 runs are within 0.5% of greedy, several to the cent. 8 are clearly worse, by 8.5 to 32%. Only central cone blind is clearly better (+186%), and that run also had the most invalid replies (15), so it needs repeating before I trust it.</li>
  <li><strong>It never beats the MILP with the same information,</strong> except on waste cover blind and partial. That’s because blind MILP is weak there, not because the LLM is strong. It is still 32% below greedy.</li>
  <li><strong>More information doesn’t help.</strong> Central cone gives 726 with full information and 2,574 blind. Waste cover partial is identical to blind even though the shaft shows the rich layer. The LLM seems to decide from the action list and mostly ignore the mine state.</li>
  <li><strong>Speed.</strong> 656 actions took about 3.9 hours, roughly <strong>21 seconds per action</strong>. MILP and greedy take under a second per mine.</li>
</ul>

<h2 id="prompt-change-1-actions-only-with-the-grade-on-each-extraction">Prompt change 1. Actions only, with the grade on each extraction</h2>

<p>Changes from the baseline:</p>

<ul>
  <li><strong>Mine state removed</strong> from every prompt, together with its description in the system prompt.</li>
  <li><strong>Grade added to each extraction action.</strong> <code class="language-plaintext highlighter-rouge">(1, block, ore_concentration, max_tons, npv_impact_per_unit)</code></li>
  <li><strong>Processing rule simplified</strong> to “choose an amount between min and max”. The LLM isn’t told about grade blending, it only needs an amount within the limits.</li>
  <li><strong>Numbers rounded</strong> to 2 decimals (no more <code class="language-plaintext highlighter-rouge">2.239999999999995</code>).</li>
</ul>

<p>A side effect is that full, blind and partial now produce <strong>identical prompts</strong>. A block only appears as an extraction action once the blocks above it are mined, and by then its grade is always known. Hidden blocks and shaft estimates only ever appeared in the mine state. So one run per mine is enough for this comparison.</p>

<h2 id="prompt-change-2-add-the-mine-structure">Prompt change 2. Add the mine structure</h2>

<p>Same as change 1, plus three general lines in the system prompt. The size comes from each mine, and there is no numbering or layout detail.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Mine Structure:
The mine is a 3 x 3 x 3 (x by y by z) block model.
Precedence: a block can only be extracted after every block touching it in the layer directly above has been fully extracted.
Uncertainty: extracting blocks uncovers the blocks beneath them. The grade of a lower block may be higher or lower than the grade of the blocks above it.
</code></pre></div></div>

<h2 id="results-on-two-mines-full">Results on two mines (full)</h2>

<table>
  <thead>
    <tr>
      <th>Mine</th>
      <th>Prompt</th>
      <th style="text-align: right">NPV</th>
      <th style="text-align: right">vs greedy</th>
      <th style="text-align: right">Actions</th>
      <th style="text-align: right">Minutes</th>
      <th style="text-align: right">Seconds per action</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3D decoy</td>
      <td>baseline</td>
      <td style="text-align: right">6,576</td>
      <td style="text-align: right">−0.3%</td>
      <td style="text-align: right">36</td>
      <td style="text-align: right">18.0</td>
      <td style="text-align: right">30.0</td>
    </tr>
    <tr>
      <td>3D decoy</td>
      <td>actions + grade</td>
      <td style="text-align: right">5,524</td>
      <td style="text-align: right">−16.3%</td>
      <td style="text-align: right">45</td>
      <td style="text-align: right">7.9</td>
      <td style="text-align: right">10.5</td>
    </tr>
    <tr>
      <td>3D decoy</td>
      <td>actions + grade + structure</td>
      <td style="text-align: right"><strong>6,582</strong></td>
      <td style="text-align: right"><strong>−0.2%</strong></td>
      <td style="text-align: right">60</td>
      <td style="text-align: right">17.8</td>
      <td style="text-align: right">17.8</td>
    </tr>
    <tr>
      <td>3D waste cover</td>
      <td>baseline</td>
      <td style="text-align: right">1,773</td>
      <td style="text-align: right">−29.1%</td>
      <td style="text-align: right">74</td>
      <td style="text-align: right">21.0</td>
      <td style="text-align: right">17.0</td>
    </tr>
    <tr>
      <td>3D waste cover</td>
      <td>actions + grade</td>
      <td style="text-align: right">242</td>
      <td style="text-align: right">−90.3%</td>
      <td style="text-align: right">36</td>
      <td style="text-align: right">6.0</td>
      <td style="text-align: right">10.0</td>
    </tr>
    <tr>
      <td>3D waste cover</td>
      <td>actions + grade + structure</td>
      <td style="text-align: right"><strong>−2,943</strong></td>
      <td style="text-align: right">−217.7%</td>
      <td style="text-align: right">78</td>
      <td style="text-align: right">31.8</td>
      <td style="text-align: right">24.5</td>
    </tr>
  </tbody>
</table>

<ul>
  <li><strong>Speed.</strong> Removing the mine state cut tokens per call from a median of about 2,700 to about 1,000, and each decision got 1.7 to 2.9 times faster. Adding the structure lines slowed it again, probably because the model reasons more and takes more, smaller steps (60 and 78 actions).</li>
  <li><strong>Decoy.</strong> Three structure lines were enough to replace the whole mine state. Same NPV as the baseline with a much smaller prompt.</li>
  <li><strong>Waste cover.</strong> It got worse with each change.</li>
</ul>

<h2 id="why-the-llm-lost-money-on-waste-cover">Why the LLM lost money on waste cover</h2>

<p>Waste cover has a poor surface (grade 0.1 everywhere) over a rich middle layer. Period by period, against greedy:</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th>Greedy</th>
      <th>LLM (actions + grade + structure)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>blocks 1, 2</td>
      <td>blocks 1, 2 (same)</td>
    </tr>
    <tr>
      <td>2</td>
      <td>2, 3</td>
      <td>2, 3 (same)</td>
    </tr>
    <tr>
      <td>3</td>
      <td>3, 4, 5</td>
      <td>3, 4, 5, plus 1 to 2 t nibbles on 6, 7, 9</td>
    </tr>
    <tr>
      <td>4</td>
      <td>finishes 5, <strong>mines block 10 (grade 0.6)</strong>, processes 30 t</td>
      <td>finishes 5, so <strong>block 10 becomes available</strong>, but mines surface blocks 6, 8, 9</td>
    </tr>
    <tr>
      <td>5</td>
      <td>block 10 and 6, processes 20 t</td>
      <td>blocks 6 and 7, only <strong>1.7 t of block 10</strong></td>
    </tr>
    <tr>
      <td>6</td>
      <td>block 11, processes 40 t</td>
      <td>blocks 7, 8, 9 (all waste)</td>
    </tr>
    <tr>
      <td><strong>NPV</strong></td>
      <td><strong>2,500</strong></td>
      <td><strong>−2,943</strong></td>
    </tr>
  </tbody>
</table>

<p>So the LLM stripped exactly the right blocks to reach the ore, the same as greedy. Then it walked past the ore. In period 4 it was offered:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(1, 6, 0.1, 21.0, -6.83), (1, 7, 0.1, 21.0, -6.83), (1, 8, 0.1, 21.0, -6.83), (1, 9, 0.1, 21.0, -6.83), (1, 10, 0.6, 21.0, -6.83)
</code></pre></div></div>

<p>Its reasoning, word for word:</p>

<blockquote>
  <p><em>“All have same cost per unit. But block 10 has ore_concentration 0.6, but cost same? … extraction cost is negative NPV per unit, independent of ore concentration. So choose any. But maybe we want to extract block 10 because it might unlock processing later? But we don’t have processing actions yet. So choose block 6.”</em></p>
</blockquote>

<p>It saw the higher grade and even guessed that block 10 might lead to processing. Then it trusted the number. With a tie, it took the first block in the list, which was waste. No processing action ever appeared to correct it, because 0.1-grade ore is below the 0.4 blend limit and can’t be processed on its own.</p>

<h2 id="the-real-problem-two-different-values">The real problem. Two different “values”</h2>

<p>The LLM and greedy rank actions by different numbers.</p>

<ul>
  <li><strong>What the LLM sees</strong> (<code class="language-plaintext highlighter-rouge">npv_impact_per_unit</code>). For an extraction it is only the discounted mining cost, <code class="language-plaintext highlighter-rouge">−10 / 1.1^t</code> per tonne. In period 4 that’s <strong>−6.83 for every block</strong>, waste or ore.</li>
  <li><strong>What greedy uses</strong> (<code class="language-plaintext highlighter-rouge">npv_delta</code>). The same mining cost <strong>plus</strong> the discounted revenue of the ore that extraction makes processable this period, if processing capacity and the blend limits allow.</li>
</ul>

<p>For the period 4 choice above (approximate, worked out from the code):</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th style="text-align: right">Block 6 (grade 0.1)</th>
      <th style="text-align: right">Block 10 (grade 0.6)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>What the LLM saw per tonne</td>
      <td style="text-align: right">−6.83</td>
      <td style="text-align: right">−6.83</td>
    </tr>
    <tr>
      <td>Mining cost of 21 t</td>
      <td style="text-align: right">−143</td>
      <td style="text-align: right">−143</td>
    </tr>
    <tr>
      <td>Ore freed</td>
      <td style="text-align: right">2.1 t, below blend limit</td>
      <td style="text-align: right">12.6 t, processable</td>
    </tr>
    <tr>
      <td>Revenue added</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">about +861</td>
    </tr>
    <tr>
      <td><strong>What greedy saw</strong></td>
      <td style="text-align: right"><strong>about −143</strong></td>
      <td style="text-align: right"><strong>about +717</strong></td>
    </tr>
  </tbody>
</table>

<p>For processing actions the two numbers are effectively the same, which explains why the LLM matches greedy so closely on mines where processing drives the choices. It also means the LLM’s greedy fallback, which ranks by <code class="language-plaintext highlighter-rouge">npv_impact_per_unit</code>, can’t tell ore from waste either.</p>

<h2 id="what-i-take-from-today">What I take from today</h2>

<ul>
  <li><strong>A smaller prompt is much faster</strong> and loses nothing when the obvious move is the right one (decoy).</li>
  <li><strong>Describing the structure isn’t enough</strong> when the LLM has to choose between blocks that only differ in what they lead to. The number it trusts says they’re all the same.</li>
  <li><strong>The LLM isn’t confused about mining. It is misled by the value it is given.</strong> Fixing that number is more important than adding more text.</li>
</ul>

<h2 id="next">Next</h2>

<ul>
  <li><strong>New prompt version.</strong> Actions only, with each extraction’s value including the ore it frees up (the same <code class="language-plaintext highlighter-rouge">npv_delta</code> greedy uses) instead of the flat mining cost. Then rerun decoy and waste cover.</li>
  <li><strong>Let the simulator pick the amount</strong> so the LLM only chooses the block. This stops the 1 t nibbles and cuts the number of calls.</li>
  <li><strong>Soften the Uncertainty line</strong> to a plain fact, so it doesn’t read as an instruction to keep digging.</li>
  <li><strong>Compare models.</strong> Try <code class="language-plaintext highlighter-rouge">qwen3:14b</code> and <code class="language-plaintext highlighter-rouge">llama3.1:8b</code> locally, and possibly <code class="language-plaintext highlighter-rouge">gpt-oss-120b</code> through Groq, to see whether a bigger model makes better use of the same information.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="llm" /><category term="simulator" /><category term="prompts" /><category term="greedy" /><category term="baseline" /><category term="waste-cover" /><category term="decoy" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Next phase. Simulator + LLM</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/19/next-phase-simulator-and-llm/" rel="alternate" type="text/html" title="Next phase. Simulator + LLM" /><published>2026-10-19T00:00:00-04:00</published><updated>2026-10-19T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/19/next-phase-simulator-and-llm</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/19/next-phase-simulator-and-llm/"><![CDATA[<div class="callout">

  <p><strong>Today in short</strong></p>

  <ul>
    <li>The MILP, greedy, blind, partial and full-information experiments are done. Next is <strong>simulator + LLM</strong>.</li>
    <li>The loop is simple. <strong>The simulator lists the possible actions, the LLM picks one, the simulator applies it and updates the mine</strong>, and the cycle repeats. All the rules (capacity, slopes, grades) are handled by the simulator, so the LLM only sees what it’s allowed to do.</li>
    <li>On a 1,000-block mine, describing the whole mine to the LLM is impossible, so <strong>only blind and partial</strong> will be run with the LLM at large scale.</li>
    <li><strong>Speed is the main problem.</strong> At the current rate one large run would take about 2.5 to 3 hours. The work now is cutting the number of calls and the time per call.</li>
    <li><strong>First step is a baseline.</strong> The current LLM setup runs on the six 27-block mines my MILP results came from, in full, blind and partial.</li>
    <li>One open question matters most. With only the action list, <strong>the LLM can’t tell one extraction from another</strong>, so it needs some extra information on each action.</li>
  </ul>

</div>

<h2 id="how-the-loop-works">How the loop works</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>simulator → list of possible actions → LLM picks one → simulator applies it → updated mine → ...
</code></pre></div></div>

<p>The simulator gives an answer instantly, so almost all of the run time is the LLM. Sending only the available actions, with no rules or full mine description, keeps the prompt short and focused on the choice that actually has to be made. On a 10 × 10 × 10 mine that’s the only option anyway, since the full mine state would not fit. That’s also why the full-information case is left to the MILP.</p>

<h2 id="fixes-to-make-before-running">Fixes to make before running</h2>

<ol>
  <li><strong>The “actions only” design isn’t in the code yet.</strong> The no-context and partial-context notebooks are still identical to the original, and all three send the full mine state in every prompt.</li>
  <li><strong>The full mine state wouldn’t fit anyway.</strong> On a 27-block mine the first prompt was already about 956 tokens. At 1,000 blocks the state alone would be far beyond the 8,192-token context limit, and the model would silently lose part of the prompt.</li>
  <li><strong>With actions only, every extraction looks the same.</strong> An extraction action only carries its mining cost, not the block’s grade, so all extractions show the same number. In blind mode the LLM would effectively be choosing at random. In partial mode the shaft estimates would never reach it. The fix is to add a few facts to each action, such as the estimated grade, how uncertain that estimate is, the layer, and how many blocks it would unlock.</li>
  <li><strong>Use the same visibility rules as the MILP runs.</strong> The LLM code currently uses a different drilling model from the one in my MILP experiments. It has to use the same shafts and the same “surface plus what’s been revealed” rule, or the comparison isn’t fair.</li>
  <li><strong>Blind can get stuck.</strong> On the waste-cover and two-pockets mines with no minimum, blind MILP scored 0 because nothing visible looked worth mining. The LLM will face the same trap, so I need to decide up front whether to keep a minimum mining rule that forces it to dig.</li>
</ol>

<h2 id="speed-what-the-earlier-run-shows">Speed. What the earlier run shows</h2>

<p>From the saved 27-block LLM run:</p>

<ul>
  <li>58 actions at <strong>11.4 seconds per call</strong> on average, 11 minutes in total, finishing <strong>1.7% below greedy</strong>.</li>
  <li>Call time follows how long the model “thinks”. A short reasoning took 3.5 s, and a long one took 22.4 s.</li>
</ul>

<p>The 1,000-block runs need about <strong>835 to 924 actions</strong>. At 11.4 s each that’s <strong>2.5 to 3 hours per run</strong>, against 70 to 90 seconds for the rolling MILP on the same mine. A full sweep would take well over 150 GPU hours.</p>

<h2 id="design-choices-to-settle">Design choices to settle</h2>

<table>
  <thead>
    <tr>
      <th>Choice</th>
      <th>Faster option</th>
      <th>What it costs</th>
      <th>Evidence</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Who picks the amount</strong></td>
      <td>The simulator always takes the largest allowed amount, and the LLM only picks which action</td>
      <td>The LLM can’t choose partial amounts</td>
      <td>The LLM wasted calls on tiny amounts, such as 1 t. Greedy always takes the maximum.</td>
    </tr>
    <tr>
      <td><strong>Processing after extraction</strong></td>
      <td>The simulator processes ore straight after it’s mined</td>
      <td>The LLM no longer decides when ore is processed</td>
      <td>Almost every extraction in the log was immediately followed by processing the same block, so this should roughly halve the calls</td>
    </tr>
    <tr>
      <td><strong>How much the model thinks</strong></td>
      <td>Low reasoning effort</td>
      <td>Possibly weaker choices</td>
      <td>Thinking length drives call time. A quick probe on the first decision can test this before the full sweep.</td>
    </tr>
    <tr>
      <td><strong>Memory of past steps</strong></td>
      <td>None, since the simulator already holds the state</td>
      <td>Less continuity between steps (a one-line “your plan” note can restore it)</td>
      <td>Remembering 3 past steps grew the prompt from about 956 to about 3,000 tokens</td>
    </tr>
    <tr>
      <td><strong>Answer format</strong></td>
      <td>A fixed format that only allows a valid action number</td>
      <td>Might change how the model reasons, so check with the probe</td>
      <td>3 of 58 replies were invalid (about 5%). At 900 steps that’s about 45 retries of around 11 s each.</td>
    </tr>
    <tr>
      <td><strong>Facts on each action</strong></td>
      <td>Estimated grade, uncertainty, layer, blocks unlocked</td>
      <td>A few more words per action</td>
      <td>Without them, blind and partial have nothing to rank extractions by</td>
    </tr>
    <tr>
      <td><strong>Serving the model</strong></td>
      <td>A server that handles many requests at once, running several mines in parallel</td>
      <td>More setup than the current tool</td>
      <td>The current setup answers one request at a time, so the GPU sits idle between calls</td>
    </tr>
  </tbody>
</table>

<h3 id="other-things-worth-doing">Other things worth doing</h3>

<ul>
  <li><strong>Show the LLM only the top few actions.</strong> The simulator can rank the possible actions and send the best 5 to 20, with simple facts on each. A one-step look-ahead from the simulator, such as how many blocks an action unlocks, gives the LLM something greedy doesn’t consider.</li>
  <li><strong>Make runs repeatable.</strong> Use a fixed random seed and no randomness in the model’s answers.</li>
  <li><strong>Fall back safely.</strong> If the LLM returns an invalid action, the simulator rejects it and uses the greedy choice instead. How often that happens is a result worth reporting.</li>
  <li><strong>Keep the comparison fair.</strong> The LLM must see exactly what MILP and greedy saw at the same information level. In particular, partial runs must not leak true grades through the action facts.</li>
  <li><strong>Check where any gain comes from.</strong> Also run greedy and a random pick on the same short list. If the LLM only matches those, the gain came from the simulator’s ranking, not the LLM.</li>
</ul>

<h3 id="the-main-risk">The main risk</h3>

<p>An LLM choosing one action at a time only sees the present, much like greedy. If it does nothing more, it may end up <strong>equal to greedy but much slower</strong>. The look-ahead facts on each action, or letting the LLM set a strategy for each period while a fast rule picks the blocks, are the most likely ways for it to beat greedy.</p>

<h2 id="first-step-a-baseline-on-the-small-mines">First step. A baseline on the small mines</h2>

<p>Before touching the large mines, the LLM is run on the same small mines my MILP results came from. That gives a fair starting point, with the current LLM setup exactly as it is.</p>

<ul>
  <li><strong>Six 27-block mines</strong> (3 × 3 × 3) from my earlier configs: 3D decoy, 3D central pipe, 3D blend lower limit, 3D central cone, 3D waste cover and dipping orebody.</li>
  <li><strong>Each in full, blind and partial</strong>, so 18 LLM runs. Here the mine is small enough that the full-information case fits in the prompt.</li>
  <li><strong>Same mines, same shaft, same drilling noise and cost</strong> as the MILP runs. As a check, greedy reproduces my earlier spreadsheet numbers exactly on all six, so the instances match.</li>
  <li>Prompts are about 500 tokens, well within the model’s limit.</li>
  <li>At about 11 seconds per call, each run takes roughly 10 to 30 minutes, so a few hours for all 18. Each result is printed next to my existing MILP and greedy numbers for the same mine.</li>
</ul>

<h2 id="then-the-first-large-test">Then the first large test</h2>

<p>Once the baseline is in, try one large mine and one setting before any sweep.</p>

<ul>
  <li><strong>Settings.</strong> The simulator picks the amount, processing happens automatically, the LLM returns only an action number, low reasoning effort, no step memory, a fixed answer format, and grade estimates on each action.</li>
  <li><strong>Mine.</strong> Opposite gradient, no minimum, in both blind and partial.</li>
  <li><strong>What would count as success.</strong> Landing between greedy (509,329) and the rolling blind MILP (672,628), at a few minutes per run. If it does, scale up to the full set of mines.</li>
</ul>

<h2 id="what-to-report-for-each-run">What to report for each run</h2>

<p>NPV gap to the MILP, total run time, number of LLM calls, words (tokens) used per decision, and how often the LLM gave an invalid answer. All of these for both blind and partial.</p>

<h2 id="next">Next</h2>

<ul>
  <li>Run the 18-run baseline on the 27-block mines and compare with the MILP and greedy results.</li>
  <li>Fix the prompt so it sends only the available actions, with the extra facts on each.</li>
  <li>Switch the LLM code to the same shaft and visibility rules as the MILP runs.</li>
  <li>Decide on the minimum mining rule for blind.</li>
  <li>Run the first test on the opposite-gradient mine.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="llm" /><category term="simulator" /><category term="blind" /><category term="partial" /><category term="planning" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Scaling up. Five 1,000-block mines</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/16/scaling-up-five-large-mines/" rel="alternate" type="text/html" title="Scaling up. Five 1,000-block mines" /><published>2026-10-16T00:00:00-04:00</published><updated>2026-10-16T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/16/scaling-up-five-large-mines</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/16/scaling-up-five-large-mines/"><![CDATA[<div class="callout">

  <p><strong>Today in short</strong></p>

  <ul>
    <li>I set up <strong>five large mines</strong> (10 × 10 × 10 = 1,000 blocks, 6 periods), one for each of my main small-mine designs, and ran every scenario on all of them. That’s 30 partial runs plus blind, full MILP and greedy.</li>
    <li><strong>At this size blind is already strong.</strong> The re-planning blind MILP reaches about 95 to 98% of perfect information on most mines, so a shaft has little left to add. Gains are mostly 0 to 2,600, against drill costs of 575 to 1,813.</li>
    <li><strong>Greedy falls well behind everywhere</strong>, at about 68 to 82% of full information.</li>
    <li><strong>One result stands out.</strong> On the dipping orebody with a minimum, two shafts did 35,000 to 53,000 <em>worse</em> than blind. The cause is the minimum mining rule, not the information the shafts gave.</li>
  </ul>

</div>

<h2 id="the-five-mines">The five mines</h2>

<p>Each mine is 10 × 10 × 10 (1,000 blocks) over 6 periods, with the same economics: 4,000 t of mining and 2,000 t of processing per period, and drilling at 125 per block plus 10% per layer of depth. The surface is kept poor everywhere, so information always has some value. Each mine was run with no minimum and with a <strong>1,200 t minimum</strong>.</p>

<table>
  <thead>
    <tr>
      <th>Mine</th>
      <th>Idea</th>
      <th style="text-align: right">Ore blocks</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Opposite gradient</strong></td>
      <td>Rich surface on one side, rich ore underneath the other</td>
      <td style="text-align: right">327</td>
    </tr>
    <tr>
      <td><strong>Decoy</strong></td>
      <td>Rich surface patch over waste, real ore elsewhere</td>
      <td style="text-align: right">129</td>
    </tr>
    <tr>
      <td><strong>Waste cover</strong></td>
      <td>Poor surface everywhere, rich layers 1 to 3 underneath</td>
      <td style="text-align: right">300</td>
    </tr>
    <tr>
      <td><strong>Dipping orebody</strong></td>
      <td>Ore near the surface on one side, dipping deeper toward the other</td>
      <td style="text-align: right">206</td>
    </tr>
    <tr>
      <td><strong>Two hidden pockets</strong></td>
      <td>Rich pockets under two opposite corners</td>
      <td style="text-align: right">90</td>
    </tr>
  </tbody>
</table>

<p>The same three shafts are used on every mine so the results compare directly.</p>

<table>
  <thead>
    <tr>
      <th>Shaft</th>
      <th>Path</th>
      <th style="text-align: right">Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Diagonal</td>
      <td>Block 1 → 1000, all 10 layers</td>
      <td style="text-align: right">1,812.5</td>
    </tr>
    <tr>
      <td>Shallow</td>
      <td>Block 23 → 356, 4 layers</td>
      <td style="text-align: right">575</td>
    </tr>
    <tr>
      <td>Vertical</td>
      <td>Block 24 → 924, all 10 layers</td>
      <td style="text-align: right">1,812.5</td>
    </tr>
  </tbody>
</table>

<p>The shafts don’t always hit ore. The shallow shaft finds nothing on the dipping and two-pockets mines, and on the dipping mine only the diagonal reaches the deep ore. I kept that on purpose, since a badly placed shaft that is pure cost is a useful case for the thesis.</p>

<p><strong>One run crashed</strong> (two pockets, shallow shaft, no minimum). That shaft passes only through waste, so the planner correctly mined nothing, and the notebook failed when saving an empty schedule. I fixed the empty case in the code and reran that one mine. The result is NPV 0, the same as blind.</p>

<h2 id="results">Results</h2>

<p>NPV, rounded to the nearest unit. Bold marks the best partial result in each row.</p>

<table>
  <thead>
    <tr>
      <th>Mine</th>
      <th>Minimum</th>
      <th style="text-align: right">Blind</th>
      <th style="text-align: right">Diagonal</th>
      <th style="text-align: right">Shallow</th>
      <th style="text-align: right">Vertical</th>
      <th style="text-align: right">Full information</th>
      <th style="text-align: right">Greedy</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Opposite</td>
      <td>0</td>
      <td style="text-align: right">672,628</td>
      <td style="text-align: right"><strong>675,276</strong></td>
      <td style="text-align: right">672,778</td>
      <td style="text-align: right">674,010</td>
      <td style="text-align: right">692,556</td>
      <td style="text-align: right">509,329</td>
    </tr>
    <tr>
      <td>Opposite</td>
      <td>1,200 t</td>
      <td style="text-align: right">674,534</td>
      <td style="text-align: right"><strong>674,629</strong></td>
      <td style="text-align: right">674,053</td>
      <td style="text-align: right">674,346</td>
      <td style="text-align: right">693,342</td>
      <td style="text-align: right">509,329</td>
    </tr>
    <tr>
      <td>Decoy</td>
      <td>0</td>
      <td style="text-align: right">275,237</td>
      <td style="text-align: right">275,237</td>
      <td style="text-align: right">275,237</td>
      <td style="text-align: right">275,237</td>
      <td style="text-align: right">280,179</td>
      <td style="text-align: right">212,200</td>
    </tr>
    <tr>
      <td>Decoy</td>
      <td>1,200 t</td>
      <td style="text-align: right">268,447</td>
      <td style="text-align: right">268,464</td>
      <td style="text-align: right">269,162</td>
      <td style="text-align: right"><strong>269,661</strong></td>
      <td style="text-align: right">278,723</td>
      <td style="text-align: right">212,200</td>
    </tr>
    <tr>
      <td>Waste cover</td>
      <td>0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right"><strong>651,653</strong></td>
      <td style="text-align: right">650,056</td>
      <td style="text-align: right">649,983</td>
      <td style="text-align: right">672,854</td>
      <td style="text-align: right">548,221</td>
    </tr>
    <tr>
      <td>Waste cover</td>
      <td>1,200 t</td>
      <td style="text-align: right">650,223</td>
      <td style="text-align: right">649,959</td>
      <td style="text-align: right">650,154</td>
      <td style="text-align: right"><strong>650,624</strong></td>
      <td style="text-align: right">672,850</td>
      <td style="text-align: right">548,221</td>
    </tr>
    <tr>
      <td>Dipping</td>
      <td>0</td>
      <td style="text-align: right">283,839</td>
      <td style="text-align: right">283,393</td>
      <td style="text-align: right"><strong>284,085</strong></td>
      <td style="text-align: right">284,043</td>
      <td style="text-align: right">321,184</td>
      <td style="text-align: right">220,624</td>
    </tr>
    <tr>
      <td>Dipping</td>
      <td>1,200 t</td>
      <td style="text-align: right">224,141</td>
      <td style="text-align: right">171,383</td>
      <td style="text-align: right">189,800</td>
      <td style="text-align: right"><strong>229,774</strong></td>
      <td style="text-align: right">321,196</td>
      <td style="text-align: right">220,624</td>
    </tr>
    <tr>
      <td>Two pockets</td>
      <td>0</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right"><strong>333,149</strong></td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">332,739</td>
      <td style="text-align: right">351,292</td>
      <td style="text-align: right">239,133</td>
    </tr>
    <tr>
      <td>Two pockets</td>
      <td>1,200 t</td>
      <td style="text-align: right">332,668</td>
      <td style="text-align: right">333,162</td>
      <td style="text-align: right">334,646</td>
      <td style="text-align: right"><strong>334,783</strong></td>
      <td style="text-align: right">350,409</td>
      <td style="text-align: right">239,133</td>
    </tr>
  </tbody>
</table>

<h3 id="what-the-table-says">What the table says</h3>

<p><strong>1. Blind is already close to perfect on large mines.</strong> On most mines blind reaches about 95 to 98% of full information (dipping is lower, about 88%). With that little gap left, shafts gain only 0 to about 2,600, and once drill cost is taken off, most are a small net loss. The clear net wins are vertical on dipping with the minimum (+3,820), shallow on two pockets with the minimum (+1,403) and diagonal on opposite with no minimum (+835).</p>

<p><strong>2. Rows where blind is 0 aren’t really wins for the shaft.</strong> On waste cover and two pockets with no minimum, the whole surface is waste, so blind sees nothing worth digging and never starts. The huge “gain” there just means the shaft persuaded the planner to dig at all. Greedy, with no shaft, gets 548k and 239k on those mines. These rows are better compared with greedy, or reported separately.</p>

<p><strong>3. Greedy is far behind everywhere</strong>, at about 68 to 82% of full information. It mines at full speed from the best-looking surface blocks, which is exactly what these layouts punish. Its result doesn’t change with the minimum, because it always mines at full capacity, so 1,200 t never binds.</p>

<p><strong>4. Two small oddities that are fine.</strong> The full MILP is sometimes slightly higher <em>with</em> the minimum (693,342 against 692,556 on opposite). Adding a rule can’t truly raise the optimum, but both are within the solver’s 0.1 to 0.4% tolerance.</p>

<h2 id="the-dipping-orebody-the-minimum-not-the-shaft">The dipping orebody. The minimum, not the shaft</h2>

<p>On the dipping mine with the 1,200 t minimum, the diagonal shaft finished 52,758 <strong>below</strong> blind (only 53% of full information) and shallow 34,341 below. Nowhere else does information hurt by more than about 2,000. Adding up each period’s mining shows why.</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th style="text-align: right">Blind</th>
      <th style="text-align: right">Vertical</th>
      <th style="text-align: right">Diagonal</th>
      <th style="text-align: right">Shallow</th>
      <th style="text-align: right">Any shaft, no minimum</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>P1 mined</td>
      <td style="text-align: right"><strong>1,200 t</strong></td>
      <td style="text-align: right"><strong>1,200 t</strong></td>
      <td style="text-align: right"><strong>1,200 t</strong></td>
      <td style="text-align: right"><strong>1,202 t</strong></td>
      <td style="text-align: right">4,000 t</td>
    </tr>
    <tr>
      <td>P2 mined</td>
      <td style="text-align: right">4,000 t</td>
      <td style="text-align: right">4,000 t</td>
      <td style="text-align: right"><strong>1,303 t</strong></td>
      <td style="text-align: right"><strong>1,200 t</strong></td>
      <td style="text-align: right">4,000 t</td>
    </tr>
    <tr>
      <td>P3 to P6 mined</td>
      <td style="text-align: right">4,000 t each</td>
      <td style="text-align: right">4,000 t each</td>
      <td style="text-align: right">4,000 t each</td>
      <td style="text-align: right">4,000 t each</td>
      <td style="text-align: right">4,000 t each</td>
    </tr>
    <tr>
      <td>NPV after P2</td>
      <td style="text-align: right">46,581</td>
      <td style="text-align: right">42,209</td>
      <td style="text-align: right">12,361</td>
      <td style="text-align: right">9,034</td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td>Final NPV</td>
      <td style="text-align: right">224,141</td>
      <td style="text-align: right">229,774</td>
      <td style="text-align: right">171,383</td>
      <td style="text-align: right">189,800</td>
      <td style="text-align: right">about 284,000</td>
    </tr>
  </tbody>
</table>

<p><strong>Period 1 is the minimum’s fault.</strong> With the minimum, every run, blind included, mines <em>exactly</em> 1,200 t in period 1 even though it could mine 4,000 t. Almost the whole surface is waste and everything underneath is hidden, so the planner values it at nothing. Because the minimum applies to every period of its plan, it reasons, “I must mine 1,200 t in each of the next periods, and the only rock I can see is this surface waste, so I’ll spread it out and pay later, when it’s cheaper after discounting.” It can’t see that stripping the surface is what uncovers the orebody. This is the same <strong>waste rationing</strong> I found on the small lens mine. Even for blind it costs about 60,000 on this mine compared with no minimum.</p>

<p><strong>Period 2 is where two shafts make it worse.</strong> Blind and vertical stop rationing in period 2 and mine at full rate. Diagonal and shallow ration for one more period. Their estimates near the surface show only waste (grades 0.08 to 0.17), which adds to the rock the planner thinks it must save for future minimums. On a dipping deposit with only 6 periods, that one-period delay means the deep ore is never reached.</p>

<p><strong>So the code is right, but the result isn’t about the value of information.</strong> It shows the every-period minimum interacting with a mine whose ore is all hidden. Without the minimum, all three shafts are within about 450 of blind on this mine.</p>

<h2 id="next">Next</h2>

<ul>
  <li>Decide how to handle the dipping result. Either report it as a finding (a hard minimum in every period makes a rolling MILP ration stripping when the ore is hidden), or soften the minimum in <strong>future</strong> periods only, keeping it hard for the current period so the schedule still meets 1,200 t every period, and rerun the dipping mine.</li>
  <li>Report the blind = 0 rows against greedy, not blind.</li>
  <li>Tighten the full MILP’s solver tolerance if the with/without-minimum comparison needs to be exact.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="large-instance" /><category term="scaling" /><category term="milp" /><category term="greedy" /><category term="blind" /><category term="partial" /><category term="drilling" /><category term="minimum-mining" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">A first look at a real mine, and a surface that misleads</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/15/real-mine-and-misleading-surface/" rel="alternate" type="text/html" title="A first look at a real mine, and a surface that misleads" /><published>2026-10-15T00:00:00-04:00</published><updated>2026-10-15T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/15/real-mine-and-misleading-surface</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/15/real-mine-and-misleading-surface/"><![CDATA[<div class="callout">

  <p><strong>Today in short</strong></p>

  <ul>
    <li>I moved from small generated mines to <strong>newman1</strong>, a real mine from the public <strong>MineLib</strong> library, with published best-known results to check against.</li>
    <li><strong>Grade means something different here.</strong> It’s grams of gold per tonne of rock, not the share of a block that is ore. A block goes wholly to the mill or wholly to waste.</li>
    <li><strong>The dataset can’t satisfy all of my model’s rules.</strong> Capacities, precedence and “mine before you process” carry over. Stockpiling, splitting a block across periods and grade blending don’t.</li>
    <li><strong>I use newman1 only as a mine.</strong> My model keeps its own rules, and MineLib’s published results are for reference, not targets.</li>
    <li>First runs. Full MILP <strong>22,386,899</strong>, greedy 21,800,955, blind 4,243,334.</li>
    <li>Blind <strong>stalls</strong> on this mine. It can’t see the ore under the waste, so it stops digging early.</li>
    <li><strong>newman1 is shelved for now.</strong> Later it will be placed in a full cube and scaled down by merging blocks into small, medium and large instances.</li>
    <li><strong>A misleading surface makes drilling shine.</strong> On a small mine where the surface is richer on one side and the ore underneath is richer on the other, <strong>every shaft gets within 2.5% of perfect information</strong> for under 100 of drilling, greedy falls 40% behind, and the minimum mining rule barely matters.</li>
  </ul>

</div>

<h2 id="why-a-real-dataset">Why a real dataset</h2>

<p>Small generated mines are good for understanding <em>why</em> a method behaves the way it does, but they’re easy to tune without meaning to. A public benchmark gives a fixed, realistic test that other researchers have already solved, so my results can be compared directly with theirs.</p>

<p><strong>MineLib</strong> is a standard library of open-pit mining problems, and <strong>newman1</strong> is one of its smaller instances. Source pages: <a href="https://minelib.org/v1/Datasets.xhtml">MineLib datasets</a> and <a href="https://minelib.org/v1/newman1.xhtml">newman1</a>.</p>

<figure><img src="/Mining_research/images/newman1.png" alt="3D view of the newman1 block model" loading="lazy" /><figcaption>newman1 in my new block model viewer. Darker orange is higher grade. The labelled blocks are the ones discussed below.</figcaption></figure>

<h2 id="the-mine-at-a-glance">The mine at a glance</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th> </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Blocks</td>
      <td>1,060</td>
    </tr>
    <tr>
      <td>Shape</td>
      <td>Irregular pit inside a 22 × 22 × 24 grid, filling only 9.1% of that box</td>
    </tr>
    <tr>
      <td>Precedence</td>
      <td>Each block needs up to 5 blocks on the bench above it removed first (3,922 links)</td>
    </tr>
    <tr>
      <td>Periods</td>
      <td>6, with an 8% discount rate</td>
    </tr>
    <tr>
      <td>Mining limit per period</td>
      <td>2,000,000 t</td>
    </tr>
    <tr>
      <td>Processing limit per period</td>
      <td>1,100,000 t</td>
    </tr>
    <tr>
      <td>Minimum mining</td>
      <td>None</td>
    </tr>
    <tr>
      <td>Grade blending limits</td>
      <td>None</td>
    </tr>
    <tr>
      <td>Total rock</td>
      <td>5.62 Mt, about 2.8 periods of mining</td>
    </tr>
  </tbody>
</table>

<p><strong>Blocks don’t all weigh the same.</strong> Most full blocks weigh 5,664 t. Full oxide ore blocks weigh 5,184 t because oxide rock is lighter. 182 blocks are partial (from 74 t up), mostly where the ground surface cuts through the top of a column. My code already uses each block’s own weight.</p>

<p><strong>The gaps aren’t holes.</strong> Every (x, y) column is a solid stack. The gaps in the picture are just columns that start lower down, so the space above them is air. The model only knows the 1,060 real blocks. One side effect is that near the pit edges some slope links point to rock MineLib left out of the instance, so the wall slopes there are only approximate. That’s a property of the dataset, not my code. In the blind setting, the scheduler starts by seeing the top block of each of the 170 columns.</p>

<h2 id="what-grade-means-here">What “grade” means here</h2>

<p>In my generated mines, grade was the <strong>share of a block that is ore</strong> (0 to 1), and only that share was processed. In newman1, grade is <strong>grams of gold per tonne of rock</strong>, which is why values go up to 3.7. MineLib doesn’t name the metal, but it fits. The price term of 19.29 per gram works out to $600 per troy ounce.</p>

<p>Each block is sent <strong>wholly</strong> to the mill or <strong>wholly</strong> to waste. Its processing value is</p>

<blockquote>
  <p>(19.29 × recovery × grade − processing cost) × tonnage</p>
</blockquote>

<p>with different costs and recoveries for oxide ore (cheaper to process, better recovery) and fresh rock. That gives a break-even (cutoff) grade.</p>

<table>
  <thead>
    <tr>
      <th>Rock</th>
      <th style="text-align: right">Cutoff grade</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fresh rock (FROR, FRWS)</td>
      <td style="text-align: right">about 1.04 g/t</td>
    </tr>
    <tr>
      <td>Oxide ore (OXOR)</td>
      <td style="text-align: right">about 0.47 g/t</td>
    </tr>
  </tbody>
</table>

<p>So a block can contain some gold and still be waste. Block 115, for example, is fresh rock at 0.76 g/t, below its cutoff. The 91 blocks of type “0” have zero grade and are treated as plain waste.</p>

<p>Because the value follows a formula, hidden blocks can be <strong>re-valued from an estimated grade</strong>, which is exactly what the blind and partial (drilling) schedulers need.</p>

<h2 id="where-my-model-and-newman1-dont-match">Where my model and newman1 don’t match</h2>

<table>
  <thead>
    <tr>
      <th>Rule</th>
      <th>My model</th>
      <th>newman1 (MineLib)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Mining and processing limits</td>
      <td>✓</td>
      <td>✓ same</td>
    </tr>
    <tr>
      <td>Precedence (blocks above first)</td>
      <td>✓</td>
      <td>✓ same</td>
    </tr>
    <tr>
      <td>Process only what’s been mined</td>
      <td>✓</td>
      <td>✓ same</td>
    </tr>
    <tr>
      <td>When ore is processed</td>
      <td>Any later period (a free stockpile)</td>
      <td>Same period it’s mined</td>
    </tr>
    <tr>
      <td>Mining a block</td>
      <td>Can be split across periods</td>
      <td>All in one period</td>
    </tr>
    <tr>
      <td>Grade blending limits</td>
      <td>Optional</td>
      <td>None</td>
    </tr>
    <tr>
      <td>Minimum mining</td>
      <td>Optional</td>
      <td>None</td>
    </tr>
  </tbody>
</table>

<p>The last four rows mean my model is a little <strong>looser</strong> than MineLib’s, so its NPV can come out slightly higher than their published values.</p>

<p><strong>Decision.</strong> I use newman1 only as a <strong>mine</strong>, meaning its blocks, values, slopes and capacities. My thesis model stays as it is, with blocks that can be mined over several periods and ore that can be processed later. MineLib’s published results follow different rules, so they’re kept for reference only and are not targets to match.</p>

<details>
  <summary>Technical detail. Fitting newman1 into the code</summary>

  <ul>
    <li>Loader <code class="language-plaintext highlighter-rouge">cleaned/newman1_loader.py</code> reads the MineLib files into the <code class="language-plaintext highlighter-rouge">Mine</code> class. It reproduces the ultimate pit value (26,086,899) exactly.</li>
    <li>My code discounts from period 1, MineLib from period 0, so my NPV = MineLib value ÷ 1.08. I confirmed this by solving MineLib’s LP relaxation, which reproduces their published bound exactly.</li>
    <li>The big-M in constraint 1 was 1e6, too small for this mine. It needs at least 5.62e6.</li>
    <li><code class="language-plaintext highlighter-rouge">newman1_compare.py</code> runs the full MILP, blind and greedy, and checks every schedule against my constraints.</li>
    <li>Ore rule. A block is ore when its processing value beats its mining value, which gives 572 ore blocks (3.02 Mt).</li>
  </ul>

</details>

<h2 id="first-results">First results</h2>

<p>My NPVs below use my own discounting (from period 1).</p>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th style="text-align: right">NPV</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full MILP</td>
      <td style="text-align: right">22,386,899</td>
    </tr>
    <tr>
      <td>Greedy</td>
      <td style="text-align: right">21,800,955</td>
    </tr>
    <tr>
      <td>Blind</td>
      <td style="text-align: right">4,243,334</td>
    </tr>
  </tbody>
</table>

<p>The full MILP solved in 14 seconds. Greedy gets within about 3% of it. Blind collapses to less than a fifth.</p>

<p><strong>For reference against MineLib.</strong> Converted to MineLib’s discounting, my full MILP is 24,177,851, about 2% above their best listed schedule (23,658,230) and still below their upper bound (24,486,549). That’s expected, partly because my model is looser, and MineLib’s listed value comes from an older heuristic. Only 2 blocks were actually split across periods and no ore was held over, so most of the difference is just a better schedule. Since MineLib’s rules aren’t targets, I don’t need to match their number.</p>

<h3 id="what-each-method-does-in-period-1">What each method does in period 1</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th style="text-align: right">Blocks mined</th>
      <th style="text-align: right">Shared with full MILP</th>
      <th style="text-align: right">Average ore grade</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full MILP</td>
      <td style="text-align: right">407</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right">1.59 g/t</td>
    </tr>
    <tr>
      <td>Greedy</td>
      <td style="text-align: right">420</td>
      <td style="text-align: right">325</td>
      <td style="text-align: right">1.31 g/t</td>
    </tr>
    <tr>
      <td>Blind</td>
      <td style="text-align: right">153 (only 18 waste)</td>
      <td style="text-align: right">134</td>
      <td style="text-align: right">1.38 g/t</td>
    </tr>
  </tbody>
</table>

<ul>
  <li><strong>Full MILP</strong> digs the full 2 Mt, 196 ore blocks and 211 waste blocks. Every waste block sits above ore it processes in periods 1 and 2. Block 271, for example, is worthless rock on the top bench, but 133 ore blocks of 1.5 to 2 g/t lie under it. The MILP pays to strip it now so the rich ore reaches the mill early.</li>
  <li><strong>Greedy</strong> grabs the best block on offer each step, starting with blocks 103 and 102 (2.0 and 2.9 g/t), which have little on top. It strips waste only when nothing better is available, so its period-1 ore is lower grade.</li>
  <li><strong>Blind</strong> sees only the top of each column. Blocks 82 and 118 sit low on the side of the pit and look profitable, but block 118 barely beats its cutoff. Blind can’t justify stripping a block like 271, because it can’t see the ore underneath. So it stops after 679,000 t in period 1 and never sees 785 of the blocks. That’s the same stall as on my generated decoy mines.</li>
</ul>

<p>The order of actions inside a period doesn’t affect NPV. Everything in a period is discounted the same, and the replay simply does all digging first, largest blocks first.</p>

<h2 id="back-on-the-small-mine-a-surface-that-misleads">Back on the small mine. A surface that misleads</h2>

<p>The last run of the day went back to the 4 × 5 × 4 mine (10 t blocks, 7 periods, my usual capacities) with a new grade layout built to test the shaft idea cleanly. The surface points one way and the ore underneath points the other.</p>

<ul>
  <li><strong>Surface.</strong> Richest at the far corner (row 4, column 3), poorest at the near corner (row 0, column 0), but nowhere pure waste. It stays below 0.5, so surface ore can’t fill the 40 t plant on its own. Only the two far-corner blocks (0.40 and 0.45) reach the 0.4 cutoff alone. The rest can only be processed blended with richer ore.</li>
  <li><strong>Layers 1 and 2.</strong> The reverse. Richest under the near corner (up to 1.00), poorest under the far one.</li>
  <li><strong>Layer 3.</strong> Waste.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Surface (layer 0)              Layer 1
richer toward far corner       richer toward near corner
 0.05 0.12 0.18 0.25            1.00 0.87 0.73 0.60
 0.10 0.17 0.23 0.30            0.90 0.77 0.63 0.50
 0.15 0.22 0.28 0.35            0.80 0.67 0.53 0.40
 0.20 0.27 0.33 0.40            0.70 0.57 0.43 0.30
 0.25 0.32 0.38 0.45            0.60 0.47 0.33 0.20
</code></pre></div></div>

<p>All three shafts start on the poor side of the surface, so they sample the rich ore underneath. Earlier versions with a richer surface made every shaft worthless, because the surface ore alone already filled the plant. The parameters are saved as <code class="language-plaintext highlighter-rouge">cleaned/runs/params_4x5x4_opposite_p7.yaml</code>.</p>

<div class="figs three">
<figure><img src="/Mining_research/images/4x5x4opp%20%5B1,%205,%2026,%2030,%2051,%2055,%2076,%2080%5D.png" alt="Full diagonal shaft, blocks 1, 5, 26, 30, 51, 55, 76, 80" loading="lazy" /><figcaption>Full diagonal shaft, blocks 1, 5, 26, 30, 51, 55, 76, 80.</figcaption></figure>
<figure><img src="/Mining_research/images/4x5x4opp%20%5B5,%206,%2026,%2031,%2051,%2052%5D.png" alt="Shallow diagonal shaft, blocks 5, 6, 26, 31, 51, 52" loading="lazy" /><figcaption>Shallow diagonal shaft, blocks 5, 6, 26, 31, 51, 52.</figcaption></figure>
<figure><img src="/Mining_research/images/4x5x4opp%20%5B10,%2030,%2050,%2070%5D.png" alt="Vertical shaft, blocks 10, 30, 50, 70" loading="lazy" /><figcaption>Vertical shaft, blocks 10, 30, 50, 70.</figcaption></figure>
</div>

<h3 id="results">Results</h3>

<table>
  <thead>
    <tr>
      <th>NPV</th>
      <th style="text-align: right">Blind</th>
      <th style="text-align: right">Diagonal</th>
      <th style="text-align: right">Shallow</th>
      <th style="text-align: right">Vertical</th>
      <th style="text-align: right">Full information</th>
      <th style="text-align: right">Greedy</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>No minimum</strong></td>
      <td style="text-align: right">891</td>
      <td style="text-align: right">12,589</td>
      <td style="text-align: right">12,482</td>
      <td style="text-align: right">12,589</td>
      <td style="text-align: right">12,675</td>
      <td style="text-align: right">8,874</td>
    </tr>
    <tr>
      <td>Net of drill cost</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right">12,497</td>
      <td style="text-align: right">12,416</td>
      <td style="text-align: right"><strong>12,543</strong></td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td><strong>20 t minimum</strong></td>
      <td style="text-align: right">8,641</td>
      <td style="text-align: right">12,582</td>
      <td style="text-align: right">12,559</td>
      <td style="text-align: right">12,362</td>
      <td style="text-align: right">12,675</td>
      <td style="text-align: right">8,874</td>
    </tr>
    <tr>
      <td>Net of drill cost</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right">12,490</td>
      <td style="text-align: right"><strong>12,493</strong></td>
      <td style="text-align: right">12,316</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"> </td>
    </tr>
  </tbody>
</table>

<p>Drill costs are 92 (diagonal), 66 (shallow) and 46 (vertical).</p>

<p><strong>1. The minimum no longer distorts partial.</strong> With and without the 20 t minimum, partial changes by at most 1.8% (vertical), and diagonal barely moves (12,589 against 12,582). On the lens layout the minimum cost partial up to 20%. The difference is that this surface has some value, especially blended with the rich ore below, so the planner never sees its future periods as forced waste mining and has no reason to ration its stripping.</p>

<p><strong>2. Every shaft gets partial close to perfect information.</strong> Partial lands within 2.5% of full information in every case. With no minimum each shaft gains about 11,600 over blind for under 100 of drilling. With the minimum the gain is about 3,700 to 3,900, because blind digs further on its own when forced to.</p>

<p><strong>3. All three shafts are nearly equal.</strong> The rich zone is broad, not a small lens, so any shaft that hits it is enough. Net of cost, the cheapest shaft wins with no minimum (vertical, 12,543), and diagonal and shallow tie with the minimum. Differences of a few hundred are within the near-tie noise seen before.</p>

<p><strong>4. Greedy falls far behind (8,874).</strong> It takes whatever looks best right now, which is the richer far side of the surface. That’s exactly the trap this layout sets, because the real ore is under the other side. Partial beats greedy by about 40%.</p>

<p><strong>5. Blind with no minimum stops almost at once (891).</strong> It mines the two processable surface blocks at the far corner, finds poor ore below them, and stops. With the minimum it’s forced to keep digging and reaches part of the rich zone (8,641).</p>

<h3 id="why-vertical-works-here-but-failed-on-the-lens">Why vertical works here but failed on the lens</h3>

<p>The vertical shaft goes straight down through blocks 10, 30, 50 and 70 (row 2, column 1).</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th style="text-align: right">Block</th>
      <th style="text-align: right">Grade</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0 (surface)</td>
      <td style="text-align: right">10</td>
      <td style="text-align: right">0.22</td>
    </tr>
    <tr>
      <td>1</td>
      <td style="text-align: right">30</td>
      <td style="text-align: right"><strong>0.67</strong></td>
    </tr>
    <tr>
      <td>2</td>
      <td style="text-align: right">50</td>
      <td style="text-align: right"><strong>0.72</strong></td>
    </tr>
    <tr>
      <td>3</td>
      <td style="text-align: right">70</td>
      <td style="text-align: right">0</td>
    </tr>
  </tbody>
</table>

<p>It starts on the poor side and finds good ore one layer down, under blocks the scheduler can already see.</p>

<ul>
  <li><strong>Stripping is cheaper.</strong> On the lens, the 9 surface blocks above vertical’s ore were pure waste, so clearing them for one rich block only broke even and the MILP never started. Here those blocks grade 0.10 to 0.33. They carry some ore that can be processed blended with the richer ore below, so stripping partly pays for itself.</li>
  <li><strong>The rich zone is broad.</strong> Once digging starts at row 2, column 1, every block uncovered nearby is good, and each reveal leads the scheduler toward the richest corner. That’s why vertical with no minimum ends with exactly the same NPV as diagonal (12,588.60). Once digging starts on the near side, the rest of the schedule is the same, and vertical is the cheapest shaft.</li>
</ul>

<h3 id="how-these-runs-plan">How these runs plan</h3>

<p>All periods at once, with re-planning. Each solve plans the <strong>whole remaining horizon</strong> (from now to period 7) as one MILP, with the 20 t minimum on every period of the plan. The loop carries out one action, reveals any newly exposed blocks, and solves the whole remaining horizon again. So every decision comes from a full multi-period plan, redone as new blocks appear. This is the method I settled on yesterday, and it’s part of why these results improved.</p>

<p>The full-information value (12,675) is the one-shot MILP. It knows every grade and solves all 7 periods once, with no re-planning.</p>

<h3 id="what-this-means-for-the-thesis">What this means for the thesis</h3>

<p>This layout gives the clearest support for the shaft approach. <strong>When the surface misleads about what’s below, one cheap shaft through the poor-looking side recovers almost all the value of perfect information.</strong> It works with and without a minimum, and it avoids the waste-rationing problem. The lens layout is the counter-case. When the surface over the ore is pure waste and a minimum applies, the same method rations its stripping.</p>

<h2 id="plan-newman1-is-shelved-for-now">Plan. newman1 is shelved for now</h2>

<p>newman1 is on hold until the rest of the framework is complete. When I come back to it, the plan is as follows.</p>

<ol>
  <li><strong>Place the mine in a full cube.</strong> A real mine sits inside solid ground, so the irregular pit will be put into a complete rectangular block model, with the empty cells filled in as blocks. This matches how my own code represents a mine, and it also fixes the approximate pit walls, since the rock MineLib left out will now exist.</li>
  <li><strong>Shrink it by combining blocks.</strong> Filling the cube increases the block count a lot, so the problem will be scaled down by merging neighbouring blocks into larger ones. That gives three sizes of the same mine.</li>
</ol>

<table>
  <thead>
    <tr>
      <th>Instance</th>
      <th>How it’s built</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Small</strong></td>
      <td>Combine each group of 4 neighbouring blocks into one</td>
    </tr>
    <tr>
      <td><strong>Medium</strong></td>
      <td>Combine each pair of neighbouring blocks into one</td>
    </tr>
    <tr>
      <td><strong>Large</strong></td>
      <td>The full cube, with no merging</td>
    </tr>
  </tbody>
</table>

<p>This gives one real mine at three sizes, so I can see how each method (MILP, greedy, blind, partial and Sim + LLM) holds up as the problem grows.</p>

<h2 id="next">Next</h2>

<p><strong>When newman1 comes back</strong></p>

<ul>
  <li>Place newman1 in a full cube and build the small, medium and large instances by merging blocks.</li>
  <li>Get blind past the stall, with a minimum mining rule or a larger reveal bonus.</li>
  <li>Adapt the partial (drilling) code. It finds “the block underneath” by ID arithmetic, which only works on a full grid (the cube will help here), and estimated blocks need the newman1 value formula.</li>
  <li>Add exploratory shafts and run partial on all three sizes.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="dataset" /><category term="minelib" /><category term="newman1" /><category term="milp" /><category term="greedy" /><category term="blind" /><category term="partial" /><category term="drilling" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">How the blind scheduler plans, and what planning one period at a time showed</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/14/how-the-blind-scheduler-plans/" rel="alternate" type="text/html" title="How the blind scheduler plans, and what planning one period at a time showed" /><published>2026-10-14T00:00:00-04:00</published><updated>2026-10-14T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/14/how-the-blind-scheduler-plans</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/14/how-the-blind-scheduler-plans/"><![CDATA[<div class="callout">

  <p><strong>Today in short</strong></p>

  <ul>
    <li>The blind scheduler works in a loop. <strong>Plan the whole future, do only the first step, look again, re-plan.</strong></li>
    <li>Every re-plan starts from an exact snapshot of the mine, so nothing already done is counted twice or forgotten.</li>
    <li>A simple check confirms this. The plan’s total value stays fixed between steps and only jumps when digging reveals new ore.</li>
    <li>One gap found. When deciding whether the 20 t minimum can be met, the blind code counts rock it <strong>can see</strong> rather than rock it <strong>can reach</strong>. The partial code already fixes this. Blind should get the same fix so the two are compared fairly.</li>
    <li><strong>Why one period at a time.</strong> Applying the minimum to every future period made partial ration its stripping. My supervisor preferred not to change the rule, so we added a new baseline instead, <strong>full information planned one period at a time</strong>.</li>
    <li>On the lens layout, that baseline loses only 3 to 4% against full planning. <strong>A good shaft with planning ahead (10,167) beats knowing everything without it (9,792).</strong></li>
    <li><strong>Decision.</strong> The main method goes back to planning over the <strong>full horizon</strong>, since that’s what the MILP is built for and one period at a time adds complexity on larger instances.</li>
  </ul>

</div>

<p>Today was mostly about making sure I understand exactly what my code does before running more tests. This post explains the blind scheduler using one real run.</p>

<h2 id="the-example-mine">The example mine</h2>

<p>A small vertical slice, 7 blocks wide and 3 layers deep (blocks 1 to 21), over 6 periods, with a mill that can process 60 t per period. Each number is the block’s grade.</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th> </th>
      <th> </th>
      <th> </th>
      <th> </th>
      <th> </th>
      <th> </th>
      <th> </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0 (surface)</td>
      <td>1 · 0.15</td>
      <td>2 · 0.16</td>
      <td>3 · <strong>0.70</strong></td>
      <td>4 · 0</td>
      <td>5 · 0.46</td>
      <td>6 · 0</td>
      <td>7 · <strong>0.83</strong></td>
    </tr>
    <tr>
      <td>1</td>
      <td>8 · 0.56</td>
      <td>9 · <strong>0.91</strong></td>
      <td>10 · 0.35</td>
      <td>11 · 0.62</td>
      <td>12 · 0.41</td>
      <td>13 · 0.28</td>
      <td>14 · 0.54</td>
    </tr>
    <tr>
      <td>2</td>
      <td>15 · 0.12</td>
      <td>16 · 0.39</td>
      <td>17 · 0.71</td>
      <td>18 · 0.52</td>
      <td>19 · 0.38</td>
      <td>20 · <strong>1.00</strong></td>
      <td>21 · 0.52</td>
    </tr>
  </tbody>
</table>

<p>At the start, the blind scheduler sees <strong>only the top row</strong>. A block below becomes visible only once the block directly above it is fully dug out.</p>

<h2 id="the-loop-plan-everything-do-one-thing-look-again">The loop. Plan everything, do one thing, look again</h2>

<p>Think of a site manager with a map that only shows what’s been uncovered so far. Every time something changes, they redraw the full plan for the rest of the job, but they only carry out the very first item on it. Then they look at the mine again and redraw.</p>

<p>Each turn of the loop has six steps.</p>

<ol>
  <li><strong>Ask the mine what’s allowed right now.</strong> The simulator lists every legal move, taking into account which blocks are uncovered, how much capacity is left this period, and the ore grade limits at the mill.</li>
  <li><strong>Re-plan if anything changed.</strong> A fresh optimisation model is built using only the visible blocks, from the current period to the end. Hidden blocks simply don’t exist in the model, so anything under them is off limits.</li>
  <li><strong>Pick one move from the plan.</strong> From this period’s part of the plan, it takes one action, digging before processing and bigger amounts first, and trims it to what the mine allows.</li>
  <li><strong>Carry it out.</strong> The move goes through the same simulator code that the greedy and LLM schedulers use, so every method is scored the same way.</li>
  <li><strong>Reveal.</strong> If a block is now fully dug, the block below it becomes visible.</li>
  <li><strong>End the period when there’s nothing left to do.</strong> When the plan has nothing more for this period, the clock moves on. Periods end because the plan runs out, not because of a counter.</li>
</ol>

<details>
  <summary>Technical detail. Where each step lives in the code</summary>

  <ul>
    <li>Step 1 is <code class="language-plaintext highlighter-rouge">scheduler.possible_actions()</code>, which calls <code class="language-plaintext highlighter-rouge">Mine.action_list(t)</code>. This is the ground truth on precedence, capacity and grade blending.</li>
    <li>Step 2 runs when <code class="language-plaintext highlighter-rouge">need_solve</code> is true (start of each period, after every action, after every reveal). <code class="language-plaintext highlighter-rouge">build_and_solve(mine, visible, period)</code> builds a new Gurobi model over periods <code class="language-plaintext highlighter-rouge">t0..T</code>. Only the period <code class="language-plaintext highlighter-rouge">t0</code> part of the solution is kept, as <code class="language-plaintext highlighter-rouge">{(1, block): tonnes, (2, block): tonnes}</code>.</li>
    <li>Step 3 is <code class="language-plaintext highlighter-rouge">pick_action</code>. It sorts extraction before processing and larger tonnage first, matches against <code class="language-plaintext highlighter-rouge">offered</code>, and clips to the offered limits, preferring an amount that already fits.</li>
    <li>Step 4 is <code class="language-plaintext highlighter-rouge">apply_action</code>, which calls <code class="language-plaintext highlighter-rouge">scheduler.action(...)</code>, updates <code class="language-plaintext highlighter-rouge">grade_blend</code> and adds <code class="language-plaintext highlighter-rouge">calculate_NPV</code> to <code class="language-plaintext highlighter-rouge">mine.npv</code>.</li>
    <li>Step 5 is <code class="language-plaintext highlighter-rouge">reveal_new_blocks</code>. A visible block with <code class="language-plaintext highlighter-rouge">mine_extraction &gt;= 1</code> adds the block <code class="language-plaintext highlighter-rouge">BELOW</code> it to <code class="language-plaintext highlighter-rouge">visible</code>.</li>
    <li>Step 6. If <code class="language-plaintext highlighter-rouge">pick_action</code> returns None, <code class="language-plaintext highlighter-rouge">time_tick()</code> runs and <code class="language-plaintext highlighter-rouge">need_solve</code> is set again.</li>
  </ul>

</details>

<h2 id="what-that-looked-like-in-the-real-run">What that looked like in the real run</h2>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th>Period</th>
      <th>What it did</th>
      <th>Why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>1</td>
      <td>Dug 39.63 t of block 7</td>
      <td>It sees only the surface, and block 7 (0.83) is the richest</td>
    </tr>
    <tr>
      <td>2</td>
      <td>1</td>
      <td>Dug 30.37 t of block 3</td>
      <td>Fills the rest of the 70 t digging limit with block 3 (0.70)</td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>Processed 32.50 t of block 7</td>
      <td>Only the dug part is ready. 39.63/50 of its 41 t of ore is 32.50 t</td>
    </tr>
    <tr>
      <td>4</td>
      <td>1</td>
      <td>Processed 21.26 t of block 3</td>
      <td>Uses the remaining mill capacity while keeping the grade mix within limits</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td><em>Nothing left planned</em></td>
      <td>Move to period 2</td>
    </tr>
    <tr>
      <td>5</td>
      <td>2</td>
      <td>Dug all 50 t of block 5</td>
      <td>Fully dug, so <strong>block 12 below it is revealed</strong></td>
    </tr>
    <tr>
      <td>6</td>
      <td>2</td>
      <td>Finished block 3 (19.63 t)</td>
      <td><strong>Block 10 is revealed</strong></td>
    </tr>
    <tr>
      <td>…</td>
      <td> </td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>27</td>
      <td>6</td>
      <td>Processed 10.8 t of block 14</td>
      <td>Last action. Final NPV 11,914.53</td>
    </tr>
  </tbody>
</table>

<p><strong>27 actions took 27 re-plans</strong>, one per action. Each took about 0.01 seconds, so re-planning every time is cheap at this size.</p>

<p>The first plan expected to earn only 7,569 in total, because it could only see the surface. The run finished at <strong>11,915</strong> because every reveal showed ore it didn’t know about, and it re-planned around it. Full information reached 12,029, so blind lost <strong>under 1%</strong>. Greedy reached 11,286.</p>

<h3 id="two-design-details">Two design details</h3>

<p><strong>Why digging comes before processing.</strong> The mine only offers a processing move once ore has been dug. If it processed first, there would be less ore ready than the plan assumed, and the move would get cut short.</p>

<p><strong>Why there’s a small “reveal bonus”.</strong> Block 7 was only partly dug after period 2 and finished in period 3, so block 14 underneath stayed hidden until then. When several visible blocks are equally valuable, the model has no reason to <em>finish</em> any one of them, and it can chip at several without uncovering anything. A tiny bonus for fully clearing a block that sits above a hidden one breaks those ties towards finishing blocks early. The bonus is not counted in the NPV.</p>

<h2 id="how-each-re-plan-stays-consistent-with-the-past">How each re-plan stays consistent with the past</h2>

<p>Each re-plan starts from a <strong>snapshot</strong> of the mine. Past periods are dropped entirely, because their money is already banked. Only the current period and the future are planned. The snapshot has five parts. As an example, take the 4th re-plan, in period 1, right after steps 1 to 3 above.</p>

<ol>
  <li><strong>How much of each block is already dug and processed.</strong> Block 7 is 79.26% dug and 79.26% processed. Block 3 is 60.74% dug and 0% processed. The plan can only touch what’s left.</li>
  <li><strong>Capacity already used this period.</strong> 70 t has been dug in period 1, which is the full limit, so the plan can’t dig anything more this period. The mill has used 32.50 t of its 60 t, leaving 27.50 t.</li>
  <li><strong>The grade mix already at the mill this period.</strong> The 32.50 t of 0.83-grade ore puts the mill comfortably above its 0.4 minimum average grade. That leaves room to blend in up to about 55.9 t of low-grade (0.15) ore this period and still stay above 0.4. Without remembering this, the plan could pick a mix that breaks the limit once combined with what’s already been processed.</li>
  <li><strong>Only process what’s been dug.</strong> Block 7 can’t be dug further this period, so none of it can be processed further either. That’s why the 4th re-plan processes block 3 instead. Exactly 21.26 t of it was dug and ready, and that’s what step 4 processed.</li>
  <li><strong>Money is always counted in today’s terms.</strong> Discounting uses the real period number, so every plan’s value is in the same units as the money already banked.</li>
</ol>

<details>
  <summary>Technical detail. The five frozen pieces in code</summary>

  <p><strong>Dug and processed fractions.</strong> Computed from tonnes remaining, not from the stored fractions, because <code class="language-plaintext highlighter-rouge">Mine.update</code> rounds <code class="language-plaintext highlighter-rouge">mine_process</code> to 4 decimals.</p>

  <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">e</span> <span class="o">=</span> <span class="p">{</span><span class="n">n</span><span class="p">:</span> <span class="mf">1.0</span> <span class="k">if</span> <span class="n">mine</span><span class="p">.</span><span class="n">mine_extraction</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="mf">1.0</span> <span class="k">else</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">mine</span><span class="p">.</span><span class="n">To_be_extracted</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="o">/</span> <span class="n">tt</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">blocks</span><span class="p">}</span>
<span class="n">p</span> <span class="o">=</span> <span class="p">{</span><span class="n">n</span><span class="p">:</span> <span class="mf">1.0</span> <span class="k">if</span> <span class="p">(</span><span class="n">o</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">or</span> <span class="n">mine</span><span class="p">.</span><span class="n">mine_process</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="mf">1.0</span><span class="p">)</span> <span class="k">else</span> <span class="mi">1</span> <span class="o">-</span> <span class="n">mine</span><span class="p">.</span><span class="n">To_be_processes</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="o">/</span> <span class="n">o</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">blocks</span><span class="p">}</span>
</code></pre></div>  </div>

  <table>
    <thead>
      <tr>
        <th>Block</th>
        <th style="text-align: right">Tonnage</th>
        <th style="text-align: right">Ore (<code class="language-plaintext highlighter-rouge">int(g×50)</code>)</th>
        <th><code class="language-plaintext highlighter-rouge">e</code> (dug)</th>
        <th><code class="language-plaintext highlighter-rouge">p</code> (processed)</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>7 (0.83)</td>
        <td style="text-align: right">50</td>
        <td style="text-align: right">41</td>
        <td>1 − 10.37/50 = 0.7926</td>
        <td>1 − 8.50/41 = 0.7926</td>
      </tr>
      <tr>
        <td>3 (0.70)</td>
        <td style="text-align: right">50</td>
        <td style="text-align: right">35</td>
        <td>30.37/50 = 0.6074</td>
        <td>0</td>
      </tr>
    </tbody>
  </table>

  <p>These set the variable bounds (<code class="language-plaintext highlighter-rouge">y[7,t] ≤ 0.2074</code>, <code class="language-plaintext highlighter-rouge">x[7,t] ≤ 0.2074</code>), the at-most-once totals, and the cumulative <code class="language-plaintext highlighter-rouge">mined_by</code>, which is what lets precedence and process-after-mine see earlier work.</p>

  <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">mined_by</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">t</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">n</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">visible</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">mine</span><span class="p">.</span><span class="n">mine_extraction</span><span class="p">[</span><span class="n">n</span><span class="p">]</span>      <span class="c1"># hidden: a constant (0)
</span>    <span class="k">return</span> <span class="n">e</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="o">+</span> <span class="n">gp</span><span class="p">.</span><span class="n">quicksum</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="n">n</span><span class="p">,</span> <span class="n">j</span><span class="p">]</span> <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">t0</span><span class="p">,</span> <span class="n">t</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>
</code></pre></div>  </div>

  <p><strong>Capacity used.</strong> <code class="language-plaintext highlighter-rouge">used = mine.Mining_capacity_used[t] if t == t0 else 0.0</code>, so the constraint becomes <code class="language-plaintext highlighter-rouge">70 + Σ 50·y[n,1] ≤ 70</code>, which forces every <code class="language-plaintext highlighter-rouge">y[n,1] = 0</code>.</p>

  <p><strong>Grade blend.</strong> The head-grade limit is linear, written as tonnes × (grade − limit).</p>

  <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">done_ub</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">((</span><span class="n">g</span> <span class="o">-</span> <span class="n">G_ub</span><span class="p">[</span><span class="n">t</span><span class="p">])</span> <span class="o">*</span> <span class="n">amt</span> <span class="k">for</span> <span class="n">amt</span><span class="p">,</span> <span class="n">g</span> <span class="ow">in</span> <span class="n">blend</span><span class="p">)</span>   <span class="c1"># (0.83 − 1.0) × 32.50 = −5.52
</span><span class="n">done_lb</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">((</span><span class="n">g</span> <span class="o">-</span> <span class="n">G_lb</span><span class="p">[</span><span class="n">t</span><span class="p">])</span> <span class="o">*</span> <span class="n">amt</span> <span class="k">for</span> <span class="n">amt</span><span class="p">,</span> <span class="n">g</span> <span class="ow">in</span> <span class="n">blend</span><span class="p">)</span>   <span class="c1"># (0.83 − 0.4) × 32.50 = +13.97
</span></code></pre></div>  </div>

  <p>13.97 / (0.4 − 0.15) ≈ 55.9 t of 0.15-grade ore can still be blended in.</p>

  <p><strong>Process after mine.</strong> <code class="language-plaintext highlighter-rouge">done = min(p[n], e[n])</code>, then <code class="language-plaintext highlighter-rouge">done + Σ x[n,j] ≤ mined_by(n,t)</code>. For block 7 this reads <code class="language-plaintext highlighter-rouge">0.7926 + x[7,1] ≤ 0.7926 + y[7,1]</code>, and since <code class="language-plaintext highlighter-rouge">y[7,1] = 0</code>, <code class="language-plaintext highlighter-rouge">x[7,1] = 0</code>. The <code class="language-plaintext highlighter-rouge">min(p, e)</code> absorbs tiny rounding.</p>

  <p><strong>Discounting.</strong> <code class="language-plaintext highlighter-rouge">(vt[n] * x[n, t] - q[n] * y[n, t]) / (1 + r) ** t</code>, where <code class="language-plaintext highlighter-rouge">t</code> is the real period, not <code class="language-plaintext highlighter-rouge">t − t0</code>.</p>

  <p><strong>Rebuilt each solve.</strong> The binaries <code class="language-plaintext highlighter-rouge">b</code> (started), <code class="language-plaintext highlighter-rouge">u</code> (fully mined) and <code class="language-plaintext highlighter-rouge">l</code> (minimum active) are new each time, but the frozen values force them to agree with the past. For block 3, <code class="language-plaintext highlighter-rouge">mined_by ≥ 0.6074 &gt; 0</code> forces <code class="language-plaintext highlighter-rouge">b[3,t] = 1</code>.</p>

</details>

<h3 id="the-check-that-proves-it-works">The check that proves it works</h3>

<p>If the snapshot is right, then <strong>money already banked plus the money the new plan expects</strong> should stay exactly the same from one re-plan to the next, as long as nothing new is revealed. Doing a step just moves value from “planned” to “banked”. It should only jump when a reveal shows the model new ore.</p>

<table>
  <thead>
    <tr>
      <th>Re-plan</th>
      <th>Period</th>
      <th style="text-align: right">Banked</th>
      <th style="text-align: right">+ Planned future</th>
      <th style="text-align: right">= Total</th>
      <th>What happened</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>1</td>
      <td style="text-align: right">0.00</td>
      <td style="text-align: right">7,569.30</td>
      <td style="text-align: right"><strong>7,569.30</strong></td>
      <td>Start</td>
    </tr>
    <tr>
      <td>2</td>
      <td>1</td>
      <td style="text-align: right">−360.29</td>
      <td style="text-align: right">7,929.59</td>
      <td style="text-align: right"><strong>7,569.30</strong></td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td style="text-align: right">−636.37</td>
      <td style="text-align: right">8,205.67</td>
      <td style="text-align: right"><strong>7,569.30</strong></td>
      <td> </td>
    </tr>
    <tr>
      <td>4</td>
      <td>1</td>
      <td style="text-align: right">2,317.99</td>
      <td style="text-align: right">5,251.30</td>
      <td style="text-align: right"><strong>7,569.29</strong></td>
      <td> </td>
    </tr>
    <tr>
      <td>5</td>
      <td>2</td>
      <td style="text-align: right">4,250.52</td>
      <td style="text-align: right">3,318.77</td>
      <td style="text-align: right"><strong>7,569.29</strong></td>
      <td>New period, still the same</td>
    </tr>
    <tr>
      <td>6</td>
      <td>2</td>
      <td style="text-align: right">3,837.30</td>
      <td style="text-align: right">4,014.16</td>
      <td style="text-align: right"><strong>7,851.46</strong></td>
      <td>↑ Block 12 revealed</td>
    </tr>
    <tr>
      <td>7</td>
      <td>2</td>
      <td style="text-align: right">3,675.05</td>
      <td style="text-align: right">4,835.98</td>
      <td style="text-align: right"><strong>8,511.03</strong></td>
      <td>↑ Block 10 revealed</td>
    </tr>
    <tr>
      <td>8 to 10</td>
      <td>2</td>
      <td style="text-align: right">…</td>
      <td style="text-align: right">…</td>
      <td style="text-align: right"><strong>8,511.04</strong></td>
      <td>Same again</td>
    </tr>
  </tbody>
</table>

<p>That’s exactly what happens. If any part of the snapshot were wrong, the total would drift between steps even without a reveal. This makes a good automatic test, and a good figure for the thesis.</p>

<h2 id="a-gap-found-can-see-isnt-the-same-as-can-reach">A gap found. “Can see” isn’t the same as “can reach”</h2>

<p>My mining rule says the mine should dig at least 20 t per period, <strong>as long as enough rock is available</strong>. Before applying that rule, the code checks how much rock is left.</p>

<p>In the blind code, that check counts <strong>every visible block that isn’t dug yet</strong>. But a block can be visible without being reachable yet. In this mine, block 12 becomes visible as soon as block 5 above it is cleared. It still can’t be dug until its neighbours 4 and 6 are also cleared, because of the pit slope.</p>

<p>Here’s how that can go wrong. Say the check finds</p>

<ul>
  <li>10 t of rock it can actually reach, and</li>
  <li>50 t of rock it can see but can’t reach yet.</li>
</ul>

<p>It concludes 60 t is available, so it insists on the 20 t minimum. Only 10 t is really reachable, so the plan becomes impossible to satisfy. The code’s fallback then drops the minimum for that whole re-plan, which means the mine might dig <strong>less</strong> than 20 t that period.</p>

<p><strong>The partial code already fixes this.</strong> It counts only rock that is currently reachable. The blind code still uses the old count.</p>

<p><strong>Has it affected any results so far?</strong> No. I checked all 15 saved blind runs, and none of them ever had to drop the minimum. The 20 t default is low enough that the problem never showed up. It could show up with a higher minimum (for example 100 t), or on wider mines where more blocks have to be cleared to reach the ones underneath.</p>

<p><strong>Why fix it anyway.</strong> Blind and partial should use the same rule for deciding whether the minimum can be met. Otherwise a difference between them could come from this code detail instead of from the drilling information, which is what I’m actually trying to measure.</p>

<details>
  <summary>Technical detail. The line in question</summary>

  <p>In <code class="language-plaintext highlighter-rouge">rolling_milp.py</code> the minimum’s “remaining rock” sums over all visible blocks.</p>

  <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">remaining</span> <span class="o">=</span> <span class="n">used</span> <span class="o">+</span> <span class="n">gp</span><span class="p">.</span><span class="n">quicksum</span><span class="p">(</span><span class="n">tt</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">e</span><span class="p">[</span><span class="n">n</span><span class="p">]</span> <span class="o">-</span> <span class="p">...)</span> <span class="k">for</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">blocks</span><span class="p">)</span>
</code></pre></div>  </div>

  <p><code class="language-plaintext highlighter-rouge">rolling_milp_partial.py</code> restricts this to a <code class="language-plaintext highlighter-rouge">reachable</code> set. The fix is to port that into <code class="language-plaintext highlighter-rouge">rolling_milp.py</code> and <code class="language-plaintext highlighter-rouge">run_configs.py</code>.</p>

</details>

<h2 id="why-we-ended-up-planning-one-period-at-a-time">Why we ended up planning one period at a time</h2>

<p>This took several steps, each one a response to something odd in the results.</p>

<p><strong>1. The minimum rule had already caused one problem.</strong> On the 10 × 10 lens with the 10 → 91 shaft, partial sat idle for 5 periods. The minimum was counting deep drilled blocks the plan couldn’t reach yet, so every plan was impossible and the minimum got dropped. That was fixed by counting only <strong>reachable</strong> rock (the same gap described above for blind).</p>

<p><strong>2. Then the 4 × 5 × 4 lens showed a subtler problem.</strong> At realistic capacities, with 10 t blocks, the shafts worked, but the 20 t minimum made partial <strong>worse</strong>, not better.</p>

<table>
  <thead>
    <tr>
      <th>Shallow shaft</th>
      <th style="text-align: right">No minimum</th>
      <th style="text-align: right">20 t minimum</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Partial</td>
      <td style="text-align: right">10,167</td>
      <td style="text-align: right">8,151</td>
    </tr>
    <tr>
      <td>Blind</td>
      <td style="text-align: right">1,273</td>
      <td style="text-align: right">5,861</td>
    </tr>
  </tbody>
</table>

<p>Partial knew where the ore was and the whole surface was open, yet it dug only 20 t in period 1. Why?</p>

<p><strong>3. The first plan showed the reason.</strong> With the minimum, the plan spread the stripping out at exactly 20 t per period, reaching ore block 26 in period 4 and block 31 in period 7, instead of periods 2 and 3. Each plan applied the 20 t minimum to <strong>every future period too</strong>, and in those future periods the plan could only see waste. It believed “I’ll have to mine 20 t of waste every period anyway”, so it saved the stripping to cover those future minimums instead of stripping fast. It couldn’t know that mining would uncover more ore and fill those periods.</p>

<p><strong>4. That led to the idea of applying the minimum only to the current period.</strong> Each plan enforces the minimum only for the period being carried out now. Every period still meets it when its turn comes, but the plan no longer assumes a future of forced waste. On the lens layout, partial went from 8,489 to 10,221 and blind from 5,861 to 8,489.</p>

<p><strong>5. My supervisor felt this approach might not be feasible.</strong> So it stays in the code as a switch, <code class="language-plaintext highlighter-rouge">CURRENT_PERIOD_MINIMUM_ONLY</code>, but it is now <strong>off by default</strong>, which keeps my original method. Instead, we followed the supervisor’s suggestion and added a new baseline. <strong>Full information, solved one period at a time.</strong> Its plan only ever contains the current period, so it can’t have the future-minimum problem. It also shows how much planning ahead is worth when every grade is known.</p>

<h3 id="an-everyday-version-of-the-problem">An everyday version of the problem</h3>

<p><strong>The situation.</strong> You’re a freelancer, and your contract says you must bill <strong>at least 2 hours every day</strong>.</p>

<ul>
  <li>On your desk is a pile of unpaid admin work (the <strong>waste</strong>), 8 hours of it.</li>
  <li>A friend tells you there’s a well-paid project waiting behind that admin work. You can only start it once the admin is cleared. This is the <strong>shaft</strong> showing ore under the waste.</li>
  <li>What you don’t know yet is that finishing that project will lead the client to send you more paid projects. This is the <strong>rest of the ore lens</strong>, revealed as you mine.</li>
</ul>

<p>Every morning you plan the rest of the week, using only the work you can see right now.</p>

<p><strong>Old rule. Minimum in every planned day.</strong> Each morning you plan so that every day this week has at least 2 hours of work. Looking at your desk, the only work you can see for later days is the admin pile. So you think</p>

<blockquote>
  <p>“If I clear all 8 hours of admin today and tomorrow, I’ll have nothing left for Thursday and Friday, and then I’d need to find more unpaid filler to meet my 2-hour minimum. Better to stretch the admin out, 2 hours a day, so it covers every day.”</p>
</blockquote>

<p>So you do 2 hours of admin a day, and the paid project doesn’t start until the end of the week. In reality, the moment you’d finished the admin and started the paid project, new paid work would have arrived to fill Thursday and Friday. You delayed the money to protect against an empty week that was never going to happen.</p>

<p><strong>New rule. Minimum only for today.</strong> Each morning you only make sure today has at least 2 hours. Future days are planned freely.</p>

<blockquote>
  <p>“Admin today and tomorrow at full speed, then start the paid project.”</p>
</blockquote>

<p>You clear the admin fast and start earning early. When Thursday comes, you plan again, and by then the new paid work is there, so the minimum is easily met.</p>

<p>Both ways, you work at least 2 hours every single day. The only difference is whether your plan assumes the future holds nothing but admin.</p>

<table>
  <thead>
    <tr>
      <th>Freelancer</th>
      <th>The mine</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Must bill at least 2 hours every day</td>
      <td>Must mine at least 20 t every period</td>
    </tr>
    <tr>
      <td>Unpaid admin pile</td>
      <td>Waste blocks on the surface</td>
    </tr>
    <tr>
      <td>Paid project behind the admin</td>
      <td>Ore block found by the shaft (26, 30 or 31)</td>
    </tr>
    <tr>
      <td>New paid work that appears later</td>
      <td>Rest of the ore lens, revealed as mining goes</td>
    </tr>
    <tr>
      <td>Planning each morning from what’s on the desk</td>
      <td>Rolling MILP re-planning from visible blocks</td>
    </tr>
    <tr>
      <td>Stretching admin out to cover every day</td>
      <td>Stripping 2 blocks (20 t) per period, ore reached in periods 4 and 7</td>
    </tr>
    <tr>
      <td>Clearing admin fast, re-planning daily</td>
      <td>Stripping at full capacity, ore in periods 2 and 3</td>
    </tr>
  </tbody>
</table>

<p>The old rule made the planner treat its blind spot (future ore it couldn’t see) as if it were certain emptiness. The new rule lets it plan only today’s commitment and leave the future open for when it knows more.</p>

<h3 id="the-new-baseline-full-information-one-period-at-a-time">The new baseline. Full information, one period at a time</h3>

<p>Lens layout (4 × 5 × 4), with the switch off, so partial uses the original method and matches the earlier runs exactly.</p>

<table>
  <thead>
    <tr>
      <th>NPV</th>
      <th style="text-align: right">Blind</th>
      <th style="text-align: right">Diagonal</th>
      <th style="text-align: right">Shallow</th>
      <th style="text-align: right">Vertical</th>
      <th style="text-align: right">Full, one period at a time</th>
      <th style="text-align: right">Full, planned all at once</th>
      <th style="text-align: right">Greedy</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>No minimum</strong></td>
      <td style="text-align: right">1,273</td>
      <td style="text-align: right">10,167</td>
      <td style="text-align: right">10,167</td>
      <td style="text-align: right">1,273</td>
      <td style="text-align: right">9,792</td>
      <td style="text-align: right">10,223</td>
      <td style="text-align: right">10,174</td>
    </tr>
    <tr>
      <td><strong>20 t minimum</strong></td>
      <td style="text-align: right">5,861</td>
      <td style="text-align: right">8,489</td>
      <td style="text-align: right">8,151</td>
      <td style="text-align: right">6,677</td>
      <td style="text-align: right">9,907</td>
      <td style="text-align: right">10,223</td>
      <td style="text-align: right">10,174</td>
    </tr>
  </tbody>
</table>

<p><strong>1. Planning one period at a time costs full information only about 3 to 4% here.</strong> Knowing every grade but planning only the current period gives 9,792 to 9,907, against 10,223 when the whole schedule is planned at once. On this layout, with 10 t blocks and 70 t of digging per period, the plan can often strip waste and dig the ore underneath in the same period, so not looking ahead costs little. On the Decoy layout it was very different, about 29%.</p>

<p><strong>2. With no minimum, a good shaft plus planning ahead beats knowing everything but not planning ahead.</strong> Diagonal and shallow reach 10,167, above full information planned one period at a time (9,792). A cheap shaft is worth more than perfect knowledge if the planner can’t look ahead. <strong>Planning ahead matters as much as the information.</strong></p>

<p><strong>3. With the 20 t minimum, partial falls below full one-period</strong> (8,489 and 8,151 against 9,907). This is the rationing effect from step 3 above. Full one-period can’t have it, because its plan only ever contains one period. So the gap shows what that rationing costs partial, about 1,400 to 1,750.</p>

<p><strong>4. Full one-period does better with the minimum than without it</strong> (9,907 against 9,792). Without a minimum, a one-period planner sometimes skips stripping that only pays off next period, because it can’t see next period. The minimum forces it to dig anyway, which helps. That’s the same reason blind improves with the minimum.</p>

<p><strong>5. The shafts behave as before.</strong> Diagonal and shallow pay well in both settings. Vertical pays only with the minimum (+816 against a cost of 46) and is worthless without it.</p>

<h3 id="where-the-value-comes-from">Where the value comes from</h3>

<p>With no minimum, the gap from blind to full information splits into two parts.</p>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th style="text-align: right">NPV</th>
      <th style="text-align: right">Added</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Blind</td>
      <td style="text-align: right">1,273</td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td>Know every grade, but plan one period at a time</td>
      <td style="text-align: right">9,792</td>
      <td style="text-align: right"><strong>+8,519</strong></td>
    </tr>
    <tr>
      <td>Also plan the whole schedule ahead</td>
      <td style="text-align: right">10,223</td>
      <td style="text-align: right">+431</td>
    </tr>
  </tbody>
</table>

<p>On this layout almost all the value comes from the <strong>information</strong>. On Decoy it was the other way round, with look-ahead worth far more. That contrast is worth showing my supervisor.</p>

<h3 id="decision-back-to-planning-over-the-full-horizon">Decision. Back to planning over the full horizon</h3>

<p>After looking at these results, I’m going back to giving the MILP the <strong>full planning horizon</strong>, all remaining periods at once, as the main method.</p>

<ul>
  <li><strong>The MILP is built to work on the full horizon.</strong> Its strength is weighing what happens now against what happens later. Cutting it down to one period at a time throws that away, and these results show look-ahead can be worth a lot (about 29% on Decoy).</li>
  <li><strong>One period at a time doesn’t scale.</strong> On larger instances it means many more separate solves and extra bookkeeping between them, which increases the complexity of the method instead of reducing it.</li>
</ul>

<p>So full information planned one period at a time stays as a <strong>comparison</strong> on these small cases, to show how much of the value comes from the information and how much from planning ahead. The main results use the full-horizon MILP.</p>

<h3 id="what-this-means-for-the-thesis">What this means for the thesis</h3>

<ul>
  <li>The current-period-only rule is <strong>off</strong>. All main results use the original method, where each plan applies the minimum to every period.</li>
  <li>Partial’s shortfall under the minimum (8,151 to 8,489 against 9,907) is a <strong>known, explained effect</strong> of that method. It can be described in the thesis, or fixed later if my supervisor agrees.</li>
  <li>The main method plans over the <strong>full horizon</strong>. Full information planned one period at a time is used only as a comparison, to separate the value of knowing the grades from the value of planning ahead.</li>
</ul>

<details>
  <summary>For reference. What the current-period switch did when it was on</summary>

  <table>
    <thead>
      <tr>
        <th>Mine</th>
        <th style="text-align: right">Blind, old rule</th>
        <th style="text-align: right">Blind, current period only</th>
        <th style="text-align: right">Partial, old rule</th>
        <th style="text-align: right">Partial, current period only</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Lens (4 × 5 × 4)</td>
        <td style="text-align: right">5,861</td>
        <td style="text-align: right">8,489</td>
        <td style="text-align: right">8,489</td>
        <td style="text-align: right">10,221</td>
      </tr>
      <tr>
        <td>Decoy</td>
        <td style="text-align: right">8,416</td>
        <td style="text-align: right">8,105</td>
        <td style="text-align: right">10,248</td>
        <td style="text-align: right">10,271</td>
      </tr>
      <tr>
        <td>Dipping orebody</td>
        <td style="text-align: right">13,668</td>
        <td style="text-align: right">13,408</td>
        <td style="text-align: right"> </td>
        <td style="text-align: right"> </td>
      </tr>
    </tbody>
  </table>

  <p>With the switch on, the lens schedule dug 70 t in every period, so the 20 t minimum was still met. The switch also changed blind, sometimes for the worse, which is another reason to keep it off for the main results. Each saved result records the setting as <code class="language-plaintext highlighter-rouge">current_period_minimum_only</code>.</p>

</details>

<h2 id="next">Next</h2>

<p><strong>Tomorrow</strong></p>

<ul>
  <li><strong>A new mine shape.</strong> Test a <strong>convex block model</strong> instead of a cube, so the pit narrows with depth the way a real open pit does.</li>
  <li><strong>Sim + LLM on the shaft approach.</strong> Run the simulator with the LLM scheduler on the partial (drilled shaft) setting, and compare it with the MILP results.</li>
</ul>

<p><strong>Also on the list</strong></p>

<ul>
  <li>Port the reachability fix into the blind code (<code class="language-plaintext highlighter-rouge">rolling_milp.py</code>, <code class="language-plaintext highlighter-rouge">run_configs.py</code>).</li>
  <li>Re-run the saved blind runs and confirm the results don’t change.</li>
  <li>Test cases where it could matter, such as a 100 t minimum and a wider mine.</li>
  <li>Turn the “banked + planned stays constant” check into an automatic test.</li>
  <li>Keep the full-horizon MILP as the main method for all batch runs.</li>
  <li>Run full information one period at a time on the small batch instances only (Decoy, Dipping orebody and the rest), as a comparison, and build the “information vs planning ahead” split for each.</li>
  <li>Show my supervisor the lens vs Decoy contrast and agree whether to address partial’s rationing under the minimum.</li>
  <li>Label every results table with the minimum rule it used.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="blind" /><category term="partial" /><category term="milp" /><category term="method" /><category term="minimum-mining" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Starting the log, and where the project stands</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/13/starting-the-log/" rel="alternate" type="text/html" title="Starting the log, and where the project stands" /><published>2026-10-13T00:00:00-04:00</published><updated>2026-10-13T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/13/starting-the-log</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/13/starting-the-log/"><![CDATA[<p>First entry. From here on I’ll write a short note at the end of each working day.</p>

<h2 id="where-things-are">Where things are</h2>

<p>The simulator plus LLM scheduler is working for the <strong>blind</strong> and <strong>full</strong> information settings. Last week I added the <strong>partial</strong> setting, where the scheduler sees noisy grade estimates along exploratory drillholes, and ran a batch of rolling-horizon MILP experiments on it (<code class="language-plaintext highlighter-rouge">rolling_milp_partial.py</code>).</p>

<p>I also cleaned up the codebase into the <code class="language-plaintext highlighter-rouge">cleaned/</code> folder and built a block model viewer notebook to inspect what each scheduler actually extracts period by period.</p>

<h2 id="a-case-worth-reporting-waste-cover">A case worth reporting: waste cover</h2>

<p>A uniform low-grade surface with no visible signal of where the ore is, and a rich zone underneath.</p>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th style="text-align: right">NPV</th>
      <th style="text-align: right">vs full MILP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Blind MILP</td>
      <td style="text-align: right">3,282.81</td>
      <td style="text-align: right">−53.0%</td>
    </tr>
    <tr>
      <td>Greedy</td>
      <td style="text-align: right">6,809.30</td>
      <td style="text-align: right">−2.4%</td>
    </tr>
    <tr>
      <td>Full MILP</td>
      <td style="text-align: right">6,978.63</td>
      <td style="text-align: right">—</td>
    </tr>
  </tbody>
</table>

<p>Greedy beats blind MILP by a wide margin here. Greedy fills the available capacity in every period. The blind MILP can’t see the lower levels, so it spreads extraction across every surface block at once and never commits to digging down to the ore.</p>

<h2 id="next">Next</h2>

<ul>
  <li>Run the partial (drilling) case on the same instance and see how much of the gap a few drillholes close.</li>
  <li>Add drilling cost to the NPV comparison so the trade-off is honest.</li>
  <li>Write up the remaining cases for the report.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="meta" /><category term="milp" /><category term="greedy" /><category term="drilling" /><summary type="html"><![CDATA[First entry. From here on I’ll write a short note at the end of each working day.]]></summary></entry><entry><title type="html">When does exploratory drilling pay off?</title><link href="https://mustavi-99.github.io/Mining_research/2026/10/13/when-does-drilling-pay-off/" rel="alternate" type="text/html" title="When does exploratory drilling pay off?" /><published>2026-10-13T00:00:00-04:00</published><updated>2026-10-13T00:00:00-04:00</updated><id>https://mustavi-99.github.io/Mining_research/2026/10/13/when-does-drilling-pay-off</id><content type="html" xml:base="https://mustavi-99.github.io/Mining_research/2026/10/13/when-does-drilling-pay-off/"><![CDATA[<div class="callout">

  <p><strong>Today in short</strong></p>

  <ul>
    <li>A <strong>minimum mining rule</strong> (dig at least so many tonnes every period) is what keeps the blind scheduler from giving up early. Without it, blind can lose most of the mine’s value.</li>
    <li>A <strong>drill shaft pays only when two things are true</strong>. The mine has to dig below the surface within the plan, and the surface has to be a poor guide to what’s underneath.</li>
    <li>On a mine with a good, honest surface, <strong>no shaft was worth its cost</strong>. On the same mine with ore hidden under waste, <strong>a shaft costing under 100 added about 8,800</strong> and got within 0.6% of perfect information.</li>
    <li>The minimum mining rule <strong>helps blind but hurts the scheduler that drilled</strong>, because of how the rule is applied to future periods. Tomorrow I test a fix.</li>
  </ul>

</div>

<h2 id="background-in-plain-words">Background in plain words</h2>

<p>An open-pit mine is modelled as a 3D grid of <strong>blocks</strong>. Each block has a <strong>grade</strong>, the share of it that is valuable ore. Each period the scheduler decides which blocks to dig and whether each one goes to the mill (ore) or the waste dump. A block can only be dug once the blocks above it are gone. The goal is the highest <strong>NPV</strong> (net present value), which is total profit with money earned sooner counted as worth more.</p>

<p>The question is how much it helps to know what’s underground before you start. I compare schedulers that know different amounts.</p>

<table>
  <thead>
    <tr>
      <th>Scheduler</th>
      <th>What it knows</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Blind</strong></td>
      <td>Only the surface. Deeper grades are revealed as digging exposes them.</td>
    </tr>
    <tr>
      <td><strong>Partial</strong></td>
      <td>The surface, plus estimated grades along one <strong>exploratory drill shaft</strong>, bought at a cost before mining starts.</td>
    </tr>
    <tr>
      <td><strong>Full information</strong></td>
      <td>Every grade in advance. The best result any schedule can reach.</td>
    </tr>
    <tr>
      <td><strong>Greedy</strong></td>
      <td>A simple rule of thumb that always digs at full capacity. A baseline.</td>
    </tr>
  </tbody>
</table>

<p>Blind, partial and full all use a <strong>rolling MILP</strong>, an optimisation model that re-plans after every step using only what it can currently see.</p>

<p>A <strong>minimum mining rule</strong> (also called the lower limit) says the mine must dig at least a set tonnage every period, as a real operation would.</p>

<p>The gap between blind and full information is the most any drilling can ever be worth. A shaft only pays if it closes enough of that gap to cover its own cost.</p>

<h2 id="experiment-1-blind-depends-on-the-minimum-mining-rule">Experiment 1. Blind depends on the minimum mining rule</h2>

<p>A 10 × 10 vertical slice of a mine (blocks 1 to 100), with two different shafts (outlined in blue).</p>

<div class="figs">
<figure><img src="/Mining_research/images/10x1x10,%201%20to%20100.png" alt="Shaft 1 → 100 runs diagonally from the top-left corner down through the rich ore band" loading="lazy" /><figcaption>Shaft 1 → 100 runs diagonally from the top-left corner down through the rich ore band.</figcaption></figure>
<figure><img src="/Mining_research/images/10x1x10,%2010%20to%2091.png" alt="Shaft 10 → 91 runs the other way, from the top-right surface block down to the bottom-left" loading="lazy" /><figcaption>Shaft 10 → 91 runs the other way, from the top-right surface block down to the bottom-left.</figcaption></figure>
</div>

<table>
  <thead>
    <tr>
      <th>NPV</th>
      <th style="text-align: right">No minimum, shaft 1→100</th>
      <th style="text-align: right">No minimum, shaft 10→91</th>
      <th style="text-align: right">Minimum 250, shaft 1→100</th>
      <th style="text-align: right">Minimum 250, shaft 10→91</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Partial</td>
      <td style="text-align: right">66,946</td>
      <td style="text-align: right">13,090</td>
      <td style="text-align: right">65,513</td>
      <td style="text-align: right">65,213</td>
    </tr>
    <tr>
      <td>Blind</td>
      <td style="text-align: right">13,090</td>
      <td style="text-align: right">13,090</td>
      <td style="text-align: right">65,652</td>
      <td style="text-align: right">65,652</td>
    </tr>
    <tr>
      <td>Full information</td>
      <td style="text-align: right">68,266</td>
      <td style="text-align: right">68,266</td>
      <td style="text-align: right">67,819</td>
      <td style="text-align: right">67,819</td>
    </tr>
    <tr>
      <td>Greedy</td>
      <td style="text-align: right">61,290</td>
      <td style="text-align: right">61,290</td>
      <td style="text-align: right">61,290</td>
      <td style="text-align: right">61,290</td>
    </tr>
  </tbody>
</table>

<p><strong>What it shows.</strong> With no minimum, blind earns only 13,090, about a fifth of what’s possible. It digs what it can see, finds nothing worth chasing, and stops. With a minimum of 250, blind is forced to keep digging, stumbles onto the ore, and jumps to 65,652. The minimum rule alone is worth more than 50,000 to blind. With no minimum, the shaft through the ore band rescues partial (66,946), but the shaft that misses it leaves partial no better than blind.</p>

<p>A second test on a 5 × 5 × 5 mine (blocks 1 to 125) with a shaft cost of 120 tells the same story.</p>

<div class="figs">
<figure><img src="/Mining_research/images/A%205x5x5,%201,%2032,%2063,%2094,%20125.png" alt="Shaft A (blocks 1, 32, 63, 94, 125) runs corner to corner and hits the shallow ore" loading="lazy" /><figcaption>Shaft A (blocks 1, 32, 63, 94, 125) runs corner to corner and hits the shallow ore.</figcaption></figure>
<figure><img src="/Mining_research/images/C%205x5x5,%2021,%2046,%2071,%2096,%20121.png" alt="Shaft C (blocks 21, 46, 71, 96, 121) goes straight down and misses the ore" loading="lazy" /><figcaption>Shaft C (blocks 21, 46, 71, 96, 121) goes straight down and misses the ore.</figcaption></figure>
</div>

<table>
  <thead>
    <tr>
      <th>Minimum mining</th>
      <th>Scheduler</th>
      <th style="text-align: right">NPV</th>
      <th style="text-align: right">Gain from shaft</th>
      <th style="text-align: right">Gain after 120 cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>Full information</td>
      <td style="text-align: right">67,008</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td>0</td>
      <td>Greedy</td>
      <td style="text-align: right">64,926</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td>0</td>
      <td>Blind</td>
      <td style="text-align: right">14,182</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td>0</td>
      <td>Shaft A (hits shallow ore)</td>
      <td style="text-align: right">57,490</td>
      <td style="text-align: right">+43,308</td>
      <td style="text-align: right"><strong>+43,188</strong></td>
    </tr>
    <tr>
      <td>0</td>
      <td>Shaft B (hits deep ore)</td>
      <td style="text-align: right">14,182</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">−120</td>
    </tr>
    <tr>
      <td>0</td>
      <td>Shaft C (misses)</td>
      <td style="text-align: right">14,182</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">−120</td>
    </tr>
    <tr>
      <td>200 t</td>
      <td>Blind</td>
      <td style="text-align: right">59,669</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td>200 t</td>
      <td>Shaft A</td>
      <td style="text-align: right">59,669</td>
      <td style="text-align: right">0</td>
      <td style="text-align: right">−120</td>
    </tr>
    <tr>
      <td>200 t</td>
      <td>Shaft B</td>
      <td style="text-align: right">57,457</td>
      <td style="text-align: right">−2,212</td>
      <td style="text-align: right">−2,332</td>
    </tr>
    <tr>
      <td>200 t</td>
      <td>Shaft C</td>
      <td style="text-align: right">57,465</td>
      <td style="text-align: right">−2,204</td>
      <td style="text-align: right">−2,324</td>
    </tr>
  </tbody>
</table>

<p>Without a minimum, only the shaft that finds shallow ore pays, and it pays hugely. With a 200 t minimum, blind reaches 59,669 on its own and every shaft either adds nothing or makes things worse.</p>

<h2 id="experiment-2-realistic-capacity-honest-surface-drilling-is-worthless">Experiment 2. Realistic capacity, honest surface. Drilling is worthless</h2>

<p>Next, a mine closer to real production limits.</p>

<ul>
  <li><strong>Mine.</strong> 4 × 5 × 4 = 80 blocks of 50 t each, 7 periods.</li>
  <li><strong>Economics.</strong> Mining costs 10 per tonne, ore earns 100 per tonne, 10% discount rate.</li>
  <li><strong>Capacity.</strong> Dig 20 to 70 t per period, process up to 40 t per period.</li>
  <li><strong>Drill cost.</strong> 50 per block drilled, plus 10% for each layer deeper, paid once before mining.</li>
  <li><strong>Minimum mining.</strong> Each case run with no minimum and with a 20 t minimum.</li>
</ul>

<table>
  <thead>
    <tr>
      <th>Shaft</th>
      <th style="text-align: right">Blocks drilled</th>
      <th>Layers reached</th>
      <th style="text-align: right">Cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Full diagonal (1 → 80)</td>
      <td style="text-align: right">8</td>
      <td>0 to 3</td>
      <td style="text-align: right">460</td>
    </tr>
    <tr>
      <td>Shallow diagonal (5 → 52)</td>
      <td style="text-align: right">6</td>
      <td>0 to 2</td>
      <td style="text-align: right">330</td>
    </tr>
    <tr>
      <td>Vertical (10 → 70)</td>
      <td style="text-align: right">4</td>
      <td>0 to 3</td>
      <td style="text-align: right">230</td>
    </tr>
  </tbody>
</table>

<div class="figs three">
<figure><img src="/Mining_research/images/4x5x4,%20%5B1,%205,%2026,%2030,%2051,%2055,%2076,%2080%5D.png" alt="Full diagonal shaft" loading="lazy" /><figcaption>Full diagonal shaft.</figcaption></figure>
<figure><img src="/Mining_research/images/4x5x4,%20%5B5,%206,%2026,%2031,%2051,%2052%5D.png" alt="Shallow diagonal shaft" loading="lazy" /><figcaption>Shallow diagonal shaft.</figcaption></figure>
<figure><img src="/Mining_research/images/4x5x4,%20%5B10,%2030,%2050,%2070%5D.png" alt="Vertical shaft" loading="lazy" /><figcaption>Vertical shaft.</figcaption></figure>
</div>

<table>
  <thead>
    <tr>
      <th>NPV</th>
      <th style="text-align: right">Blind</th>
      <th style="text-align: right">Diagonal shaft</th>
      <th style="text-align: right">Shallow shaft</th>
      <th style="text-align: right">Vertical shaft</th>
      <th style="text-align: right">Full info</th>
      <th style="text-align: right">Greedy</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>No minimum</td>
      <td style="text-align: right">16,812.54</td>
      <td style="text-align: right">16,812.54</td>
      <td style="text-align: right">16,812.54</td>
      <td style="text-align: right">16,812.54</td>
      <td style="text-align: right">16,812.54</td>
      <td style="text-align: right">16,065.79</td>
    </tr>
    <tr>
      <td>20 t minimum</td>
      <td style="text-align: right">16,812.39</td>
      <td style="text-align: right">16,812.54</td>
      <td style="text-align: right">16,812.39</td>
      <td style="text-align: right">16,812.39</td>
      <td style="text-align: right">16,812.54</td>
      <td style="text-align: right">16,065.79</td>
    </tr>
  </tbody>
</table>

<table>
  <thead>
    <tr>
      <th>Shaft gain minus cost</th>
      <th style="text-align: right">Diagonal</th>
      <th style="text-align: right">Shallow</th>
      <th style="text-align: right">Vertical</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>No minimum</td>
      <td style="text-align: right">−460</td>
      <td style="text-align: right">−330</td>
      <td style="text-align: right">−230</td>
    </tr>
    <tr>
      <td>20 t minimum</td>
      <td style="text-align: right">−459.85</td>
      <td style="text-align: right">−330</td>
      <td style="text-align: right">−230</td>
    </tr>
  </tbody>
</table>

<p><strong>No shaft paid for itself.</strong> The reasons are simple.</p>

<ul>
  <li><strong>Blind was already perfect.</strong> It matched full information, so there was no gap for drilling to close.</li>
  <li><strong>The mill was the bottleneck.</strong> At 40 t per period the mill takes about 280 t over 7 periods, only five or six blocks. The surface alone had 20 blocks of good ore (grades 0.54 to 0.75), all visible from the start. The best plan never needed to go deep.</li>
  <li><strong>Deep information went unused.</strong> Most lower blocks were never even uncovered, so knowing their grades changed nothing.</li>
  <li><strong>The surface told the truth.</strong> Rich surface sat over rich ground, so there was nothing misleading for a shaft to correct.</li>
</ul>

<h2 id="experiment-3-same-mine-hidden-ore-now-drilling-pays">Experiment 3. Same mine, hidden ore. Now drilling pays</h2>

<p>Same mine, capacities and costs. Only the grades change. The surface now shows a small <strong>decoy</strong> of ore surrounded by waste, while a rich <strong>lens</strong> of ore sits just below, under most of the waste.</p>

<div class="figs three">
<figure><img src="/Mining_research/images/4x5x4,%20L%20%5B1,%205,%2026,%2030,%2051,%2055,%2076,%2080%5D.png" alt="Full diagonal shaft on the lens layout" loading="lazy" /><figcaption>Full diagonal shaft on the lens layout.</figcaption></figure>
<figure><img src="/Mining_research/images/4x5x4,%20L%20%5B5,%206,%2026,%2031,%2051,%2052%5D.png" alt="Shallow diagonal shaft on the lens layout" loading="lazy" /><figcaption>Shallow diagonal shaft on the lens layout.</figcaption></figure>
<figure><img src="/Mining_research/images/4x5x4,%20L%20%5B10,%2030,%2050,%2070%5D.png" alt="Vertical shaft on the lens layout" loading="lazy" /><figcaption>Vertical shaft on the lens layout.</figcaption></figure>
</div>

<p>Shaft costs here are 92 (diagonal), 66 (shallow) and 46 (vertical).</p>

<table>
  <thead>
    <tr>
      <th>NPV</th>
      <th style="text-align: right">Blind</th>
      <th style="text-align: right">Diagonal</th>
      <th style="text-align: right">Shallow</th>
      <th style="text-align: right">Vertical</th>
      <th style="text-align: right">Full information</th>
      <th style="text-align: right">Greedy</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>No minimum</strong></td>
      <td style="text-align: right">1,273</td>
      <td style="text-align: right">10,167</td>
      <td style="text-align: right">10,167</td>
      <td style="text-align: right">1,273</td>
      <td style="text-align: right">10,223</td>
      <td style="text-align: right">10,174</td>
    </tr>
    <tr>
      <td>Shaft gain minus cost</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"><strong>+8,802</strong></td>
      <td style="text-align: right"><strong>+8,828</strong></td>
      <td style="text-align: right">−46</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"> </td>
    </tr>
    <tr>
      <td><strong>20 t minimum</strong></td>
      <td style="text-align: right">5,861</td>
      <td style="text-align: right">8,489</td>
      <td style="text-align: right">8,151</td>
      <td style="text-align: right">6,677</td>
      <td style="text-align: right">10,223</td>
      <td style="text-align: right">10,174</td>
    </tr>
    <tr>
      <td>Shaft gain minus cost</td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"><strong>+2,537</strong></td>
      <td style="text-align: right"><strong>+2,224</strong></td>
      <td style="text-align: right"><strong>+771</strong></td>
      <td style="text-align: right"> </td>
      <td style="text-align: right"> </td>
    </tr>
  </tbody>
</table>

<p><strong>No minimum.</strong> Blind digs the decoy and stops, because everything else it can see is waste (1,273). The diagonal and shallow shafts each spot two rich blocks just under the surface. That’s enough to make clearing the waste worthwhile, and once digging starts the rest of the lens comes into view. Both finish within 0.6% of perfect information for a shaft costing under 100. The vertical shaft finds only one rich block, which isn’t worth clearing nine waste blocks for, so it ends up exactly like blind.</p>

<p><strong>20 t minimum.</strong> Blind is forced to dig and finds part of the lens on its own (5,861), so there’s less left for a shaft to add. All three shafts still pay, in the order of how much shallow ore they find. Diagonal and shallow are effectively tied. An earlier run had shallow ahead, which is normal sensitivity in the rolling MILP when two options are this close.</p>

<p><strong>Greedy nearly matches perfect information here (10,174).</strong> It always digs at full speed, and the lens lies under most of the waste, so wherever it digs it soon hits ore. On the decoy layouts in Experiment 1 the same habit wasted effort.</p>

<h2 id="why-the-minimum-helps-blind-but-hurts-the-scheduler-that-drilled">Why the minimum helps blind but hurts the scheduler that drilled</h2>

<p>One result looked odd. With the lens layout, the diagonal shaft earned <strong>less</strong> with the 20 t minimum (8,489) than without it (10,167), even though it dug well over 20 t each period in both runs. Comparing the two runs period by period:</p>

<table>
  <thead>
    <tr>
      <th>Period</th>
      <th>No minimum, tonnes dug</th>
      <th>20 t minimum, tonnes dug</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>70 t (decoy plus the waste above the ore)</td>
      <td>20 t (decoy only)</td>
    </tr>
    <tr>
      <td>2</td>
      <td>70 t</td>
      <td>50 t</td>
    </tr>
    <tr>
      <td>3 to 7</td>
      <td>70 t</td>
      <td>about 70 t</td>
    </tr>
  </tbody>
</table>

<p>With the minimum, it reached the ore about two periods later, and that delay cost about 1,700.</p>

<p><strong>The cause is how the minimum is written into the plan.</strong> Each time the MILP re-plans, it makes a full plan for all remaining periods, and the minimum currently applies to <strong>every</strong> period in that plan, not just the one it’s about to carry out.</p>

<p>Picture a site manager who can see some rock now and will discover more as they dig. If they must promise in advance to dig at least 20 t in every future period, and the only rock they can see for those future periods is waste, they’ll think, “I’ll need waste to fill those future quotas anyway, so I’ll save the waste above the ore for later.” That spreads the stripping out and delays reaching the ore. What they don’t know yet is that the moment they start digging, the ore lens will appear and fill those later periods on its own.</p>

<p>If instead the minimum only applied to the <strong>current</strong> period, the manager would think, “I know where the ore is. I’ll clear the waste now, meet this period’s 20 t, and decide next period when I can see more.” The rule is still met every period, because each period becomes the current one in turn. It just isn’t pre-committed into a future the model can’t see yet.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>P1</th>
      <th>P2</th>
      <th>P3</th>
      <th>P4</th>
      <th>P5</th>
      <th>P6</th>
      <th>P7</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>A. Minimum in every planned period.</strong> The plan reserves at least 20 t of work in every period and can spread out the stripping.</td>
      <td>≥20</td>
      <td>≥20</td>
      <td>≥20</td>
      <td>≥20</td>
      <td>≥20</td>
      <td>≥20</td>
      <td>≥20</td>
    </tr>
    <tr>
      <td><strong>B. Minimum in the current period only.</strong> The plan is free to front-load stripping and re-plans each period.</td>
      <td>≥20</td>
      <td>free</td>
      <td>free</td>
      <td>free</td>
      <td>free</td>
      <td>free</td>
      <td>free</td>
    </tr>
  </tbody>
</table>

<p><em>Illustrative only. Actual digging still depends on grades, access and mill capacity. The future plan must still respect what is physically possible to dig.</em></p>

<p><strong>Why this cuts opposite ways.</strong></p>

<ul>
  <li><strong>Blind doesn’t know where the hidden ore is.</strong> The minimum forces it to dig and discover valuable blocks it would otherwise never reach. So the minimum helps.</li>
  <li><strong>Partial already knows where the ore is, from the shaft.</strong> Applying the minimum to every future period makes it spread out the waste removal instead of going straight for that known ore. So the minimum hurts.</li>
</ul>

<p>In one sentence, “minimum in every planned period” means <em>plan to dig at least 20 t every period, including the future</em>, while “minimum in the current period only” means <em>meet this period’s target now, then re-plan the future once you can see more</em>.</p>

<p>This is a limit of planning only over visible blocks, not a problem with the shaft itself.</p>

<h2 id="what-this-means">What this means</h2>

<p>Drilling pays only when two conditions hold together.</p>

<ol>
  <li><strong>The mine must actually dig below the surface within the plan.</strong> That needs enough capacity compared with the mine’s size, or a long enough horizon.</li>
  <li><strong>The surface must be a poor guide to what’s below.</strong> If the surface is honest, blind already does as well as anyone.</li>
</ol>

<p>When both hold, one well-placed shallow shaft can be worth many times its cost. When either fails, the same shaft is wasted money. <strong>The value of drilling comes from the decision it changes, not from the information itself.</strong></p>

<h2 id="next-tomorrow">Next (tomorrow)</h2>

<ul>
  <li><strong>Test the minimum in every period vs the minimum in the current period only.</strong> Change the rolling MILP so the 20 t minimum applies only to the period being carried out, re-run the lens layout for blind and all three shafts, and compare with today’s numbers. Expect partial to recover towards 10,167 with the minimum. Check that blind still benefits and that every period still meets 20 t when it actually runs. Also handle any period where 20 t isn’t reachable.</li>
  <li>Change from block structure format and a very large instance.</li>
</ul>]]></content><author><name>Mustavi Ibne Masum</name></author><category term="drilling" /><category term="minimum-mining" /><category term="milp" /><category term="greedy" /><category term="capacity" /><summary type="html"><![CDATA[]]></summary></entry></feed>