stcoder-qwen25-14b-gguf / article_stcoder-qwen25-14b.html
Mikrodev's picture
Upload article_stcoder-qwen25-14b.html with huggingface_hub
37339cc verified
Raw
History Blame Contribute Delete
97.2 kB
<title>stcoder-qwen25-14b &mdash; Mikrodev STCoder for PLC Structured Text</title>
<style>
:root{--bg:#e9edf1;--paper:#fff;--paper2:#f4f7f9;--rule:#d3dae1;--ink:#131a21;--mid:#5c6b78;--dim:#8b98a5;
--teal:#12657f;--teal-soft:#d6eef1;--teal-line:#93cfd7;--amber:#9a6512;--amber-soft:#f2e4c9;--amber-line:#d8bb84;
--red:#ac3527;--red-soft:#f6dcd7;--code-bg:#f6f8fa;--code-ink:#1e2831;
--shadow:0 1px 1px rgba(19,26,33,.04),0 8px 26px rgba(19,26,33,.06);}
@media (prefers-color-scheme:dark){:root{--bg:#0b1014;--paper:#131b22;--paper2:#18222a;--rule:#25313b;
--ink:#d5dee6;--mid:#8c9aa7;--dim:#5f6d79;--teal:#3ba3c6;--teal-soft:#10353a;--teal-line:#1d5b65;
--amber:#d09a3f;--amber-soft:#382c12;--amber-line:#6b5322;--red:#e07963;--red-soft:#3d1d17;
--code-bg:#0e161c;--code-ink:#c2ccd6;--shadow:0 1px 1px rgba(0,0,0,.4),0 10px 30px rgba(0,0,0,.34);}}
:root[data-theme=light]{--bg:#e9edf1;--paper:#fff;--paper2:#f4f7f9;--rule:#d3dae1;--ink:#131a21;--mid:#5c6b78;
--dim:#8b98a5;--teal:#12657f;--teal-soft:#d6eef1;--teal-line:#93cfd7;--amber:#9a6512;--amber-soft:#f2e4c9;
--amber-line:#d8bb84;--red:#ac3527;--red-soft:#f6dcd7;--code-bg:#f6f8fa;--code-ink:#1e2831;
--shadow:0 1px 1px rgba(19,26,33,.04),0 8px 26px rgba(19,26,33,.06);}
:root[data-theme=dark]{--bg:#0b1014;--paper:#131b22;--paper2:#18222a;--rule:#25313b;--ink:#d5dee6;--mid:#8c9aa7;
--dim:#5f6d79;--teal:#3ba3c6;--teal-soft:#10353a;--teal-line:#1d5b65;--amber:#d09a3f;--amber-soft:#382c12;
--amber-line:#6b5322;--red:#e07963;--red-soft:#3d1d17;--code-bg:#0e161c;--code-ink:#c2ccd6;
--shadow:0 1px 1px rgba(0,0,0,.4),0 10px 30px rgba(0,0,0,.34);}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);line-height:1.6;-webkit-font-smoothing:antialiased;
font-family:"Segoe UI",system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif}
.sheet{max-width:1080px;margin:0 auto;padding:clamp(18px,3.5vw,50px) clamp(14px,3.5vw,42px) 90px}
code,pre,.mono{font-family:"Cascadia Code","JetBrains Mono",Consolas,ui-monospace,SFMono-Regular,Menlo,monospace}
.num,table,.bar b{font-variant-numeric:tabular-nums}
.mast{border-top:3px solid var(--ink);padding-top:14px}
.kick{display:flex;flex-wrap:wrap;gap:13px;align-items:baseline;font-family:"Cascadia Code",Consolas,monospace;
font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mid)}
.kick .id{color:var(--teal);font-weight:700}
h1{font-size:clamp(28px,5vw,47px);line-height:1.03;letter-spacing:-.025em;margin:.3em 0 .16em;font-weight:800;
text-wrap:balance;max-width:24ch}
.sf{font-size:clamp(16px,2vw,19px);color:var(--mid);max-width:66ch;margin:0}
.sf b{color:var(--ink);font-weight:650}
.spec{display:grid;grid-template-columns:repeat(auto-fit,minmax(128px,1fr));gap:1px;background:var(--rule);
border:1px solid var(--rule);border-radius:10px;overflow:hidden;margin:28px 0 0;box-shadow:var(--shadow)}
.spec div{background:var(--paper);padding:12px 14px}
.spec .k{font-family:"Cascadia Code",Consolas,monospace;font-size:10px;letter-spacing:.13em;
text-transform:uppercase;color:var(--dim)}
.spec .v{font-size:19px;font-weight:750;letter-spacing:-.01em;margin-top:3px;word-break:break-word}
.spec .v small{font-size:11.5px;font-weight:500;color:var(--mid);letter-spacing:0;display:block}
section{margin-top:clamp(42px,6vw,70px)}
.sh{display:flex;gap:13px;align-items:baseline;border-bottom:1px solid var(--rule);padding-bottom:9px;margin-bottom:18px}
.sh .sn{font-family:"Cascadia Code",Consolas,monospace;font-size:12px;font-weight:700;color:var(--teal);letter-spacing:.08em}
h2{font-size:clamp(19px,2.6vw,26px);margin:0;letter-spacing:-.015em;font-weight:750;text-wrap:balance}
h3{font-size:16px;margin:24px 0 7px;font-weight:700}
p{max-width:70ch}p.lead{color:var(--mid)}
ul,ol{max-width:72ch;padding-left:20px}li{margin:.3em 0}
.card{background:var(--paper);border:1px solid var(--rule);border-radius:12px;padding:18px 20px;
margin:18px 0;box-shadow:var(--shadow)}
.card.hi{border-left:3px solid var(--teal)}
.card.warn{border-left:3px solid var(--red)}
.card h3{margin-top:0}
.tw{overflow-x:auto;border:1px solid var(--rule);border-radius:11px;box-shadow:var(--shadow);margin:18px 0}
table{border-collapse:collapse;width:100%;background:var(--paper);font-size:13.2px;min-width:600px}
th,td{padding:10px 13px;text-align:right;border-bottom:1px solid var(--rule);vertical-align:top}
th:first-child,td:first-child{text-align:left}
thead th{background:var(--paper2);font-size:10.4px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);font-weight:700}
tbody tr:last-child td{border-bottom:none}
td.ft{color:var(--teal);font-weight:700}td.bs{color:var(--amber);font-weight:650}
.pick{display:grid;gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:11px;
overflow:hidden;box-shadow:var(--shadow)}
.pick .row{background:var(--paper);padding:14px 17px;display:grid;grid-template-columns:120px 150px 1fr;gap:14px;align-items:start}
@media(max-width:700px){.pick .row{grid-template-columns:1fr;gap:5px}}
.pick .vram{font-weight:750;font-size:15px}
.pick .q{font-family:"Cascadia Code",Consolas,monospace;font-size:12.6px;font-weight:700;color:var(--teal)}
.pick .q small{display:block;color:var(--mid);font-weight:400;font-size:11px}
.pick .why{color:var(--mid);font-size:13.5px}
.pick .why em{color:var(--amber);font-style:normal;font-weight:650}
.steps{counter-reset:s;list-style:none;padding:0}
.steps li{counter-increment:s;position:relative;padding-left:38px;margin-bottom:16px}
.steps li::before{content:counter(s);position:absolute;left:0;top:0;width:25px;height:25px;border-radius:50%;
background:var(--teal-soft);color:var(--teal);font-family:"Cascadia Code",Consolas,monospace;font-size:12px;
font-weight:700;display:flex;align-items:center;justify-content:center}
.steps b{display:block;font-size:14.5px}
.steps p{margin:.2em 0;color:var(--mid);font-size:13.5px}
pre{margin:9px 0 0;padding:12px 14px;overflow-x:auto;background:var(--code-bg);color:var(--code-ink);
font-size:12.3px;line-height:1.55;border-radius:8px;border:1px solid var(--rule)}
pre.plain{border-radius:0;border:none;margin:0}
pre .bad{background:var(--red-soft);color:var(--red);font-weight:700;border-radius:3px;padding:0 2px}
pre .good{background:var(--teal-soft);color:var(--teal);font-weight:700;border-radius:3px;padding:0 2px}
.pgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(292px,1fr));gap:13px}
.pc{background:var(--paper);border:1px solid var(--rule);border-radius:11px;padding:14px 16px;box-shadow:var(--shadow)}
.pc .q{font-size:14px;font-weight:650;margin-bottom:6px}
.pc .q::before{content:"\201C"}.pc .q::after{content:"\201D"}
.pc .e{font-size:13px;color:var(--mid)}
.pc .n{font-size:12.2px;color:var(--dim);margin-top:6px;font-style:italic}
.fig{background:var(--paper);border:1px solid var(--rule);border-radius:12px;padding:19px 21px 15px;
margin:20px 0;box-shadow:var(--shadow)}
.fc{font-family:"Cascadia Code",Consolas,monospace;font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
color:var(--dim);margin-bottom:4px}
.ftl{font-weight:700;font-size:15px;margin-bottom:13px}
.legend{display:flex;flex-wrap:wrap;gap:15px;font-size:12.3px;color:var(--mid);margin:0 0 14px}
.legend span{display:inline-flex;align-items:center;gap:6px}
.sw{width:11px;height:11px;border-radius:2px}.sw.b{background:var(--amber)}.sw.f{background:var(--teal)}
.sw.r{background:var(--red)}
.frow{display:grid;grid-template-columns:180px 1fr;gap:13px;align-items:center;margin-bottom:14px}
@media(max-width:640px){.frow{grid-template-columns:1fr;gap:5px}}
.flab{font-size:13px;font-weight:650}
.flab small{display:block;font-weight:400;color:var(--mid);font-size:10.6px;font-family:"Cascadia Code",Consolas,monospace}
.fbars{display:flex;flex-direction:column;gap:5px}
.bar{position:relative;height:23px;background:var(--paper2);border:1px solid var(--rule);border-radius:5px;overflow:hidden}
.bar>i{position:absolute;inset:0 auto 0 0;display:block}
.bar.base>i{background:var(--amber)}.bar.base2>i{background:var(--amber);opacity:.6}
.bar.ft>i{background:var(--teal)}.bar.ft2>i{background:var(--teal);opacity:.6}
.bar>b{position:absolute;right:8px;top:0;height:23px;display:flex;align-items:center;font-size:11.4px;
font-weight:700;color:var(--ink)}
.strip{display:flex;gap:2px;height:26px}
.strip i{flex:1 1 0;border-radius:1.5px;min-width:3px}
.strip i.ok{background:var(--teal)}.strip i.vi{background:var(--amber)}.strip i.nc{background:var(--red)}
.diff{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:820px){.diff{grid-template-columns:1fr}}
.panel{background:var(--paper);border:1px solid var(--rule);border-radius:11px;overflow:hidden;box-shadow:var(--shadow)}
.panel.base{border-top:3px solid var(--amber-line)}.panel.ft{border-top:3px solid var(--teal-line)}
.ph{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:9px 13px;border-bottom:1px solid var(--rule)}
.tag{font-family:"Cascadia Code",Consolas,monospace;font-size:10.3px;letter-spacing:.12em;text-transform:uppercase;font-weight:700}
.tag.base{color:var(--amber)}.tag.ft{color:var(--teal)}
.badge{font-family:"Cascadia Code",Consolas,monospace;font-size:10.7px;font-weight:700;padding:3px 8px;
border-radius:99px;white-space:nowrap}
.badge.ok{background:var(--teal-soft);color:var(--teal)}.badge.warn{background:var(--amber-soft);color:var(--amber)}
.pv{display:flex;flex-wrap:wrap;gap:5px;padding:9px 13px;border-top:1px solid var(--rule)}
.pv code{font-size:10.5px;padding:2px 7px;border-radius:99px}
.pv code.v{background:var(--red-soft);color:var(--red)}.pv code.c{background:var(--teal-soft);color:var(--teal)}
.note{font-size:13.2px;color:var(--mid);border-left:2px solid var(--rule);padding-left:13px;margin:12px 0 0;max-width:82ch}
.note b{color:var(--ink);font-weight:650}
.chip{display:inline-block;font-family:"Cascadia Code",Consolas,monospace;font-size:10.3px;font-weight:700;
padding:2px 7px;border-radius:99px}
.chip.ok{background:var(--teal-soft);color:var(--teal)}.chip.vi{background:var(--amber-soft);color:var(--amber)}
.chip.nc{background:var(--red-soft);color:var(--red)}
.sib{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:11px;margin-top:14px}
.sib a{display:block;background:var(--paper);border:1px solid var(--rule);border-radius:10px;padding:12px 14px;
text-decoration:none;color:inherit;box-shadow:var(--shadow)}
.sib a.self{border-color:var(--teal);background:var(--teal-soft)}
.sib .sn2{font-family:"Cascadia Code",Consolas,monospace;font-size:11.6px;font-weight:700;color:var(--teal)}
.sib .sd{font-size:12.6px;color:var(--mid);margin-top:2px}
footer{margin-top:60px;padding-top:19px;border-top:1px solid var(--rule);font-size:12.4px;color:var(--dim);max-width:82ch}
footer code{color:var(--mid);font-size:11.6px}
</style>
<div class="sheet">
<div class="mast">
<div class="kick"><span class="id">Mikrodev/stcoder-qwen25-14b-gguf</span><span>Fine-tuned from Qwen/Qwen2.5-Coder-14B-Instruct</span>
<span>apache-2.0</span><span>GGUF &middot; Ollama / llama.cpp</span></div>
<h1>stcoder-qwen25-14b</h1>
<p class="sf">stcoder-qwen25-14b is the rule-compliance member of the line: a chat-only fine-tune of Qwen2.5-Coder-14B-Instruct that writes body-only Mikrodev LogicStudio Structured Text, and the only one of the four fine-tunes that was 100% rule-clean with zero prose in both prompt conditions. The line default is stcoder-qwen25-7b at Q8_0; choose this one on 12 GB of VRAM or more when clean dialect output is what you are buying.</p>
<p class="sf" style="font-size:15px;margin-top:10px">This is a fine-tune for the Structured Text coding rules of Advance Logic Builder (ALB), the IEC 61131-3 engineering environment developed by Mikrodev and shipped as Mikrodev LogicStudio. It follows Mikrodev&#x27;s ST dialect, not generic IEC 61131-3 and not another vendor&#x27;s conventions.</p>
</div>
<div class="spec">
<div><div class="k">Base model</div><div class="v" style="font-size:14px">Qwen2.5-Coder-14B-Instruct<small>Qwen/Qwen2.5-Coder-14B-Instruct</small></div></div>
<div><div class="k">Builds</div><div class="v">3<small>Q4_K_M / Q6_K / Q8_0</small></div></div>
<div><div class="k">Smallest</div><div class="v">8.4 GiB<small>Q4_K_M</small></div></div>
<div><div class="k">Recommended</div><div class="v" style="color:var(--teal)">Q8_0<small>14.62 GiB</small></div></div>
<div><div class="k">Dialect-clean</div><div class="v" style="color:var(--teal)">100.0%<small>base: 0.0%</small></div></div>
<div><div class="k">Tool calling</div><div class="v" style="color:var(--red);font-size:15px">Not supported<small>chat only</small></div></div>
</div>
<!-- 1. PICK -->
<section>
<div class="sh"><span class="sn">1</span><h2>Which build should you download?</h2></div>
<p class="lead">Start from the graphics memory you have. Every build below is the same fine-tune;
they differ only in how heavily the weights are compressed.</p>
<div class="pick"><div class="row"><div class="vram">16.6 GiB free VRAM</div><div class="q">Q8_0<small>qwen25_coder_14b-tc.q8_0.gguf &middot; 14.62 GiB</small></div><div class="why">recommended <em>needs a 24 GB-class card. A 16 GiB card cannot fit it - including the RTX 5080 this model&#x27;s own evaluation ran on, which is why the measured numbers below are Q6_K. On 16 GiB or less take Q6_K</em></div></div><div class="row"><div class="vram">13.3 GiB free VRAM</div><div class="q">Q6_K<small>qwen25_coder_14b-tc.q6_k.gguf &middot; 11.29 GiB</small></div><div class="why">fine &mdash; the build this study ran</div></div><div class="row"><div class="vram">10.4 GiB free VRAM</div><div class="q">Q4_K_M<small>qwen25_coder_14b-tc.q4_k_m.gguf &middot; 8.37 GiB</small></div><div class="why">not recommended</div></div></div>
<div class="card hi"><h3>Short answer</h3>
<p>Download <b>Q8_0</b> (<code>qwen25_coder_14b-tc.q8_0.gguf</code>, 14.62 GiB).
Q8_0 is the smallest build with no practically measurable loss against the trained weights - it is the precision we recommend for real work. Free VRAM needed: about
16.6 GiB &mdash; the file plus the 8192-token context and runtime.</p></div>
<div class="card warn"><h3>Why not Q4_K_M</h3>
<p>Q4_K_M halves the bits per weight again. The only capability we measured across quantisations degraded sharply - tool-call composite 0.816 at Q8_0 against 0.642 at Q4_K_M, measured on the Qwen3.5-9B family, the only one of the four where tool-calling worked at all - and low-bit quantisation is a known source of drift and hallucination on long or unusual requests. Our 15-prompt code study never ran at Q4, so we have no measured code-quality figure for it: treat this as a precaution, not a measured code-quality gap.</p></div>
<p class="note"><b>Also published for this model.</b> The LoRA adapter it was trained as
(<code>lora_adapter/</code> in the same repository) so you can merge it at any
precision or continue training from it, and the f16 merged weights
(f16 is the merged fine-tune at full precision: the reference build, for evaluation or for producing your own quantisations.). Ready-to-run Ollama <code>Modelfile</code>s ship next to every build:
correct chat template, stop tokens, <code>num_ctx 8192</code> &mdash; the sequence length this
model was fine-tuned at &mdash; and the system prompt.</p>
<div class="tw"><table>
<thead><tr><th>File</th><th>Quantisation</th><th>Size</th><th>Fit on 16 GB</th><th style="text-align:left">Note</th></tr></thead>
<tbody><tr><td><code>qwen25_coder_14b-tc.q4_k_m.gguf</code></td><td class="num">Q4_K_M</td><td class="num">8.37 GiB</td><td class="num">solid</td><td></td></tr><tr><td><code>qwen25_coder_14b-tc.q6_k.gguf</code></td><td class="num">Q6_K</td><td class="num">11.29 GiB</td><td class="num">tight</td><td>tested in this study</td></tr><tr><td><code>qwen25_coder_14b-tc.q8_0.gguf</code></td><td class="num">Q8_0</td><td class="num">14.62 GiB</td><td class="num">no, 24 GB+</td><td>recommended</td></tr></tbody>
</table></div>
</section>
<!-- 2. WHO -->
<section>
<div class="sh"><span class="sn">2</span><h2>Is this the right model for you?</h2></div>
<p>First, the thing every card in this line states up front: tool calling was trained and evaluated for this line, but only the Qwen3.5-9B family learned it (tool-call composite 0.816 at Q8_0, 0.805 at Q6_K, falling to 0.642 at Q4_K_M) while the code-focused families scored 0.02-0.06 and the Gemma family 0.00. Because it was not usable across the line, tool calling was dropped as a product decision and these builds are published and supported as chat-only models.
If you have not decided which model to take, take stcoder-qwen25-7b at Q6_K (5.82 GiB). It is the line default: fastest per answer (5.8 s), smallest, 100% delivery, and the highest ChrF among models that answered every prompt (38.5 at 15/15).
Choose stcoder-qwen25-14b instead, on 12 GB or more, for one measured reason: in the 240-run head-to-head it was the only fine-tune at 100% clean in both the minimal and the full-prompt condition (7B 93.3/100, stcoder-qwen35-9b 93.3/100, stcoder-gemma4-12b 86.7/80), with 0% prose leakage in both and 100% delivery in every arm. It is also the most economical of the four in output length at the minimal prompt: 288 tokens mean against 340 for the 7B, 401 for the Gemma and 1227 for the 9B. What you give up is measurable too: it has the lowest ChrF of the four at the minimal prompt (34.9, against 7B 38.5 at 15/15, stcoder-qwen35-9b 40.9 at 14/15, stcoder-gemma4-12b 35.9 at 15/15), and it is the slowest of the four per token at roughly 50 tok/s, which puts it at 8.6 s per answer - slower than the 7B&#x27;s 5.8 s, but not the slowest per answer in the set (that is stcoder-qwen35-9b at 17.2 s). Hardware: Q4_K_M is 8.37 GiB and Q6_K is 11.29 GiB, so an 8 GB card is out.
The other two, by their one measured difference: stcoder-qwen35-9b is the reasoning-family option and had the highest ChrF of the four (40.9) but delivered only 14 of 15 at the minimal prompt, so its ChrF carries a smaller denominator than everyone else&#x27;s; stcoder-gemma4-12b is there for non-Qwen lineage diversity and is the weakest fine-tuning case in the set.
Composites are within noise across the three Qwen fine-tunes (stcoder-qwen35-9b 0.845, stcoder-qwen25-7b 0.844, stcoder-qwen25-14b 0.842), so do not choose on that number.</p>
<div class="sib"><a class="" href="https://huggingface.co/Mikrodev/stcoder-qwen25-7b-gguf"><div class="sn2">Mikrodev/stcoder-qwen25-7b-gguf</div><div class="sd">7B &middot; default pick, fastest per answer</div></a><a class="self" href="https://huggingface.co/Mikrodev/stcoder-qwen25-14b-gguf"><div class="sn2">Mikrodev/stcoder-qwen25-14b-gguf</div><div class="sd">14B &middot; only one 100% clean in both conditions &middot; you are here</div></a><a class="" href="https://huggingface.co/Mikrodev/stcoder-qwen35-9b-gguf"><div class="sn2">Mikrodev/stcoder-qwen35-9b-gguf</div><div class="sd">9B &middot; reasoning family, highest ChrF when it answers</div></a><a class="" href="https://huggingface.co/Mikrodev/stcoder-gemma4-12b-gguf"><div class="sn2">Mikrodev/stcoder-gemma4-12b-gguf</div><div class="sd">12B &middot; non-Qwen lineage, weakest fine-tune case</div></a></div>
<div class="fig">
<div class="fc">Table &mdash; the four models on identical measurements</div>
<div class="ftl">Same 15 requests, realistic prompt, greedy decoding. ChrF is averaged over
<i>delivered</i> replies so the denominator is the same for every model.</div>
<div class="tw"><table>
<thead><tr><th>Model</th><th>Clean</th><th>Delivered</th><th>ChrF</th><th>Composite</th>
<th>Out tok</th><th>Per reply</th><th>Recommended build</th></tr></thead>
<tbody><tr><td><code>stcoder-qwen25-7b</code><br><span style="color:var(--dim);font-size:11.5px">7B &middot; default pick, fastest per answer</span></td><td class="num">93.3%</td><td class="num">15/15</td><td class="num">38.5</td><td class="num">0.844</td><td class="num">340</td><td class="num">5.8s</td><td class="num">Q8_0 &middot; 7.54 GiB</td></tr><tr><td><code>stcoder-qwen25-14b</code> (this model)<br><span style="color:var(--dim);font-size:11.5px">14B &middot; only one 100% clean in both conditions</span></td><td class="num ft">100.0%</td><td class="num">15/15</td><td class="num">34.9</td><td class="num">0.842</td><td class="num">288</td><td class="num">8.6s</td><td class="num">Q8_0 &middot; 14.62 GiB</td></tr><tr><td><code>stcoder-qwen35-9b</code><br><span style="color:var(--dim);font-size:11.5px">9B &middot; reasoning family, highest ChrF when it answers</span></td><td class="num">93.3%</td><td class="num">14/15</td><td class="num">40.9</td><td class="num">0.845</td><td class="num">1227</td><td class="num">17.2s</td><td class="num">Q8_0 &middot; 9.11 GiB</td></tr><tr><td><code>stcoder-gemma4-12b</code><br><span style="color:var(--dim);font-size:11.5px">12B &middot; non-Qwen lineage, weakest fine-tune case</span></td><td class="num">86.7%</td><td class="num">15/15</td><td class="num">35.9</td><td class="num">0.83</td><td class="num">401</td><td class="num">8.5s</td><td class="num">Q8_0 &middot; 11.80 GiB</td></tr></tbody>
</table></div>
<p class="note"><b>How to read this.</b> The 14B has the highest clean rate (100%). The 9B has
the highest ChrF among delivered replies (40.9) but delivered on only 14 of 15, so at full
delivery the 7B leads (38.5). Composites for the 9B, 7B and 14B (0.845 / 0.844 / 0.842) are
within noise of each other. Sizes are <b>GiB</b> throughout.
<b>If you are undecided, take stcoder-qwen25-7b at Q8_0.</b></p>
</div>
<div class="card warn"><h3>Read this before you start</h3>
<ul>
<li><b>Chat only &mdash; no tool calling.</b> You describe what you need and the model writes
Structured Text. Tool calling was trained and evaluated for this line: the Qwen3.5-9B family
did learn it (tool-call composite 0.816 at Q8_0, falling to 0.642 at Q4_K_M) while the
code-focused families scored 0.02&ndash;0.06 and the Gemma family exactly 0.00. Because it was
not usable across the line, <b>tool calling was dropped as a product decision</b> and these
builds are published and supported as chat models. Do not build an agent on them.</li>
<li><b>Always compile before deployment.</b> The model produces dialect-correct code, which is
not the same as correct control logic. Compile it in LogicStudio and review the logic.</li>
<li><b>It is a small local model.</b> Expect to iterate on a prompt, not to receive a finished
project.</li>
</ul>
</div>
</section>
<!-- 3. SETUP -->
<section>
<div class="sh"><span class="sn">3</span><h2>Set-up, step by step</h2></div>
<ol class="steps"><li><b>Install Ollama</b><p>Any recent release. Ollama supplies the GGUF loader, the local HTTP API and the chat REPL used below. llama.cpp works equally well if you prefer it; the Modelfile parameters map onto its command-line flags.</p><pre># Windows / macOS: download the installer from ollama.com
# Linux:
curl -fsSL https://ollama.com/install.sh | sh</pre></li><li><b>Download one GGUF file</b><p>Take Q4_K_M (8.37 GiB) on 12 GB of VRAM, or Q6_K (11.29 GiB) on 16 GB or more. You need one file, not the whole repository.</p><pre>pip install -U &quot;huggingface_hub[cli]&quot;
# 12 GB VRAM - recommended default
hf download Mikrodev/stcoder-qwen25-14b-gguf qwen25_coder_14b-tc.q4_k_m.gguf --local-dir .
# 16 GB+ VRAM - the quant the published numbers were measured on
# hf download Mikrodev/stcoder-qwen25-14b-gguf qwen25_coder_14b-tc.q6_k.gguf --local-dir .</pre></li><li><b>Write a Modelfile next to the GGUF</b><p>The sampling settings and the system prompt the model was evaluated with. No chat template: Ollama formats the chat from the template stored inside the GGUF - the one this model was trained with - and on current Ollama a TEMPLATE line in this file is ignored anyway (verified by comparing prompt token counts with and without one). Edit the FROM line if you downloaded Q6_K. Do not add the dialect rule book to the system prompt - the weights already hold it, and the study found the full rule book changed rule compliance not at all.</p><pre>cat &gt; Modelfile &lt;&lt;&#x27;EOF&#x27;
FROM ./qwen25_coder_14b-tc.q4_k_m.gguf
PARAMETER temperature 0.2
PARAMETER top_p 0.95
PARAMETER top_k 20
PARAMETER repeat_penalty 1.0
PARAMETER num_ctx 8192
# the published numbers were measured with num_ctx 16384 / num_predict 8192
# num_predict 2048 is a practical local default (longest reply in the study: 708 tokens); raise it to 8192 to reproduce them or if a long block truncates
PARAMETER num_predict 2048
SYSTEM &quot;&quot;&quot;You are STCoder, a conversational coding assistant for the Mikrodev LogicStudio IDE and an expert in IEC 61131-3 Structured Text. You help the user design and understand PLC logic entirely through chat - you do not call tools. Answer the user&#x27;s message directly and helpfully. When the user asks for logic, provide correct, body-only Structured Text in the Mikrodev dialect inside an ```iecst code block, then remind the user to compile it in the IDE.&quot;&quot;&quot;
EOF</pre></li><li><b>Register the model with Ollama</b><p>This copies the GGUF into Ollama&#x27;s store and binds the template, parameters and system prompt to the tag. Takes a minute or two for a file this size. For reproduction, the tags actually run in the study were: fine-tunes stcoder_qwen25-7b:q8, stcoder_qwen25-14b:q6, stcoder_qwen35-9b:q8, stcoder_gemma4-12b:q6; bases qwen2.5-coder:7b, qwen2.5-coder:14b, qwen3.5:9b, gemma4:12b. The run data records no quantisation for the base arms, so calling them &quot;effectively Q4&quot; is an inference from the Ollama library default, not a recorded value.</p><pre>ollama create stcoder-qwen25-14b:q4 -f Modelfile</pre></li><li><b>Smoke-test it</b><p>You should get a body-only ```iecst block with a VAR CONSTANT holding 4000 and a positional TON call - no PROGRAM wrapper, no T#4s, no IN := / PT :=. If you see any of those, the wrong GGUF or the wrong template is loaded.</p><pre>ollama run stcoder-qwen25-14b:q4 &quot;Surface grinder: when the operator presses start, sound the warning siren for 4 seconds, then energise the spindle contactor. Stop button and a coolant-low fault both drop the spindle.&quot;</pre></li><li><b>Set temperature 0 when you need repeatability</b><p>The published study used greedy decoding with seed 42 at 8192 context. The Modelfile ships temperature 0.2 because it reads better in an interactive session; set it to 0 if you are comparing runs or filing a bug.</p><pre>ollama run stcoder-qwen25-14b:q4 --verbose
&gt;&gt;&gt; /set parameter temperature 0</pre></li><li><b>Paste into LogicStudio and compile - every time</b><p>The output is a POU body: paste it into the block, then run the IDE compiler and fix what it reports. This is not optional. Rule compliance was measured without a compiler, and at least six of this model&#x27;s thirty study replies carry a real defect while scoring a perfect rule score - including one that referenced two undeclared symbols and would not have built.</p><pre># In Mikrodev LogicStudio: paste into the block body, then Compile and read the diagnostics.</pre></li></ol>
</section>
<!-- 4. PROMPTS -->
<section>
<div class="sh"><span class="sn">4</span><h2>What to ask it first</h2></div>
<p class="lead">These are ordinary single-purpose requests, the kind this model handles well.
Type them as they are.</p>
<div class="pgrid"><div class="pc"><div class="q">Surface grinder start sequence: when the operator presses start, sound the warning siren for 4 seconds, then energise the spindle contactor. The stop button and a coolant-low fault both drop the spindle immediately.</div><div class="e">A body-only block with VAR_INPUT / VAR_OUTPUT / VAR CONSTANT, SIREN_MS : INT := 4000, a positional TON(...) call and an RS latch for the spindle, read back as rsSpindle.Q1.</div><div class="n">Fresh prompt, not one of the 15 study prompts - written to exercise the same capability as the study&#x27;s timer_start_delay row, where this fine-tune scored R24 100 in 193 tokens while the base broke three rule classes on the equivalent request.</div></div><div class="pc"><div class="q">Dust extractor run-on: the extraction fan runs whenever the panel saw runs and keeps running for 45 seconds after the saw stops. E-stop cuts it immediately.</div><div class="e">A single TON with PURGE_MS : INT := 45000, a self-holding fan equation and an explicit IF bEStop THEN ... override at the end.</div><div class="n">Fresh prompt. Same shape as the study&#x27;s timer_offdelay_coastdown row (R24 100, 186 tokens). Off-delay via a TON plus a hold term is the Mikrodev idiom, since TOF conventions and named parameters from other IDEs are forbidden.</div></div><div class="pc"><div class="q">Flash the tower lamp while the line is in manual mode: lit for 300 ms, dark for 1200 ms.</div><div class="e">BLINK_ON_MS and BLINK_OFF_MS as INT constants, a TON restarted on its own Q, and a toggled internal bool gating the lamp.</div><div class="n">Fresh prompt, matching the study&#x27;s timer_blink_oscillator row (R24 100, 180 tokens). Check the duty cycle before you deploy: on the study version the model used a single timer for the whole period, which gives an even blink rather than the asymmetric one asked for.</div></div><div class="pc"><div class="q">Scale a pressure transmitter from the analog card: 6000 raw counts is 0 barg, 30000 raw counts is 25 barg. Clamp the result to the range and raise a wire-break flag below 5400 counts. There is an enable bit from the scan logic.</div><div class="e">REAL constants for the raw and engineering endpoints, straight REAL arithmetic with no INT_TO_REAL call, two IF clamps instead of LIMIT, and a wire-break branch that forces the output to zero.</div><div class="n">Fresh prompt with fresh count endpoints. The study&#x27;s analog_scale_raw_to_bar row scored R24 100 and ChrF 38.8. Cast functions and MIN/MAX/LIMIT are forbidden in this dialect and the fine-tune avoids both without being told.</div></div><div class="pc"><div class="q">Unpack a 16-bit soft-starter status word from Modbus into ready, running, tripped and bypass-closed flags. The manual gives the masks in hex: 16#0004 ready, 16#0010 running, 16#0040 tripped, 16#0100 bypass closed.</div><div class="e">The hex masks rewritten as decimal INT constants (4, 16, 64, 256), bitwise AND tests compared against 0, and a shift for any packed warning code.</div><div class="n">Fresh prompt with different masks. The study&#x27;s status_word_bit_mask row was one of this model&#x27;s stronger rows (R24 100, ChrF 43.8): hex literals are forbidden and the fine-tune converts them silently, while the base emitted them verbatim and lost points.</div></div><div class="pc"><div class="q">Clamp a chilled-water setpoint from the HMI to between 6.0 and 14.0 degC, set an out-of-range warning bit, and ramp the working setpoint towards the clamped value in steps of 0.5 per scan. An acknowledge button clears the warning.</div><div class="e">LOW_LIMIT / HIGH_LIMIT / MAX_STEP as REAL constants, an IF/ELSIF clamp chain, an R_TRIG on the acknowledge input and a three-branch ramp.</div><div class="n">Fresh prompt. On the study&#x27;s setpoint_clamp_and_ramp row the fine-tune was rule-clean but scored ChrF 36.1 against its base&#x27;s 37.8 - one of five rows where fidelity to the reference dropped. R_TRIG is the dialect-correct alternative to the edge helpers other IDEs provide.</div></div><div class="pc"><div class="q">Average the last 10 samples of motor line current, work out apparent power as average current times 400 V times 1.732, and set an overload flag if power stays above 15 kW for 5 seconds. A reset pushbutton clears the flag.</div><div class="e">An ARRAY[0..9] OF REAL ring buffer, a FOR loop over a separate loop counter to sum it, multiplication instead of the forbidden ** operator, and a TON on the overload condition.</div><div class="n">Fresh prompt. The study&#x27;s closest row, heater_power_avg_squared, produced this model&#x27;s highest-fidelity output (R24 100, ChrF 50.0) and also its clearest logic bug: it reused the ring-buffer index as the FOR loop counter, destroying the index every scan. Read the loop before you compile.</div></div></div>
<ul><li>Do not paste the rule book into your system prompt. Under a short realistic prompt with no rules listed, this fine-tune was already 100% clean at R24 100; the full rule book changed rule compliance not at all and moved ChrF by 1.9 points. The system prompt in the setup steps is all you need.</li><li>Ask for one block at a time. Every prompt in the study was a single, self-contained plant request, and the model&#x27;s median reply is 262-292 tokens. Multi-part requests are where it starts stubbing things out - the three-pump call site is the documented example.</li><li>Put the real numbers in the request: milliseconds, raw counts, engineering range, ohms, degrees. The model reliably turns them into a named VAR CONSTANT (SIREN_MS : INT := 4000) rather than burying magic numbers in the body, and hex masks from a drive manual come back as decimal constants.</li><li>Read the VAR blocks against the body before you compile. One of the fifteen study replies used two symbols it never declared. It is a five-second check that catches the most common failure.</li><li>Read loops and scan order. The highest-fidelity reply in this model&#x27;s whole set also reused a ring-buffer index as its FOR loop counter. The rule score is blind to this class of bug; you are not.</li><li>Check comment syntax on annunciator-style requests. On the first-out fault prompt the model emitted nested comments, (* (* ... *) *), in both prompt conditions - 2 occurrences under the minimal prompt, 4 under the full one - and Mikrodev will not accept them.</li><li>Anything that lives outside the code body - symbol retention, I/O mapping, instance wiring, POU shells - must be asked about separately, and the model will not always tell you it is missing. The retained-counter request is the clear case: retention is an IDE symbol setting in this dialect, and the reply just counted.</li><li>Expect code, not commentary: prose was 0% in both study conditions. If you want the design rationale, the state numbering or the safety reasoning explained, ask in a second turn - it is a chat model and it answers follow-ups.</li><li>Set temperature 0 when you want a result you can reproduce or file a bug against. The published numbers are greedy decoding, seed 42, 8192 context, tag stcoder_qwen25-14b:q6.</li><li>If a long block truncates mid-listing, raise num_predict. This model’s longest reply at the minimal prompt was 568 tokens, so 2048 is generous, but a multi-part request can run past it.</li><li>Iterate with the compiler in the loop rather than trying to get it right in one shot: paste, compile, feed the diagnostics back as your next message, compile again. That is how the model is meant to be used.</li><li>If you find yourself wanting shorter turnaround or closer-to-reference structure, switch to stcoder-qwen25-7b at Q6_K - it is the line default and it was faster per answer (5.8 s) with higher ChrF at full delivery (38.5).</li><li>Do not port its output to other IEC 61131-3 environments. The constructs it deliberately avoids are the constructs those environments require.</li></ul>
</section>
<!-- 5. EVIDENCE -->
<section>
<div class="sh"><span class="sn">5</span><h2>What fine-tuning changed, measured</h2></div>
<p class="lead">The same 15 requests were sent to the stock base model
(<code>qwen2.5-coder:14b</code>) and to this fine-tune, twice: once with a short realistic
system prompt that lists <b>no dialect rules</b>, and once with the entire rule book pasted in.
Greedy decoding, fixed seed, identical prompts.</p>
<div class="fig">
<div class="fc">Figure 1 &mdash; dialect-clean replies out of 15</div>
<div class="ftl">Clean means: code was delivered and it broke no dialect rule</div>
<div class="legend"><span><i class="sw b"></i>base</span><span><i class="sw f"></i>this fine-tune</span></div>
<div class="frow"><div class="flab">No rules in prompt<small>realistic use</small></div>
<div class="fbars"><div class="bar base"><i style="width:0.7%"></i><b>0.0%</b></div><div class="bar ft"><i style="width:100.0%"></i><b>100.0%</b></div></div></div>
<div class="frow"><div class="flab">Full rule book in prompt<small>charitable to base</small></div>
<div class="fbars"><div class="bar base2"><i style="width:40.0%"></i><b>40.0%</b></div><div class="bar ft2"><i style="width:100.0%"></i><b>100.0%</b></div></div></div>
</div>
<div class="fig">
<div class="fc">Figure 2 &mdash; per-prompt outcome, no rules in prompt</div>
<div class="ftl">One tick per test prompt, same order in both rows</div>
<div class="legend"><span><i class="sw f"></i>clean</span><span><i class="sw b"></i>rule violation</span>
<span><i class="sw r"></i>no code delivered</span></div>
<div class="frow"><div class="flab">Base</div><div class="strip"><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i><i class="vi"></i></div></div>
<div class="frow"><div class="flab">Fine-tune</div><div class="strip"><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i><i class="ok"></i></div></div>
</div>
<div class="tw"><table>
<thead><tr><th>Measure</th><th>Base</th><th>This fine-tune</th><th>Base + full rules</th><th>Fine-tune + full rules</th></tr></thead>
<tbody>
<tr><td>Dialect-clean replies</td><td class="num bs">0.0%</td><td class="num ft">100.0%</td><td class="num">40.0%</td><td class="num">100.0%</td></tr>
<tr><td>Delivered code at all</td><td class="num">100.0%</td><td class="num">100.0%</td><td class="num">100.0%</td><td class="num">100.0%</td></tr>
<tr><td>R24 rule compliance</td><td class="num">92.36</td><td class="num">100.0</td><td class="num">97.16</td><td class="num">100.0</td></tr>
<tr><td>ChrF vs reference code</td><td class="num">32.6</td><td class="num">34.9</td><td class="num">32.5</td><td class="num">36.8</td></tr>
<tr><td>Output tokens (mean &middot; median)</td><td class="num">713 &middot; 674</td><td class="num">288 &middot; 262</td><td class="num">664 &middot; 606</td><td class="num">313 &middot; 292</td></tr>
<tr><td>Time per reply</td><td class="num">11.1s</td><td class="num">8.6s</td><td class="num">10.2s</td><td class="num">8.7s</td></tr>
<tr><td>Throughput</td><td class="num">84.3 t/s</td><td class="num">50.0 t/s</td><td class="num">84.1 t/s</td><td class="num">49.6 t/s</td></tr>
<tr><td>Opened with prose, not code</td><td class="num">80.0%</td><td class="num">0.0%</td><td class="num">100.0%</td><td class="num">0.0%</td></tr>
</tbody>
</table></div>
<p class="note"><b>On throughput:</b> the fine-tune is served here at higher precision than the
stock base model (Ollama's default build is more heavily compressed), so it generates more slowly
per token. Quantisation was not matched in this comparison &mdash; see the limitations.</p>
</section>
<!-- 6. VIOLATIONS -->
<section>
<div class="sh"><span class="sn">6</span><h2>Which dialect rules the base model breaks</h2></div>
<p class="lead">Counted over replies that actually contained code. The upper bar is the realistic
condition; the lower, lighter bar is with the full rule book in the system prompt &mdash; the
difference shows which habits an instruction can fix and which it cannot.</p>
<div class="fig">
<div class="fc">Figure 3 &mdash; base-model rule violations, qwen2.5-coder:14b &mdash;
all 9 rules it broke</div>
<div class="legend" style="margin-bottom:10px"><span style="color:var(--dim)">bar length = share
of delivered replies that broke the rule, full width = 100%</span></div>
<div class="frow"><div class="flab"><code>no_pou_program</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:100.0%"></i><b>15/15</b></div><div class="bar base2"><i style="width:0.7%"></i><b>0/15</b></div></div></div><div class="frow"><div class="flab"><code>no_named_param</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:40.0%"></i><b>6/15</b></div><div class="bar base2"><i style="width:46.7%"></i><b>7/15</b></div></div></div><div class="frow"><div class="flab"><code>no_TIME_literal</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:40.0%"></i><b>6/15</b></div><div class="bar base2"><i style="width:0.7%"></i><b>0/15</b></div></div></div><div class="frow"><div class="flab"><code>no_based_literal</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:6.7%"></i><b>1/15</b></div><div class="bar base2"><i style="width:6.7%"></i><b>1/15</b></div></div></div><div class="frow"><div class="flab"><code>no_output_arrow</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:6.7%"></i><b>1/15</b></div><div class="bar base2"><i style="width:6.7%"></i><b>1/15</b></div></div></div><div class="frow"><div class="flab"><code>no_struct</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:6.7%"></i><b>1/15</b></div><div class="bar base2"><i style="width:6.7%"></i><b>1/15</b></div></div></div><div class="frow"><div class="flab"><code>no_pou_fb</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:6.7%"></i><b>1/15</b></div><div class="bar base2"><i style="width:6.7%"></i><b>1/15</b></div></div></div><div class="frow"><div class="flab"><code>no_oop</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:6.7%"></i><b>1/15</b></div><div class="bar base2"><i style="width:0.7%"></i><b>0/15</b></div></div></div><div class="frow"><div class="flab"><code>no_cast_function</code><small>minimal &middot; full rules</small></div><div class="fbars"><div class="bar base"><i style="width:6.7%"></i><b>1/15</b></div><div class="bar base2"><i style="width:0.7%"></i><b>0/15</b></div></div></div>
</div>
<p class="note">For comparison, this fine-tune's violations across its 15 delivered
replies in the same condition: none.</p>
</section>
<!-- 7. CODE -->
<section>
<div class="sh"><span class="sn">7</span><h2>The same request, side by side</h2></div>
<p class="lead">Highlighted tokens are the ones that decide whether the code is accepted by the
IDE. Both columns are verbatim model output.</p>
<div style="margin-top:26px">
<h3>Same prompt, full rule book: the base learns milliseconds but keeps writing IN :=</h3>
<p class="pmeta" style="margin-bottom:8px">reusable_pump_block &middot; program_structure
&middot; no dialect rules in the system prompt</p>
<p class="note" style="margin-bottom:12px"><b>The request:</b> We have three identical booster pumps on a skid and I am tired of copy-pasting the same logic three times. Give me one reusable pump control block that I can instantiate once per pump. Per instance: run request, motor thermal overload contact, dry-run (low suction) switch, fault reset. Outputs: motor run command, fault flag, and a fault code for the HMI so the operator can see what tripped. Behavi</p>
<div class="diff">
<div class="panel base"><div class="ph"><span class="tag base">Base &middot; qwen2.5-coder:14b</span>
<span class="badge warn">R24 79.41 &middot; 1385 tok</span></div>
<pre class="plain">Certainly! Below is a reusable pump control block implemented in Structured Text (ST) that you can instantiate for each of your three booster pumps. This block will handle the logic for start delay, dry-run detection, overload protection, fault latching, and restart delay.
### Pump Control Block (PumpControlBlock)
```st
<span class="bad">FUNCTION_BLOCK</span> PumpControlBlock
VAR_INPUT
RunRequest : BOOL; // Start request from HMI or PLC
OverloadContact : BOOL; // Motor thermal overload contact
DryRunSwitch : BOOL; // Low suction switch indicating dry-run condition
FaultReset : BOOL; // Reset fault command from HMI or PLC
END_VAR
VAR_OUTPUT
MotorRunCommand : BOOL; // Command to start the motor
FaultFlag : BOOL; // General fault flag
FaultCode : INT; // Specific fault code for HMI display
END_VAR
VAR
StartDelayTimer : TON; // Timer for 2s start delay
DryRunCheckTimer : TON; // Timer for 5s dry-run check after motor starts
RestartDelayTimer : TON; // Timer for 10s restart delay after pump stops
MotorRunning : BOOL := FALSE; // Internal state to track if the motor is running
FaultLatched : BOOL := FALSE; // Internal state to latch faults
END_VAR
<span class="bad">METHOD</span> Run : VOID
VAR
DryRunDetected : BOOL;
END_VAR
// Start delay timer logic
IF NOT MotorRunning AND RunRequest THEN
<span class="bad">StartDelayTimer(IN :=</span> TRUE, PT := <span class="bad">T#2</span>s);
ELSE
<span class="bad">StartDelayTimer(IN :=</span> FALSE);
END_IF;
// Check if the motor can start after the delay
IF StartDelayT</pre>
<div class="pv"><code class="v">no_TIME_literal</code><code class="v">no_named_param</code><code class="v">no_oop</code><code class="v">no_output_arrow</code><code class="v">no_pou_fb</code><code class="v">no_pou_program</code></div></div>
<div class="panel ft"><div class="ph"><span class="tag ft">STCoder fine-tune</span>
<span class="badge ok">R24 100.0 &middot; 568 tok</span></div>
<pre class="plain">```iecst
(* Reusable pump control block *)
VAR_INPUT
bRunReq : BOOL;
bOverload : BOOL;
bDryRun : BOOL;
bReset : BOOL;
END_VAR
VAR_OUTPUT
bMotor : BOOL;
bFault : BOOL;
nFaultCode : INT;
END_VAR
VAR
tStart : TON;
tDryRun : TON;
tRestart : TON;
rsFault : RS;
END_VAR
VAR CONSTANT
START_MS : INT := 2000;
DRY_MS : INT := 5000;
RESTART_MS : INT := 10000;
END_VAR
(* Start delay *)
tStart(bRunReq, START_MS);
(* Dry-run check only after motor has started *)
tDryRun(bMotor, DRY_MS);
(* Restart cooldown *)
tRestart(NOT bMotor, RESTART_MS);
(* Thermal overload trips immediately *)
rsFault(bOverload OR (tDryRun.Q AND bDryRun), bReset);
bFault := rsFault.Q1;
IF bFault THEN
nFaultCode := 1;
IF bOverload THEN nFaultCode := 2; END_IF;
IF bDryRun THEN nFaultCode := 3; END_IF;
END_IF;
(* Motor run command *)
bMotor := tStart.Q AND NOT bFault AND NOT tRestart.Q;
```
```iecst
(* Main program *)
VAR
bP1RunReq, bP2RunReq, bP3RunReq : BOOL;
bP1Overload, bP2Overload, bP3Overload : BOOL;
bP1DryRun, bP2DryRun, bP3DryRun : BOOL;
bP1Reset, bP2Reset, bP3Reset : BOOL;
bP1Motor, bP2Motor, bP3Motor : BOOL;
bP1Fault, bP2Fault, bP3Fault : BOOL;
nP1FaultCode, nP2FaultCode, nP3FaultCode : INT;
END_VAR
(* Instantiate the pump block three times *)
bP1Motor := bP1RunReq;
bP2Motor := bP2RunReq;
bP3Motor := bP3RunReq;
bP1Fault := bP1Overload OR bP1DryRun;
bP2Fault := bP2Overload OR bP2DryRun;
bP3Fault := bP3Overload OR bP3DryRun;
nP1FaultCode := 0;
nP2FaultCode := 0</pre>
<div class="pv"><code class="c">no violations</code></div></div>
</div>
<p class="note">The same request with the entire rule book pasted into the system prompt, which isolates what prompting alone can buy. The base model clearly read the rules and partly complied: T#2s became a plain 2000 and T#5s became 5000, so the TIME literal rule transferred from prompt to output. Two habits did not. It still wrote FUNCTION_BLOCK PumpControl despite being told the IDE owns the shell, and it still called every timer with named parameters - tStartDelay(IN := FALSE, PT := 2000); tDryRunCheck(IN := FALSE, PT := 5000); nRestartDelay(IN := FALSE, PT := 10000); - six such calls in one reply. Across the full condition, named parameters were flagged in 7 of the 15 base replies, more often than under the minimal prompt, making it the single most prompt-resistant habit in this base model&#x27;s weights: reading a rule is not the same as having unlearned a pattern. Score 89.71, and 1364 tokens to get there. The fine-tune&#x27;s answer needed 390 tokens and scored 100, with all three timers positional - tStart(bRunReq, START_MS); tDry(bMotor, DRY_MS); tStop(NOT bRunReq, STOP_MS); - and rsFault(bOverload, bReset); for the latch. Two things to note against it, both invisible to R24: the 10 s restart cooldown is computed into bStopEdge and then never used in the final bMotor equation, so the requirement is quietly unmet, and under the full prompt it dropped the call-site half of the question altogether.</p>
<p class="note" style="color:var(--dim);font-size:12.3px">Red spans in the base panel are the
exact substrings the rule checker matched. The fine-tune panel is shown unmarked because it
broke no rule on this prompt &mdash; read it against the base, line for line.</p>
</div>
<div style="margin-top:26px">
<h3>Drilling station cycle, minimal prompt: a clean state machine that still will not compile</h3>
<p class="pmeta" style="margin-bottom:8px">machine_cycle_full_program &middot; program_structure
&middot; no dialect rules in the system prompt</p>
<p class="note" style="margin-bottom:12px"><b>The request:</b> Can you write the complete program for our drilling station cycle? One part per cycle: when the guard is closed, a part is present and there are no alarms, pressing Start closes the clamp, waits 1.5 s for clamp pressure to build, then the drill motor starts and the head feeds down until the down limit switch, dwells 3 s at depth, retracts until the up limit, opens the clamp after 0.8 s, and the st</p>
<div class="diff">
<div class="panel base"><div class="ph"><span class="tag base">Base &middot; qwen2.5-coder:14b</span>
<span class="badge warn">R24 88.24 &middot; 1049 tok</span></div>
<pre class="plain">Certainly! Below is an example of how you might implement this drilling station cycle using Structured Text (ST) in the Mikrodev LogicStudio IDE, following the IEC 61131-3 standard.
```pascal
<span class="bad">PROGRAM DrillingStationCycle</span>
VAR
// Inputs
GuardClosed : BOOL; // Guard switch input
PartPresent : BOOL; // Part presence sensor input
NoAlarms : BOOL; // Alarm status input
StartButton : BOOL; // Start button input
EStop : BOOL; // Emergency stop input
DrillOverload : BOOL; // Drill overload sensor input
// Outputs
ClampClose : BOOL; // Close clamp output
DrillMotorStart : BOOL; // Start drill motor output
HeadFeedDown : BOOL; // Feed head down output
HeadRetract : BOOL; // Retract head output
ClampOpen : BOOL; // Open clamp output
// Internal variables
PartCounter : INT := 0; // Counter for parts processed
State : INT := 0; // State machine state variable
Timer1 : TON; // Timer for clamp pressure build-up
Timer2 : TON; // Timer for dwell at depth
Timer3 : TON; // Timer for clamp opening delay
// Constants
CLAMP_PRESSURE_BUILD_TIME : TIME := <span class="bad">T#1</span>.5S;
DWELL_AT_DEPTH_TIME : TIME := <span class="bad">T#3</span>S;
CLAMP_OPEN_DELAY_TIME : TIME := <span class="bad">T#0</span>.8S;
END_VAR
// State machine logic
CASE State OF
0: // Idle state
ClampClose := FALSE;
DrillMotorStart := FALSE;
HeadFeedDown := FALSE;
</pre>
<div class="pv"><code class="v">no_TIME_literal</code><code class="v">no_named_param</code><code class="v">no_pou_program</code></div></div>
<div class="panel ft"><div class="ph"><span class="tag ft">STCoder fine-tune</span>
<span class="badge ok">R24 100.0 &middot; 541 tok</span></div>
<pre class="plain">```iecst
(* Drilling station cycle *)
VAR_INPUT
bGuardClosed : BOOL;
bPartPresent : BOOL;
bStart : BOOL;
bReset : BOOL;
bEStop : BOOL;
bDrillOverload : BOOL;
END_VAR
VAR_OUTPUT
bClampClosed : BOOL;
bDrillMotor : BOOL;
bFeedDown : BOOL;
bFeedUp : BOOL;
nPartCount : DINT;
nState : INT;
END_VAR
VAR
tClamp, tDwell, tOpen : TON;
rStartEdge : R_TRIG;
END_VAR
VAR CONSTANT
CLAMP_MS : INT := 1500;
DWELL_MS : INT := 3000;
OPEN_MS : INT := 800;
END_VAR
rStartEdge(bStart);
IF bEStop OR bDrillOverload THEN
bClampClosed := FALSE;
bDrillMotor := FALSE;
bFeedDown := FALSE;
bFeedUp := FALSE;
nState := 0;
ELSIF rStartEdge.Q AND bGuardClosed AND bPartPresent THEN
nState := 1;
END_IF;
CASE nState OF
0: (* Idle *)
bClampClosed := FALSE;
bDrillMotor := FALSE;
bFeedDown := FALSE;
bFeedUp := FALSE;
1: (* Close clamp *)
bClampClosed := TRUE;
tClamp(nState = 1, CLAMP_MS);
IF tClamp.Q THEN nState := 2; END_IF;
2: (* Feed down *)
bFeedDown := TRUE;
bDrillMotor := TRUE;
IF bDownLimit THEN nState := 3; END_IF;
3: (* Dwell at depth *)
tDwell(nState = 3, DWELL_MS);
IF tDwell.Q THEN nState := 4; END_IF;
4: (* Feed up *)
bFeedDown := FALSE;
bFeedUp := TRUE;
IF bUpLimit THEN nState := 5; END_IF;
5: (* Open clamp *)
bFeedUp := FALSE;
tOpen(nState = 5, OPEN_MS);
IF tOpen.Q THEN
nPartCount := nPartCount + 1;
nState :=</pre>
<div class="pv"><code class="c">no violations</code></div></div>
</div>
<p class="note">The longest request in the set - a full drilling cycle with step sequencing, three timers, a part counter and an E-stop fault state. The base model produced a competent portable-ST program and lost 11.76 points on form alone: PROGRAM DrillingStationCycle as the wrapper, a constants block declaring CLAMP_PRESSURE_BUILD_TIME : TIME := T#1.5S; DWELL_AT_DEPTH_TIME : TIME := T#3S; CLAMP_OPEN_DELAY_TIME : TIME := T#0.8S;, and every timer driven as Timer1(IN := TRUE, PT := CLAMP_PRESSURE_BUILD_TIME);. Three rule classes, 1049 tokens. The fine-tune restructured the whole thing to the dialect in 541 tokens: constants as CLAMP_MS : INT := 1500; DWELL_MS : INT := 3000; OPEN_MS : INT := 800;, a rising-edge trigger rStartEdge(bStart); on the start button, an E-stop branch ahead of the sequencer that drops every output, and a CASE nState OF ladder in which each timer is gated by its own step - tClamp(nState = 1, CLAMP_MS);. R24 100, ChrF 48.03, one of its stronger rows. And it would not build. States 2 and 4 branch on IF bDownLimit and IF bUpLimit, and neither symbol is declared in any VAR block. This is the row to point at whenever someone reads 100% clean as 100% correct: the dialect is right, the shape is right, the structure is better than the base&#x27;s, and the compiler will still reject it on two undeclared identifiers. Paste it into LogicStudio, compile, add the two limit-switch inputs, compile again.</p>
<p class="note" style="color:var(--dim);font-size:12.3px">Red spans in the base panel are the
exact substrings the rule checker matched. The fine-tune panel is shown unmarked because it
broke no rule on this prompt &mdash; read it against the base, line for line.</p>
</div>
<div style="margin-top:26px">
<h3>timer blink oscillator</h3>
<p class="pmeta" style="margin-bottom:8px">timer_blink_oscillator &middot; timer_time_based
&middot; no dialect rules in the system prompt</p>
<p class="note" style="margin-bottom:12px"><b>The request:</b> Need a flashing warning beacon done in ST. While the machine warning flag is active the lamp should blink continuously - lit for 500 ms, then dark for 1.5 seconds, over and over. When the warning clears the lamp stays off. There&#x27;s no hardware flasher relay on this panel so it has to be done in software.</p>
<div class="diff">
<div class="panel base"><div class="ph"><span class="tag base">Base &middot; qwen2.5-coder:14b</span>
<span class="badge warn">R24 88.24 &middot; 589 tok</span></div>
<pre class="plain">Certainly! Below is a Structured Text (ST) program that implements a flashing warning beacon based on your requirements. The program assumes you have two variables: `WarningFlag` (a boolean input indicating whether the warning is active) and `LampOutput` (a boolean output controlling the lamp).
```st
<span class="bad">PROGRAM FlashingBeacon</span>
VAR
WarningFlag : BOOL; // Input: Machine warning flag
LampOutput : BOOL; // Output: Control for the lamp
Timer : TON; // Timer to control the blinking sequence
BlinkState : BOOL; // State to alternate between on and off
END_VAR
// Initialize timer if not already done
IF NOT Timer.PT := <span class="bad">T#2</span>s THEN
Timer.PT := <span class="bad">T#2</span>s;
END_IF;
// Main logic for flashing beacon
IF WarningFlag THEN
// Start the timer if it&#x27;s not running
IF NOT Timer.Q THEN
<span class="bad">Timer(ET:=</span><span class="bad">T#0</span>ms);
END_IF;
// Alternate blink state based on timer elapsed time
IF Timer.ET &gt;= <span class="bad">T#1</span>500ms THEN
BlinkState := NOT BlinkState;
<span class="bad">Timer(ET:=</span><span class="bad">T#0</span>ms); // Reset the timer for the next cycle
END_IF;
// Set lamp output based on the current blink state
LampOutput := BlinkState;
ELSE
// Turn off the lamp when warning is cleared
LampOutput := FALSE;
END_IF;
```
### Explanation:
1. **Variables**:
- `WarningFlag`: A boolean input that indicates whether a warning condition exists.
- `LampOutput`: A boolean output that controls the lamp&#x27;s state (on or off).
- `Timer`: A timer object (`TON`) used to manage the timing of the bl</pre>
<div class="pv"><code class="v">no_TIME_literal</code><code class="v">no_named_param</code><code class="v">no_pou_program</code></div></div>
<div class="panel ft"><div class="ph"><span class="tag ft">STCoder fine-tune</span>
<span class="badge ok">R24 100.0 &middot; 180 tok</span></div>
<pre class="plain">```iecst
VAR_INPUT
bWarningFlag : BOOL;
END_VAR
VAR_OUTPUT
bBeaconLamp : BOOL;
END_VAR
VAR
tBlink : TON;
bLit : BOOL;
END_VAR
VAR CONSTANT
BLINK_ON_MS : INT := 500;
BLINK_OFF_MS : INT := 1500;
END_VAR
(* Blink timer toggles every 2 seconds *)
tBlink(bWarningFlag, BLINK_ON_MS + BLINK_OFF_MS);
IF tBlink.Q THEN
tBlink(FALSE, BLINK_ON_MS + BLINK_OFF_MS);
bLit := NOT bLit;
END_IF;
(* Beacon lamp is lit only when warning is active and blink is in the on phase *)
bBeaconLamp := bWarningFlag AND bLit;
```</pre>
<div class="pv"><code class="c">no violations</code></div></div>
</div>
<p class="note"></p>
<p class="note" style="color:var(--dim);font-size:12.3px">Red spans in the base panel are the
exact substrings the rule checker matched. The fine-tune panel is shown unmarked because it
broke no rule on this prompt &mdash; read it against the base, line for line.</p>
</div>
</section>
<!-- 8. FULL RESULTS -->
<section>
<div class="sh"><span class="sn">8</span><h2>Every test prompt and its result</h2></div>
<p class="lead">All 15 prompts, realistic condition (no rules in the system prompt). Prompt text
is in the appendix below.</p>
<div class="tw"><table>
<thead><tr><th>Prompt</th><th>Base</th><th>R24</th><th>ChrF</th><th>tok</th>
<th>Fine-tune</th><th>R24</th><th>ChrF</th><th>tok</th><th style="text-align:left">Rules the base broke</th></tr></thead>
<tbody><tr><td><code>analog_scale_raw_to_bar</code><br><span style="color:var(--dim);font-size:11.5px">analog_math</span></td><td><span class="chip vi">violation</span></td><td class="num bs">97.06</td><td class="num">29.66</td><td class="num">660</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">38.82</td><td class="num">262</td><td style="text-align:left;font-size:11.6px"><code>no_pou_program</code></td></tr><tr><td><code>setpoint_clamp_and_ramp</code><br><span style="color:var(--dim);font-size:11.5px">analog_math</span></td><td><span class="chip vi">violation</span></td><td class="num bs">97.06</td><td class="num">37.79</td><td class="num">699</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">36.11</td><td class="num">287</td><td style="text-align:left;font-size:11.6px"><code>no_pou_program</code></td></tr><tr><td><code>heater_power_avg_squared</code><br><span style="color:var(--dim);font-size:11.5px">analog_math</span></td><td><span class="chip vi">violation</span></td><td class="num bs">97.06</td><td class="num">37.42</td><td class="num">800</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">50.0</td><td class="num">288</td><td style="text-align:left;font-size:11.6px"><code>no_pou_program</code></td></tr><tr><td><code>timer_start_delay</code><br><span style="color:var(--dim);font-size:11.5px">timer_time_based</span></td><td><span class="chip vi">violation</span></td><td class="num bs">88.24</td><td class="num">29.93</td><td class="num">583</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">36.18</td><td class="num">193</td><td style="text-align:left;font-size:11.6px"><code>no_TIME_literal</code>, <code>no_named_param</code>, <code>no_pou_program</code></td></tr><tr><td><code>timer_blink_oscillator</code><br><span style="color:var(--dim);font-size:11.5px">timer_time_based</span></td><td><span class="chip vi">violation</span></td><td class="num bs">88.24</td><td class="num">29.66</td><td class="num">589</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">33.75</td><td class="num">180</td><td style="text-align:left;font-size:11.6px"><code>no_TIME_literal</code>, <code>no_named_param</code>, <code>no_pou_program</code></td></tr><tr><td><code>timer_offdelay_coastdown</code><br><span style="color:var(--dim);font-size:11.5px">timer_time_based</span></td><td><span class="chip vi">violation</span></td><td class="num bs">88.24</td><td class="num">32.43</td><td class="num">760</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">37.17</td><td class="num">186</td><td style="text-align:left;font-size:11.6px"><code>no_TIME_literal</code>, <code>no_named_param</code>, <code>no_pou_program</code></td></tr><tr><td><code>io_terminal_seal_in</code><br><span style="color:var(--dim);font-size:11.5px">io_misc</span></td><td><span class="chip vi">violation</span></td><td class="num bs">88.24</td><td class="num">37.01</td><td class="num">347</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">25.82</td><td class="num">157</td><td style="text-align:left;font-size:11.6px"><code>no_TIME_literal</code>, <code>no_named_param</code>, <code>no_pou_program</code></td></tr><tr><td><code>status_word_bit_mask</code><br><span style="color:var(--dim);font-size:11.5px">io_misc</span></td><td><span class="chip vi">violation</span></td><td class="num bs">91.18</td><td class="num">35.07</td><td class="num">444</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">43.77</td><td class="num">254</td><td style="text-align:left;font-size:11.6px"><code>no_based_literal</code>, <code>no_cast_function</code>, <code>no_pou_program</code></td></tr><tr><td><code>first_out_fault_annunciator</code><br><span style="color:var(--dim);font-size:11.5px">io_misc</span></td><td><span class="chip vi">violation</span></td><td class="num bs">97.06</td><td class="num">32.56</td><td class="num">541</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">26.18</td><td class="num">273</td><td style="text-align:left;font-size:11.6px"><code>no_pou_program</code></td></tr><tr><td><code>machine_cycle_full_program</code><br><span style="color:var(--dim);font-size:11.5px">program_structure</span></td><td><span class="chip vi">violation</span></td><td class="num bs">88.24</td><td class="num">36.36</td><td class="num">1049</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">48.03</td><td class="num">541</td><td style="text-align:left;font-size:11.6px"><code>no_TIME_literal</code>, <code>no_named_param</code>, <code>no_pou_program</code></td></tr><tr><td><code>reusable_pump_block</code><br><span style="color:var(--dim);font-size:11.5px">program_structure</span></td><td><span class="chip vi">violation</span></td><td class="num bs">79.41</td><td class="num">33.14</td><td class="num">1385</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">36.06</td><td class="num">568</td><td style="text-align:left;font-size:11.6px"><code>no_TIME_literal</code>, <code>no_named_param</code>, <code>no_oop</code>, <code>no_output_arrow</code>, <code>no_pou_fb</code>, <code>no_pou_program</code></td></tr><tr><td><code>analog_scale_helper</code><br><span style="color:var(--dim);font-size:11.5px">program_structure</span></td><td><span class="chip vi">violation</span></td><td class="num bs">97.06</td><td class="num">30.97</td><td class="num">770</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">24.46</td><td class="num">298</td><td style="text-align:left;font-size:11.6px"><code>no_pou_program</code></td></tr><tr><td><code>retain_counter_power_cycle</code><br><span style="color:var(--dim);font-size:11.5px">data_memory</span></td><td><span class="chip vi">violation</span></td><td class="num bs">97.06</td><td class="num">23.2</td><td class="num">582</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">25.7</td><td class="num">250</td><td style="text-align:left;font-size:11.6px"><code>no_pou_program</code></td></tr><tr><td><code>shared_project_value</code><br><span style="color:var(--dim);font-size:11.5px">data_memory</span></td><td><span class="chip vi">violation</span></td><td class="num bs">97.06</td><td class="num">31.36</td><td class="num">674</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">32.69</td><td class="num">208</td><td style="text-align:left;font-size:11.6px"><code>no_pou_program</code></td></tr><tr><td><code>machine_data_grouping</code><br><span style="color:var(--dim);font-size:11.5px">data_memory</span></td><td><span class="chip vi">violation</span></td><td class="num bs">94.12</td><td class="num">31.91</td><td class="num">808</td><td><span class="chip ok">clean</span></td><td class="num ft">100.0</td><td class="num">28.76</td><td class="num">380</td><td style="text-align:left;font-size:11.6px"><code>no_pou_program</code>, <code>no_struct</code></td></tr></tbody>
</table></div>
</section>
<!-- 9. QUANT -->
<section>
<div class="sh"><span class="sn">9</span><h2>Q4, Q6 and Q8 &mdash; what actually differs</h2></div>
<p>All file sizes in this card are GiB, as reported by the build tooling. Q4_K_M, Q6_K and Q8_0 are increasingly faithful compressions of the same trained weights: more bits per weight means a larger file and a closer match to the original, at no change in behaviour or licence. For this model only Q4_K_M (8.37 GiB) and Q6_K (11.29 GiB) are published; a Q8_0 would land near 14.6 GiB and was left out deliberately.
The reason it was left out is the earlier 2000-prompt internal evaluation, where this fine-tune scored a code composite of 0.851-0.852 and ChrF 52.0 across Q4, Q6 and Q8 - a spread of 0.001 or less. One disclosure is required before you read those figures: that evaluation&#x27;s code holdout overlapped the training distribution by about 84%, so its absolute code scores are inflated and only weakly discriminative between models. They are used here for one purpose only - comparing quantisations of this single model - and never to rank the four models against each other. For that within-model purpose the conclusion holds: on the code task, quantisation is effectively free here, and Q4_K_M produces the same quality of Structured Text as Q8 would.
That is worth stating plainly because an earlier report showed a large Q4 penalty. That penalty was measured on the tool-calling task, where argument-level precision collapses first under quantisation - the Qwen3.5-9B family fell from 0.816 at Q8_0 to 0.642 at Q4_K_M. Tool calling was dropped as a product decision and these builds are published and supported as chat-only models, so that Q4 penalty does not apply to anything these models are asked to do.
Practical guidance: run Q4_K_M unless you have spare VRAM. Q6_K is the quant the head-to-head actually used (tag stcoder_qwen25-14b:q6), so choose it if you want to reproduce the published numbers exactly.</p>
<div class="card"><h3>Evidence base &mdash; and its limit</h3>
<p>From an earlier 2,000-prompt internal evaluation of these same fine-tunes:
<b>code composite 0.851-0.852 and ChrF 52.0 across Q4/Q6/Q8 (spread under 0.001)</b>. The head-to-head study on this page used the
<b>Q6_K</b> build. File sizes are <b>GiB</b>.</p>
<p class="note" style="margin-top:10px"><b>Important caveat.</b> That earlier evaluation's code
holdout overlapped the training distribution by roughly 84%, so its absolute code scores are
inflated and only weakly separate one model from another. Use those figures for the
<i>within-model</i> quantisation comparison, which is what they are cited for here &mdash; not
to rank the four models against each other. For cross-model ranking use the identical-conditions
table in section&nbsp;2.</p></div>
</section>
<!-- 10. STRENGTHS -->
<section>
<div class="sh"><span class="sn">10</span><h2>Strengths, weaknesses, and what not to use it for</h2></div>
<h3>Strengths</h3><ul><li>Highest clean rate in the head-to-head: R24 100.0 and 100% clean across all 30 fine-tune replies, in both the minimal and the full-prompt condition. It is the only one of the four fine-tunes that reached 100% clean in both (stcoder-qwen25-7b 93.3/100, stcoder-qwen35-9b 93.3/100, stcoder-gemma4-12b 86.7/80).</li><li>The dialect lives in the weights, not the prompt. Under the minimal prompt - a short realistic system message that lists no rules at all - the fine-tune was 100% clean while the same base weights were 0.0% clean. Pasting the entire rule book into the base model&#x27;s prompt only lifted it to 40%.</li><li>No prose leakage in either condition (0%, against 80% for the base at the minimal prompt and 100% at the full prompt). Replies are a code fence, not an essay wrapped around a code fence, so the output can be pasted straight into a block.</li><li>Fewest output tokens in the head-to-head at the minimal prompt: 288 mean and 262 median, against 340 for stcoder-qwen25-7b, 401 for stcoder-gemma4-12b and 1227 for stcoder-qwen35-9b - and 2.5x more economical than its own base on the same 15 requests (713 mean / 674 median). Mean wall clock was 8.6 s against 11.1 s for the base.</li><li>100% delivery in both conditions and both arms - no empty or non-delivered replies. Of the four fine-tunes only stcoder-qwen35-9b missed one (14 of 15 at the minimal prompt), which is why its higher ChrF carries a smaller denominator.</li><li>The only fine-tune in the set whose ChrF improved when the rule book was added to the prompt (34.9 to 36.8); the other three all dropped. Extra context does not destabilise this model.</li><li>Reaches for the right Mikrodev idioms unprompted: positional TON(bRunReq, START_MS), RS latches read via .Q1, R_TRIG for pushbutton edges, INT millisecond values in VAR CONSTANT, bitwise masks as decimal constants, IF clamps instead of LIMIT, and no POU wrapper.</li><li>Quantisation-insensitive on the code task within this model: composite 0.851-0.852 across Q4, Q6 and Q8 in the earlier 2000-prompt evaluation, a spread of 0.001 or less, so Q4_K_M costs nothing measurable in exchange for 2.9 GiB less VRAM. That evaluation&#x27;s code holdout overlapped the training distribution by about 84%, so its absolute scores are inflated and weakly discriminative; it is used here only to compare quantisations of this one model, never to rank models against each other.</li></ul>
<h3>Weaknesses</h3><ul><li>Lowest ChrF of the four at the minimal prompt: 34.9 over 15 delivered replies, against stcoder-qwen25-7b 38.5 (15/15), stcoder-qwen35-9b 40.9 (14/15) and stcoder-gemma4-12b 35.9 (15/15). Rule-clean output is not the same as output that resembles hand-written reference code, and on this metric this model is last in the set.</li><li>Fine-tuning made reference similarity worse on 5 of the 15 minimal prompts, where the fine-tune&#x27;s ChrF fell below its own base: analog_scale_helper 24.46 against 30.97, io_terminal_seal_in 25.82 against 37.01, first_out_fault_annunciator 26.18 against 32.56, machine_data_grouping 28.76 against 31.91, setpoint_clamp_and_ramp 36.11 against 37.79. The training buys dialect compliance and brevity, not uniformly closer structure.</li><li>Clean does not mean correct. R24 is reference-free and has no compiler behind it. In the heater_power_avg_squared row the model scored R24 100 while reusing the ring-buffer index nIdx as the FOR loop counter (FOR nIdx := 0 TO 7), which destroys the index every scan. The metric cannot see that.</li><li>Undeclared symbols occur. The machine_cycle_full_program reply is dialect-clean at R24 100 but branches on bDownLimit and bUpLimit, neither of which appears in any VAR block. That code does not build. Compile before you trust it.</li><li>It can hand back a stub instead of admitting a limit. Asked to show how a reusable pump block is called for three pumps, it produced a second block that instantiates nothing - bP1Motor := bP1RunReq; and similar - with all three fault codes set to 0. Under the full prompt condition it dropped the call-site block entirely.</li><li>It can answer the dialect while missing the engineering question. The retain_counter_power_cycle prompt was explicitly about totals surviving a nightly power-down; retention is forbidden in code in this dialect and must be set on the symbol in the IDE, but the reply just counts and never mentions it. ChrF 25.7 on that row reflects the gap.</li><li>Unused logic slips through. In the full-condition pump-block reply the 10 s restart cooldown timer tStop is computed and assigned to bStopEdge, then never used in the motor equation. The cooldown requirement is silently unmet.</li><li>First-out latching was not actually first-out: the fault annunciator reply re-evaluates its IF/ELSIF chain every scan, so the captured code follows the highest-priority active fault rather than the one that tripped first, and a later bAlarmLamp := bAnyFault; overwrites the acknowledge handling above it.</li><li>Nested comments, (* (* ... *) *), which Mikrodev does not accept. They appear on the same prompt in both conditions - first_out_fault_annunciator, 2 occurrences under the minimal prompt and 4 under the full prompt - so this is a reproducible failure on that request rather than a one-off. It will stop a compile and the rule score does not catch it.</li><li>At least six of the thirty fine-tune replies contain a real defect while scoring R24 100: heater_power_avg_squared (FOR-index reuse), machine_cycle_full_program (two undeclared symbols), reusable_pump_block minimal (stub call site, all fault codes 0), reusable_pump_block full (cooldown computed then unused, call site dropped), first_out_fault_annunciator in both conditions (nested comments plus the priority and overwrite bugs) and retain_counter_power_cycle (the retention requirement never addressed).</li><li>Slowest of the four per token: 50.0 tok/s at Q6_K on an RTX 5080 (49.6 at the full prompt), against 103.6 for stcoder-qwen25-7b, 86.3 for stcoder-qwen35-9b and 72.7 for stcoder-gemma4-12b. Per answer it is not the slowest - 8.6 s against 5.8 s for the 7B, 8.5 s for the Gemma and 17.2 s for the 9B - because it also emits the fewest tokens. Name the axis when comparing: slowest per token, mid-pack per answer.</li><li>The tok/s comparison against its own base (50.0/49.6 fine-tune against 84.3/84.1 base) is not precision-controlled. The fine-tune ran at Q6_K and the stock base at the Ollama library default, and the run data records no quantisation for the base arms at all, so &quot;effectively Q4&quot; there is an inference from that default rather than a recorded value. Part of the throughput difference is precision, not training.</li><li>Terse by design. Prose was 0% in both conditions, so you get code and almost no reasoning. If you want the design explained, ask for it in a follow-up turn.</li><li>No Q8_0 is published, and Q6_K at 11.29 GiB is a tight fit on a 16 GB card. An 8 GB card cannot hold either published quant in VRAM.</li><li>Study caveat: quantisation was not controlled in the head-to-head, so some of the base-versus-fine-tune gap may be precision rather than training - though a precision difference does not plausibly explain 0.0% clean against 100%. Also note the metric variant: the R24 and clean figures in this card are computed over the whole reply. The same data holds a fence-only variant, and because base replies were prose-wrapped 66.7-100% of the time while fine-tune replies were 0-20%, the variant matters for the base arms - pooled base clean at the minimal prompt moves from 6.7% whole-reply to 10.0% fence-only. For this model specifically both variants give the same numbers (base 0.0% clean, fine-tune 100%), so the gap reported here is not an artefact of the choice.</li></ul>
<h3>Not the right tool for</h3><ul><li>Tool calling or function calling of any kind. Tool calling was trained and evaluated for this line; only the Qwen3.5-9B family learned it (0.816 at Q8_0, 0.805 at Q6_K, 0.642 at Q4_K_M) while the code-focused families scored 0.02-0.06 and the Gemma family 0.00. Because it was not usable across the line it was dropped as a product decision, and these builds are published and supported as chat-only models. If you are building an agent that drives IDE tools, do not use them.</li><li>Agent frameworks that expect structured JSON actions, MCP calls or an OpenAI tools schema. This is a chat model that returns Structured Text in a code fence.</li><li>Machines with 8 GB of VRAM or less - the smallest published file is 8.37 GiB of weights. Use stcoder-qwen25-7b there (Q4_K_M, 4.36 GiB).</li><li>Readers who want the line default. That is stcoder-qwen25-7b at Q6_K: faster per answer, smaller, and higher ChrF at full delivery. Come to the 14B for the clean rate, not as a general upgrade.</li><li>Any dialect other than Mikrodev LogicStudio. It was trained to violate conventions that other IEC 61131-3 environments require, so its output is deliberately wrong for them: no POU wrappers, no TIME literals, no named parameters, no output arrows.</li><li>Unreviewed deployment to a live machine. At least six of the thirty replies in this model&#x27;s own study data would have failed to build or would have misbehaved despite scoring a perfect rule score. Compile it, review the logic, and test it.</li><li>Safety-instrumented functions. Nothing here substitutes for a rated safety controller and a proper functional-safety assessment.</li><li>General-purpose programming, or PLC languages other than Structured Text. The system prompt declines non-PLC requests and the training gives it no advantage outside this narrow task.</li><li>Frontier-scale work: long multi-file project generation, deep architectural reasoning, or specification review. This is a 14B model running locally.</li></ul>
</section>
<!-- 11. METHOD -->
<section>
<div class="sh"><span class="sn">11</span><h2>How the numbers were produced</h2></div>
<p>240 controlled generations across four model families, 15 prompts each, run twice per model
under two prompting conditions. Greedy decoding (<code>temperature 0</code>, <code>seed 42</code>),
8,192-token budget, local Ollama on an RTX 5080 (16 GB).</p>
<ul>
<li><b>R24</b> &mdash; dialect rule compliance. 33 forbidden patterns, individually weighted; the
score is the share of total weight left intact (0&ndash;100). Comments, prose and string contents
are removed before matching, so discussion never triggers a rule.</li>
<li><b>ChrF</b> &mdash; character n-gram F-score of the generated code against a hand-written
reference solution (sacreBLEU chrF++). Higher means closer to house style. An alternative but
correct solution legitimately scores lower, so it is always read together with R24.</li>
<li><b>Clean</b> &mdash; the reply delivered code <i>and</i> broke no rule. A reply containing no
code scores R24 = 100 trivially, because there is nothing left to violate, so delivery is
checked first.</li>
<li><b>Which R24 variant.</b> The R24 and clean figures on this page are computed over the
<b>whole reply</b>. That matters because base replies were prose-wrapped 80.0% of the
time for this model: scoring only the fenced code lifts the pooled base clean rate across the
study from 6.7% to 10.0%. The base-versus-fine-tune gap is not sensitive to the choice, but the
variant is named here so the numbers can be reproduced exactly.</li>
<li><b>Test prompts</b> &mdash; written as ordinary plant requests. No prompt mentions any dialect
rule; a prompt that said &ldquo;use milliseconds&rdquo; would hand the answer to the base model.
Reference solutions were verified rule-clean before use.</li>
</ul>
<div class="card warn"><h3>Limitations</h3>
<ul>
<li><b>Quantisation was not matched.</b> The fine-tune ran at higher precision than the stock
base build. This plausibly helps the fine-tune on quality and demonstrably penalises it on
raw throughput.</li>
<li><b>&ldquo;Clean&rdquo; is not &ldquo;correct&rdquo;.</b> R24 is reference-free: it certifies
that no forbidden construct appears, not that the logic works. Correctness was checked by
reading the code, not by compiling it.</li>
<li><b>The published builds do not ship this decoding configuration.</b> These numbers were
measured greedily (<code>temperature 0</code>,
<code>seed 42</code>) at
<code>num_ctx 16384</code> /
<code>num_predict 8192</code>. The Modelfiles published with
the models default to <code>temperature 0.2</code> and <code>num_ctx 8192</code> &mdash; the
sequence length the models were trained at &mdash; because that is the better interactive
default. Match the three measured values to reproduce these figures; otherwise expect
run-to-run variation.</li>
<li><b>Scope.</b> 15 prompts, one dialect, English only, no tool-calling, and the prompt set
was deliberately built on the categories where this dialect diverges from standard IEC. It
measures dialect fidelity on dialect-sensitive work, not general PLC competence.</li>
<li><b>Small sample.</b> 15 prompts per condition per model. Treat single-prompt differences
as anecdote and the aggregate as the signal.</li>
</ul>
</div>
</section>
<!-- 12. CLOSING -->
<section>
<div class="sh"><span class="sn">12</span><h2>In summary</h2></div>
<p>What this model buys you is a local assistant that has stopped writing generic textbook Structured Text and writes Mikrodev Structured Text instead: across 30 fine-tune replies in the controlled study it broke no dialect rule and leaked no prose, from the same weights whose stock version was 0.0% clean when the rules were not spelled out. Within the four-model line that clean rate is its one measured distinction - it is the only fine-tune at 100% clean in both prompt conditions - and it is bought at a real price: the lowest ChrF of the four at the minimal prompt (34.9 against 38.5, 40.9 at 14/15 delivery, and 35.9), the slowest generation per token at roughly 50 tok/s, and 8.4-11.3 GiB of VRAM. If you have not decided, the line default is stcoder-qwen25-7b at Q6_K. This is a small chat model, tool calling was dropped from the product and it is supported as chat-only, and its clean score says nothing about whether the logic is right - at least six of its thirty study replies scored a perfect 100 and still carried a real defect - so treat every reply as a first draft that goes to the IDE compiler before it goes anywhere near a machine.</p>
</section>
<!-- APPENDIX -->
<section>
<div class="sh"><span class="sn">A</span><h2>Appendix &mdash; the test prompts</h2></div>
<div class="tw"><table>
<thead><tr><th>ID</th><th style="text-align:left">Prompt as sent to the model</th></tr></thead>
<tbody><tr><td><code>analog_scale_raw_to_bar</code></td><td style="text-align:left">We have a 4-20 mA pressure transmitter landing on an analog input card. The raw word comes in as an integer where 5530 counts = 4 mA and 27648 counts = 20 mA, and the transmitter is ranged 0-16 bar. Can you write the Structured Text that turns the raw count into bar, keeps the result inside 0 to 16, and sets a wire-break flag when the loop current drops under about 3.6 mA (roughly 4977 counts)? There is also an enable bit coming from the scan logic - when that is off the scaled output should read zero.</td></tr><tr><td><code>setpoint_clamp_and_ramp</code></td><td style="text-align:left">Operators type a hot water setpoint on the HMI. Anything below 40.0 degC or above 85.0 degC has to be pulled back to the nearest limit and an out-of-range warning bit raised, which the operator can acknowledge once the entry is sensible again. On top of that, the setpoint that actually goes to the controller must not jump more than 2.0 degC per PLC cycle, so ramp it toward the clamped target. Structured Text please.</td></tr><tr><td><code>heater_power_avg_squared</code></td><td style="text-align:left">Heater panel job. I sample the load current every scan and it is noisy, so I need the average of the last 8 samples first. From that averaged current I want the dissipated power using P = I squared times R, with R = 4.7 ohm. If the power stays above 1500 W for 3 seconds, latch an overload bit that only clears when the operator hits the reset pushbutton. Please write it in Structured Text.</td></tr><tr><td><code>timer_start_delay</code></td><td style="text-align:left">We&#x27;ve got a conveyor with a pre-start warning horn. When the operator hits the start button I need the horn to sound for 5 seconds first, then the motor kicks in and the horn goes quiet. Pressing stop, or any fault coming in, has to drop the motor and the horn straight away. Can you write the Structured Text for this?</td></tr><tr><td><code>timer_blink_oscillator</code></td><td style="text-align:left">Need a flashing warning beacon done in ST. While the machine warning flag is active the lamp should blink continuously - lit for 500 ms, then dark for 1.5 seconds, over and over. When the warning clears the lamp stays off. There&#x27;s no hardware flasher relay on this panel so it has to be done in software.</td></tr><tr><td><code>timer_offdelay_coastdown</code></td><td style="text-align:left">Our extraction fan needs a run-on timer. The fan should run whenever the main motor is running, and then keep running for 30 seconds after the motor is switched off so the duct clears. Emergency stop has to cut the motor and the fan immediately, with no run-on. Also give me a flag I can put on the HMI to show that the purge is in progress. Structured Text please.</td></tr><tr><td><code>io_terminal_seal_in</code></td><td style="text-align:left">On a small booster pump skid the start pushbutton is wired to the first digital input on the CPU and the pump contactor sits on the first relay output. There is an NC stop button on the second input and the motor thermal overload aux contact on the third. Write me the ST for a normal start/stop seal-in: the start button is bouncy so it has to be held for half a second before the pump picks up, an overload trip must latch the pump out, and the lockout should only clear when the operator presses stop with the overload already reset.</td></tr><tr><td><code>status_word_bit_mask</code></td><td style="text-align:left">We poll a 16-bit status word from a VFD over Modbus TCP. The drive manual gives the bit masks in hex: 16#0001 = ready, 16#0002 = running, 16#0008 = fault, 16#0080 = at speed, and 16#0F00 holds a 4-bit warning code. Break the word out into individual booleans, pull the warning code out as a plain integer 0-15, and give me a single &#x27;drive healthy&#x27; flag that is only true when it is ready and running with no fault and no warning. If the Modbus poll is not valid, do not trust any of it.</td></tr><tr><td><code>first_out_fault_annunciator</code></td><td style="text-align:left">I need a first-out fault annunciator for a compressor package. The trip inputs are low oil pressure, high discharge temperature, motor overload and high vibration. Whichever one trips first has to be captured and reported to the HMI so the operator can tell at a glance which fault actually caused the trip - later trips must not overwrite it. I also want a common alarm lamp, a horn that goes quiet when acknowledge is pressed while the lamp stays on, and a reset that is only accepted when all four inputs are clear again. The compressor run permissive drops out on any fault.</td></tr><tr><td><code>machine_cycle_full_program</code></td><td style="text-align:left">Can you write the complete program for our drilling station cycle? One part per cycle: when the guard is closed, a part is present and there are no alarms, pressing Start closes the clamp, waits 1.5 s for clamp pressure to build, then the drill motor starts and the head feeds down until the down limit switch, dwells 3 s at depth, retracts until the up limit, opens the clamp after 0.8 s, and the station goes back to idle ready for the next part while incrementing the part counter. E-stop or drill overload must drop every output and put the station into a fault state that only the reset button clears. I want the step sequencing and the timers in there.</td></tr><tr><td><code>reusable_pump_block</code></td><td style="text-align:left">We have three identical booster pumps on a skid and I am tired of copy-pasting the same logic three times. Give me one reusable pump control block that I can instantiate once per pump. Per instance: run request, motor thermal overload contact, dry-run (low suction) switch, fault reset. Outputs: motor run command, fault flag, and a fault code for the HMI so the operator can see what tripped. Behaviour: 2 s start delay after the run request, the dry-run switch is only evaluated 5 s after the motor has actually started, overload trips immediately, any trip latches until it is reset, and once the pump stops it has to stay off for at least 10 s before it may restart. Also show me how it gets called for the three pumps.</td></tr><tr><td><code>analog_scale_helper</code></td><td style="text-align:left">I need a small reusable routine that turns the raw analog word from our pressure transmitters into engineering units, because I have to do the same conversion for three transmitters on the line. They are 4-20 mA into a 0-32767 count input card, so 6553 counts is 4 mA and 32767 counts is 20 mA, and the measuring range is 0-250 bar. It should clamp the result to the range and set an out-of-range flag, plus a broken-wire flag when the raw count drops below 3200 (under 2 mA). A rounded integer in tenths of a bar for the HMI would be handy too.</td></tr><tr><td><code>retain_counter_power_cycle</code></td><td style="text-align:left">Line 3 bottler: a photo eye (bPartDetect) pulses once for every bottle that goes past. I need two totals out of it - a lifetime total that goes into the maintenance records, and a shift total that the supervisor clears with bShiftReset at the start of each shift. The problem is we power the cabinet down every night and in the morning both totals come back as zero, so quality has no numbers to report. Write the ST for the counting logic so the lifetime total is still correct after the panel comes back up.</td></tr><tr><td><code>shared_project_value</code></td><td style="text-align:left">Tank farm project. The level control block works out the tank level in percent and a plant-running flag. The alarm block, the Modbus reporting block and the HMI block all need to see exactly the same two values on every scan - right now each of them recalculates them on its own and they end up disagreeing with each other. Write the level control block so those two values are produced in one place and the rest of the project can use them. Inputs are rLevelRaw from the transmitter (already scaled 0-100), bPumpRunning, bValveOpen and bEStop; the published percent should stay inside 0-100 and a transmitter reading outside range has to be flagged.</td></tr><tr><td><code>machine_data_grouping</code></td><td style="text-align:left">I have three identical packaging machines in one project. For each machine I track a temperature setpoint, the actual temperature, the belt speed, a cycle counter and a state code. I would like each machine&#x27;s values kept together so I can run the same monitoring logic over all three in a loop instead of copy-pasting the same block three times. Logic per machine: if it is in the running state and the actual temperature is more than 5 degrees off setpoint, or the belt speed drops below 10, fault it; count a cycle on each rising edge of that machine&#x27;s cycle-done input; EStop faults all of them. Also give me how many machines are currently faulted. Write it in ST.</td></tr></tbody>
</table></div>
</section>
<footer>
<b>Licence.</b> apache-2.0. Fine-tuned from <code>Qwen/Qwen2.5-Coder-14B-Instruct</code>; the base
model's attribution and NOTICE are preserved. You may use, modify and redistribute under the
same terms.<br><br>
<b>Training data.</b> A curated in-house multi-task PLC corpus (approximately 2,930 training and
550 validation examples) focused on Structured Text generation. The dataset is not publicly
released.<br><br>
<b>A note on tags.</b> The reproduction tags below use an underscore
(<code>stcoder_qwen25-14b:q6</code>) because that is how the study's local Ollama models were named.
The tag you create by following section&nbsp;3 uses hyphens
(<code>stcoder-qwen25-14b</code>) &mdash; the name is arbitrary and does not affect the weights.<br><br>
<b>Exact builds compared.</b> Fine-tune <code>stcoder_qwen25-14b:q6</code> versus stock base
<code>qwen2.5-coder:14b</code>, both served by local Ollama. The base arm's quantisation was
<b>not recorded</b> in the run data; where this page says the base ran "effectively Q4" that is an
inference from the Ollama library default for that tag, not a measured value.<br><br>
<b>Reproducibility.</b> Harness <code>scripts/compare/paper_eval.py</code>; aggregation
<code>paper_analyze.py</code>; violation localisation <code>paper_contrasts.py</code>; prompt set
<code>dataset/paper_eval_suite_v1.json</code>; raw rows <code>results/paper/results_v3.jsonl</code>.
Every figure on this page is generated from those rows by
<code>scripts/compare/build_model_articles.py</code>.<br><br>
<b>Where the tool-calling numbers come from.</b> The tool-call composites quoted on this page
were measured in a separate internal evaluation that is not part of this release, not in the
240-generation code study, so they cannot be traced to the rows above. The same is true of the
per-quantisation figures in section&nbsp;9, whose code holdout overlapped the training
distribution by roughly 84%.
</footer>
</div>