stcoder-qwen35-9b is a chat-only fine-tune of Qwen3.5-9B that writes Mikrodev LogicStudio Structured Text; it is the only member of the line built on a reasoning base, so it thinks before it answers — which buys clean, reference-shaped code but costs roughly four times the tokens of its siblings and cost it one non-delivery in fifteen.
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's ST dialect, not generic IEC 61131-3 and not another vendor's conventions.
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.
Download Q8_0 (qwen35_9b-tc.q8_0.gguf, 9.11 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
11.1 GiB — the file plus the 8192-token context and runtime.
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.
Also published for this model. The LoRA adapter it was trained as
(lora_adapter/ 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 Modelfiles ship next to every build:
correct chat template, stop tokens, num_ctx 8192 — the sequence length this
model was fine-tuned at — and the system prompt.
| File | Quantisation | Size | Fit on 16 GB | Note |
|---|---|---|---|---|
qwen35_9b-tc.q4_k_m.gguf | Q4_K_M | 5.38 GiB | solid | |
qwen35_9b-tc.q6_k.gguf | Q6_K | 7.04 GiB | solid | |
qwen35_9b-tc.q8_0.gguf | Q8_0 | 9.11 GiB | solid | tested in this study |
Read this first: tool calling was trained and evaluated for this line and then dropped as a product decision. This build is published and supported as a chat-only model. This family is in fact the one that did learn the skill (tool-call composite 0.816 at Q8_0, 0.805 at Q6_K, collapsing to 0.642 at Q4_K_M) while stcoder-qwen25-7b and stcoder-qwen25-14b scored 0.02-0.06 and stcoder-gemma4-12b scored 0.00. Because it was not usable across the line it is not part of this release and is not supported here. If you have not already decided, take stcoder-qwen25-7b at Q6_K (5.82 GiB). That is the line default: 5.8 s per answer, the smallest files, 100% delivery, and the best ChrF at full delivery in this study (38.5 over 15 of 15), with composite within noise of everything else. Pick stcoder-qwen35-9b instead only if you specifically want the reasoning family and can pay for it in latency. On the minimal-prompt condition it scored R24 100.0 with an empty violation table on every delivered reply, 93.3% clean, and composite 0.845 over its 14 delivered replies (0.835 if the missing reply is counted as zero, which puts it behind the 7B's 0.844 and the 14B's 0.842). Its ChrF is 40.9 over those 14 delivered replies — the highest delivered-only figure in the set — but 38.2 over all 15, below the 7B's 38.5; the lead exists only after excluding its own empty reply, so quote the denominator whenever you quote the number. Every reply carries a hidden thinking block (100% of replies in all four arms), averaging 1227 output tokens and 17.2 s per answer at the minimal prompt against 340 tokens and 5.8 s for the 7B. It is the slowest of the four per answer; the 14B is the slowest per token (about 50 tok/s). Accept also that it was the only fine-tune in the study that ever returned no code at all (14 of 15 at the minimal prompt, 15 of 15 with the full rule book).
| Model | Clean | Delivered | ChrF | Composite | Out tok | Per reply | Recommended build |
|---|---|---|---|---|---|---|---|
stcoder-qwen25-7b7B · default pick, fastest per answer | 93.3% | 15/15 | 38.5 | 0.844 | 340 | 5.8s | Q8_0 · 7.54 GiB |
stcoder-qwen25-14b14B · only one 100% clean in both conditions | 100.0% | 15/15 | 34.9 | 0.842 | 288 | 8.6s | Q8_0 · 14.62 GiB |
stcoder-qwen35-9b (this model)9B · reasoning family, highest ChrF when it answers | 93.3% | 14/15 | 40.9 | 0.845 | 1227 | 17.2s | Q8_0 · 9.11 GiB |
stcoder-gemma4-12b12B · non-Qwen lineage, weakest fine-tune case | 86.7% | 15/15 | 35.9 | 0.83 | 401 | 8.5s | Q8_0 · 11.80 GiB |
How to read this. 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 GiB throughout. If you are undecided, take stcoder-qwen25-7b at Q8_0.
Any recent release. Verify it responds before continuing.
ollama --version
Q6_K is the recommendation for 12 GB and up; Q4_K_M for 8 GB; Q8_0 is the quant the published study numbers were measured on. You need one file only. The "-tc" in the filename is a build-pipeline tag, not a tool-calling build - these are chat-only.
hf download Mikrodev/stcoder-qwen35-9b-gguf qwen35_9b-tc.q6_k.gguf --local-dir .
Context window matters more for this model than for its siblings because it reasons before it answers. 8192 is the budget used in the study; 16384 removes the empty-reply risk. Greedy decoding (temperature 0) is what was measured.
cat > Modelfile <<'EOF' FROM ./qwen35_9b-tc.q6_k.gguf PARAMETER num_ctx 8192 # the published numbers were measured with num_ctx 16384 / num_predict 8192 # this family reasons before answering and the thinking is drawn from the same budget, so do not set num_predict lower PARAMETER num_predict 8192 PARAMETER temperature 0 PARAMETER top_p 1 SYSTEM """You are a PLC programming assistant for the Mikrodev LogicStudio IDE. You write IEC 61131-3 Structured Text in the Mikrodev dialect: no POU wrappers, no TIME literals (use INT milliseconds), positional function-block calls only, no OOP, no pointers, no global or retained variables, no STRING types, no hex literals, no cast or selection functions. Declare variables in VAR / VAR_INPUT / VAR_OUTPUT / VAR CONSTANT blocks and use (* *) or // comments.""" EOF
Takes a few seconds; the GGUF is copied into the Ollama store.
ollama create stcoder-qwen35-9b:q6 -f Modelfile
The first reply includes a hidden reasoning pass, so expect a pause of several seconds before code appears. That is normal for this model and is why it averaged 17.2 s per answer in the study.
ollama run stcoder-qwen35-9b:q6 "Chiller circulation pump has to keep running 45 seconds after the compressor stops. E-stop drops both at once. Give me the ST."
The 240-generation head-to-head used these exact tags. The base arms ran at the Ollama library default, whose quantisation was not recorded in the run data.
# fine-tunes: stcoder_qwen35-9b:q8 stcoder_qwen25-7b:q8 stcoder_qwen25-14b:q6 stcoder_gemma4-12b:q6 # bases: qwen3.5:9b qwen2.5-coder:7b qwen2.5-coder:14b gemma4:12b
Always compile before deployment. The scoring behind this release checks dialect compliance, not whether the logic is correct - and this model's replies also need a check for doubled comment delimiters, which the scorer does not catch.
These are ordinary single-purpose requests, the kind this model handles well. Type them as they are.
The same 15 requests were sent to the stock base model
(qwen3.5:9b) and to this fine-tune, twice: once with a short realistic
system prompt that lists no dialect rules, and once with the entire rule book pasted in.
Greedy decoding, fixed seed, identical prompts.
| Measure | Base | This fine-tune | Base + full rules | Fine-tune + full rules |
|---|---|---|---|---|
| Dialect-clean replies | 6.7% | 93.3% | 26.7% | 100.0% |
| Delivered code at all | 80.0% | 93.3% | 80.0% | 100.0% |
| R24 rule compliance | 93.02 | 100.0 | 96.57 | 100.0 |
| ChrF vs reference code | 28.9 | 40.9 | 34.1 | 37.6 |
| Output tokens (mean · median) | 6345 · 6952 | 1227 · 793 | 2902 · 1521 | 554 · 488 |
| Time per reply | 54.5s | 17.2s | 26.7s | 9.0s |
| Throughput | 123.2 t/s | 86.3 t/s | 120.5 t/s | 84.0 t/s |
| Opened with prose, not code | 80.0% | 0.0% | 66.7% | 0.0% |
On throughput: 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 — see the limitations.
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 — the difference shows which habits an instruction can fix and which it cannot.
no_VAR_GLOBALminimal · full rulesno_named_paramminimal · full rulesno_pou_fbminimal · full rulesno_pou_programminimal · full rulesno_single_quoteminimal · full rulesno_c_commentminimal · full rulesno_lowercase_kwminimal · full rulesno_cast_functionminimal · full rulesno_double_quoteminimal · full rulesno_var_retainminimal · full rulesno_select_fnminimal · full rulesno_TIME_literalminimal · full rulesno_direct_addressminimal · full rulesno_based_literalminimal · full rulesno_STRING_typeminimal · full rulesFor comparison, this fine-tune's violations across its 14 delivered replies in the same condition: none.
Highlighted tokens are the ones that decide whether the code is accepted by the IDE. Both columns are verbatim model output.
The request: 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'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 t
Here is the Structured Text solution designed for Mikrodev LogicStudio (IEC 61131-3).
This approach uses a **Type Definition** to encapsulate machine data, an **Array of Structures** to handle all three machines simultaneously in loops, and global variables to manage state persistence across scan cycles.
```st
(*
Project: Packaging Machine Monitoring System
Description: Monitors 3 identical packaging machines using arrays for scalability.
Handles Temperature deviation, Belt Speed, Cycle Counting, and EStop logic.
*)
TYPE T_MachineData :
(* Configuration *)
SetpointTemp : REAL; // Target temperature (e.g., in Celsius)
ActualTemp : REAL; // Current sensor reading
(* Process Parameters *)
BeltSpeed : REAL; // Speed of the belt (e.g., m/s or RPM)
(* Status & Control *)
IsRunning : BOOL; // True if machine is in active running state
CycleCounter : INT := 0; // Counts completed cycles
(* Faulting State *)
MachineFaulted : BOOL; // Indicates current fault status for this specific machine
END_TYPE
(* Global Variables - Persist across scan cycles *)
VAR_GLOBAL
Machines : ARRAY[1..3] OF T_MachineData;
PrevCycleDone : ARRAY[1..3] OF BOOL := (FALSE, FALSE, FALSE); // For rising edge detection
(* External Inputs/Outputs *)
EStopInput : BOOL; // Emergency Stop Input from hardware/controller
TotalFaultCount
no_VAR_GLOBALno_double_quoteno_lowercase_kwno_single_quote```iecst
(* Machine monitoring for 3 identical packaging machines *)
(* Each machine has the same structure and logic applied via array *)
TYPE MACHINE :
nState : INT := 0; (* 0=STOPPED, 1=RUNNING, 2=FAULTED *)
rTempSetpoint : REAL;
rTempActual : REAL;
rBeltSpeed : REAL;
nCycleCount : DINT;
bEStop : BOOL;
bFaulted : BOOL;
END_TYPE
VAR_INPUT
arrTempSetpoint : ARRAY[0..2] OF REAL;
arrTempActual : ARRAY[0..2] OF REAL;
arrBeltSpeed : ARRAY[0..2] OF REAL;
arrCycleDone : ARRAY[0..2] OF BOOL;
bEStop : BOOL;
END_VAR
VAR_OUTPUT
arrMachine : ARRAY[0..2] OF MACHINE;
nFaultedCount : INT;
END_VAR
VAR
i : INT;
rCycleDoneEdge : R_TRIG;
END_VAR
(* Initialize machines *)
FOR i := 0 TO 2 DO
arrMachine[i].nState := 0;
arrMachine[i].nCycleCount := 0;
arrMachine[i].bFaulted := FALSE;
END_FOR;
(* Process each machine with identical logic *)
FOR i := 0 TO 2 DO
arrMachine[i].rTempSetpoint := arrTempSetpoint[i];
arrMachine[i].rTempActual := arrTempActual[i];
arrMachine[i].rBeltSpeed := arrBeltSpeed[i];
(* Temperature deviation fault *)
IF arrMachine[i].nState = 1 THEN
IF (arrMachine[i].rTempActual - arrMachine[i].rTempSetpoint) > 5.0
OR (arrMachine[i].rTempSetpoint - arrMachine[i].rTempActual) > 5.0 THEN
arrMachine[i].bFaulted := TRUE;
arrMachine[i].nState := 2;
END_IF;
END_IF;
(* Belt speed fault *)
IF arrMachine[i].nState = 1 AND arrMachine[i].rBeltSpeed < 10.0 THEN
arrMachine[i].bFaulted := TR
no violationsThe base answer opens with two paragraphs of prose, then builds exactly the structure the Mikrodev runtime rejects. It declares a user type with TYPE T_MachineData :, parks the machine array in a VAR_GLOBAL block so it "persists across scan cycles", annotates the fields with C-style // Current sensor reading comments, and wraps the logic in a BEGIN block - a Pascal-ism that is not ST at all. R24 put it at 86.76 after 5671 tokens, flagging no_VAR_GLOBAL, no_double_quote, no_single_quote and no_lowercase_kw; worth knowing that the lowercase-keyword hits landed on English words inside the reply's comment prose rather than on executable statements, while the VAR_GLOBAL, user-type and BEGIN problems are real. The fine-tune spent 876 tokens and scored 100.0. It exposes the per-machine values as plain 1-D arrays in VAR_INPUT, keeps the state code as an INT with an inline (* 0=STOPPED, 1=RUNNING, 2=FAULTED *) comment instead of an enum, and detects the cycle pulse with a declared R_TRIG. One honest caveat, visible in the marks: the fine-tune still emitted a TYPE MACHINE : ... END_TYPE block of its own for the output array. R24 did not flag it and the row counted as clean, but the dialect has no user-defined types, so that declaration has to go before the block will compile.
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 — read it against the base, line for line.
The request: 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
Here is a reusable **Pump Control Function Block** written in Structured Text (ST), compatible with the Mikrodev LogicStudio IDE environment following IEC 61131-3 standards.
### PumpControl FB Definition
This block encapsulates all logic, timers, and fault handling into one instance per pump. It handles start delays, dry-run protection windows, overload latching, restart lockouts, and HMI reporting.
```st
FUNCTION_BLOCK PumpControl :
VAR_INPUT
RunRequest : BOOL; // Operator request to run the pump (e.g., from level switch)
OverloadIn : BOOL; // TRUE = Thermal Overload Relay Tripped / Fault Active
DryRunSwitch : BOOL; // TRUE = Low Suction Pressure Detected (Dry-run condition)
ResetButton : BOOL; // Momentary or Level signal to clear fault latch and reset timers
END_VAR
VAR_OUTPUT
MotorCmd : BOOL; // Output command for the motor starter/drive
FaultFlag : BOOL; // General alarm flag for HMI (TRUE if any active fault)
FaultCode : INT; // Specific code: 0=OK, 1=Overload, 2=Dry Run
END_VAR
VAR
StartDelayTimer : TON; // Handles the 2s start delay after request
SpinUpTimer : TON; // Waits for motor to stabilize (5s) before checking dry-run
CooldownTimer : TON; // Enforces 10s restart lockout after pump stops
MotorRunning : BOOL := FALSE; // Internal flag to track if we are currently running
END_VAR
// --- Logic Implementation ---
// 1. Reset Fault L
no_TIME_literalno_named_paramno_pou_fb```iecst
(* (*
Reusable booster pump control block.
Instantiate once per pump to avoid copy-paste.
*) *)
VAR_INPUT
bRunReq : BOOL; (* (* Operator run request *) *)
bOverload : BOOL; (* (* Motor thermal overload contact *) *)
bDryRun : BOOL; (* (* Low suction / dry-run switch *) *)
bFaultReset : BOOL; (* (* Fault reset button *) *)
bStartPulse : BOOL; (* (* Start pulse for 2s delay *) *)
bStopPulse : BOOL; (* (* Stop pulse for 10s off time *) *)
END_VAR
VAR_OUTPUT
bMotorRun : BOOL; (* (* Motor run command *) *)
bFault : BOOL; (* (* Latched fault flag *) *)
nFaultCode : INT; (* (* Fault code for HMI *) *)
END_VAR
VAR
tStartDelay : TON; (* (* 2s start delay *) *)
tDryRunDelay : TON; (* (* 5s dry-run evaluation delay *) *)
tOffDelay : TON; (* (* 10s minimum off time *) *)
bStartDone : BOOL; (* (* Start delay elapsed *) *)
bDryRunEval : BOOL; (* (* Dry-run evaluation ready *) *)
bFaultLatched : BOOL; (* (* Latched fault *) *)
END_VAR
VAR CONSTANT
START_DELAY_MS : INT := 2000;
DRYRUN_DELAY_MS: INT := 5000;
OFF_DELAY_MS : INT := 10000;
END_VAR
(* (* Start delay *) *)
tStartDelay(bStartPulse, START_DELAY_MS);
bStartDone := tStartDelay.Q;
(* (* Dry-run evaluation delay *) *)
tDryRunDelay(bMotorRun, DRYRUN_DELAY_MS);
bDryRunEval := tDryRunDelay.Q;
(* (* Off delay *) *)
tOffDelay(NOT bMotorRun, OFF_DELAY_MS);
(* (* Fault logic *)
no violationsThis is the clearest case of the base fighting the dialect. It opens with FUNCTION_BLOCK PumpControl : - the IDE owns that shell, code is body-only - declares its timers with C-style // Handles the 2s start delay after request comments, and then, further down the reply, calls every one of them the CODESYS way: the scorer recorded StartDelayTimer(IN:=FALSE, PT := T#2S), CooldownTimer(IN:=TRUE) and SpinUpTimer(IN:=TRUE) under no_named_param and no_TIME_literal, where Mikrodev wants positional arguments and INT milliseconds. It also tried to assign to its own input with OverloadIn := FALSE;, argued with itself in a comment about whether that was legal, and finally ran out at the full 8192-token budget mid-thought. Score 88.24. The fine-tune produced the whole block in 1139 tokens at 100.0: START_DELAY_MS : INT := 2000 in a VAR CONSTANT block, tStartDelay(bStartPulse, START_DELAY_MS) called positionally, tOffDelay(NOT bMotorRun, OFF_DELAY_MS) for the restart lockout, and the three instances shown separately as asked. The flaw to fix by hand is cosmetic but real, and it is in the marks: this reply doubled its comment delimiters throughout, (* (* 2s start delay *) *), which is not valid ST and which the scorer does not catch.
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 — read it against the base, line for line.
The request: 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
(* Drilling Station Cycle Logic *)
VAR
(* Inputs *)
bGuardClosed : BOOL;
bPartPresent : BOOL;
btnStart : BOOL;
swDownLimit : BOOL;
swUpLimit : BOOL;
btnEStop : BOOL;
alrDrillOverload : BOOL;
(* Outputs *)
clmClampClosed : BOOL;
mtrFeedMotor : BOOL;
mtrRetractMotor : BOOL;
vlvOpenClamp : BOOL;
(* Timers - clamp pressure build, dwell at depth, open clamp delay *)
tClampPressure : TON;
tDwellAtDepth : TON;
tOpenClampDelay : TON;
(* State machine for step sequencing *)
eState : (IDLE, CLAMPING, DRILLING, DWELL, RETRACTING, OPEN_CLAMP);
(* Fault state flag *)
bFaultActive : BOOL;
END_VAR
(* Safety check: E-stop or overload drops all outputs and sets fault *)
IF btnEStop OR alrDrillOverload THEN
clmClampClosed := FALSE;
mtrFeedMotor := FALSE;
mtrRetractMotor := FALSE;
vlvOpenClamp := TRUE; (* Open clamp for safety *)
bFaultActive := TRUE;
END_IF
(* Reset fault when reset button pressed and no active faults *)
IF btnReset AND NOT (btnEStop OR alrDrillOverload) THEN
bFaultActive := FALSE;
clmClampClosed := FALSE; (* Ensure clamp is open during reset *)
mtrFeedMotor := FALSE;
mtrRetractMotor := FALSE;
END_IF
(* Only allow cycle to start if guard closed, part present, no fault, and not already running *)
IF NOT bFaultActive AND btnStart THEN
CASE eState OF
IDLE:
(* Start clamp closing sequence *)
tClampPressure(IN :
no_named_param```iecst
(* Drilling station cycle with clamp, drill, feed, dwell, retract, and clamp-open sequence *)
(* Safety: E-stop or overload forces fault state; reset button clears fault *)
VAR_INPUT
bGuardClosed : BOOL;
bPartPresent : BOOL;
bStartCycle : BOOL;
bEStop : BOOL;
bResetBtn : BOOL;
bDrillOverload : BOOL;
bClampClosed : BOOL;
bDownLimit : BOOL;
bUpLimit : BOOL;
END_VAR
VAR_OUTPUT
bClampMotor : BOOL;
bDrillMotor : BOOL;
bFeedMotor : BOOL;
bClampOpen : BOOL;
bStationFault : BOOL;
nPartsDone : INT;
END_VAR
VAR
nState : INT := 0;
tClamp : TON;
tDwell : TON;
bFault : BOOL;
END_VAR
VAR CONSTANT
CLAMP_BUILD_MS : INT := 1500;
DWELL_MS : INT := 3000;
CLAMP_OPEN_MS : INT := 800;
END_VAR
(* Safety interlocks *)
IF bEStop OR bDrillOverload THEN
bStationFault := TRUE;
bClampMotor := FALSE;
bDrillMotor := FALSE;
bFeedMotor := FALSE;
bClampOpen := FALSE;
nState := 0;
RETURN;
END_IF;
CASE nState OF
0: (* Idle *)
bClampMotor := FALSE;
bDrillMotor := FALSE;
bFeedMotor := FALSE;
bClampOpen := FALSE;
IF bGuardClosed AND bPartPresent AND bStartCycle THEN
nState := 1;
END_IF;
1: (* Clamp close *)
bClampMotor := TRUE;
tClamp(TRUE, CLAMP_BUILD_MS);
IF tClamp.Q THEN
bClampMotor := FALSE;
nState := 2;
END_IF;
2: (* Dri
no violationsHere the base model had the entire rule book in its system prompt and still could not stop naming timer parameters. It dropped the POU wrapper and avoided TIME literals - prompting bought that much - but wrote tClampPressure(IN := TRUE); and five more calls in the same style, six flagged named-parameter tokens in total, which is the single rule it broke: no_named_param, 95.59, 3392 tokens. It never gave those timers a period either, so the 1.5 s, 3 s and 0.8 s values from the request are simply absent, and it referenced btnReset in the fault-reset branch without ever declaring it. It also typed its step variable as an anonymous enumeration, eState : (IDLE, CLAMPING, DRILLING, DWELL, RETRACTING, OPEN_CLAMP);, which the scorer did not flag but the dialect does not support. The fine-tune needed 809 tokens for 100.0: CLAMP_BUILD_MS : INT := 1500 and DWELL_MS : INT := 3000 as constants, tClamp(TRUE, CLAMP_BUILD_MS) and tDwell(TRUE, DWELL_MS) positional, a numeric CASE nState OF step chain, and an E-stop branch at the top that drops every output and RETURNs before the sequencer can run. The step logic still deserves review - the dwell is nested inside the down-limit branch and the retract step drives the feed motor in the same direction as the drill step - but nothing in it violates the dialect.
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 — read it against the base, line for line.
All 15 prompts, realistic condition (no rules in the system prompt). Prompt text is in the appendix below.
| Prompt | Base | R24 | ChrF | tok | Fine-tune | R24 | ChrF | tok | Rules the base broke |
|---|---|---|---|---|---|---|---|---|---|
analog_scale_raw_to_baranalog_math | violation | 91.18 | 32.82 | 7197 | clean | 100.0 | 42.58 | 995 | no_cast_function, no_pou_fb, no_select_fn |
setpoint_clamp_and_rampanalog_math | violation | 97.06 | 27.55 | 6389 | clean | 100.0 | 51.15 | 524 | no_pou_program |
heater_power_avg_squaredanalog_math | no code | 100.0 | 0.0 | 8192 | clean | 100.0 | 45.36 | 520 | — |
timer_start_delaytimer_time_based | violation | 91.18 | 28.21 | 8060 | clean | 100.0 | 41.15 | 673 | no_VAR_GLOBAL, no_c_comment, no_pou_program |
timer_blink_oscillatortimer_time_based | violation | 98.53 | 22.08 | 1739 | clean | 100.0 | 30.25 | 793 | no_double_quote |
timer_offdelay_coastdowntimer_time_based | clean | 100.0 | 5.65 | 8192 | clean | 100.0 | 41.73 | 460 | — |
io_terminal_seal_inio_misc | no code | 100.0 | 0.0 | 8192 | clean | 100.0 | 46.95 | 545 | — |
status_word_bit_maskio_misc | no code | 100.0 | 0.0 | 8192 | clean | 100.0 | 49.83 | 509 | — |
first_out_fault_annunciatorio_misc | violation | 94.12 | 34.8 | 6621 | no code | 100.0 | 0.0 | 8192 | no_VAR_GLOBAL, no_pou_program |
machine_cycle_full_programprogram_structure | violation | 92.65 | 30.93 | 5122 | clean | 100.0 | 46.5 | 989 | no_named_param, no_pou_program |
reusable_pump_blockprogram_structure | violation | 88.24 | 28.56 | 8192 | clean | 100.0 | 41.88 | 1139 | no_TIME_literal, no_named_param, no_pou_fb |
analog_scale_helperprogram_structure | violation | 97.06 | 30.24 | 6952 | clean | 100.0 | 23.58 | 907 | no_pou_fb |
retain_counter_power_cycledata_memory | violation | 85.29 | 30.49 | 1304 | clean | 100.0 | 33.4 | 836 | no_STRING_type, no_VAR_GLOBAL, no_c_comment, no_single_quote, no_var_retain |
shared_project_valuedata_memory | violation | 94.12 | 39.82 | 5166 | clean | 100.0 | 38.49 | 450 | no_VAR_GLOBAL, no_pou_fb |
machine_data_groupingdata_memory | violation | 86.76 | 35.64 | 5671 | clean | 100.0 | 40.42 | 876 | no_VAR_GLOBAL, no_double_quote, no_lowercase_kw, no_single_quote |
Quantisation compresses the weights: Q8_0 keeps roughly 8 bits per weight, Q6_K about 6, Q4_K_M about 4. Smaller file, less VRAM, slightly noisier arithmetic. All sizes here are GiB as reported by the build tooling: 9.11 GiB (Q8_0), 7.04 GiB (Q6_K), 5.38 GiB (Q4_K_M). A note on the filenames: the "-tc" in qwen35_9b-tc.*.gguf is a build-pipeline tag left over from the tool-call training stage. It does not mark a tool-calling build. These files are published and supported as chat-only, as described above. On the code task the quant spread is small. An earlier internal 2000-prompt evaluation of these same fine-tunes measured code composite 0.836 (Q8_0), 0.832 (Q6_K) and 0.828 (Q4_K_M), with ChrF 47.4 / 46.0 / 44.5 - roughly 0.008 of composite across the whole range, far less than the difference between prompting styles. That evaluation's code holdout overlapped the training distribution by about 84%, so those absolute code scores are inflated and weakly discriminative between models. They are quoted here only to compare quantisations within this one model, and must not be used to rank the four models against each other. The large Q4 penalty reported earlier for this family was measured on the tool-calling task, where low precision damaged structured argument emission (0.816 at Q8_0 down to 0.642 at Q4_K_M). Tool calling has been dropped from this line as a product decision, so for a chat-only deployment the quantisation choice is a memory decision, not an accuracy decision. Methodology, stated the same way in all four cards. The R24 and clean figures here are computed over the whole reply. Base replies were prose-wrapped 66.7-100% of the time and fine-tune replies 0-20%, so whole-reply scoring flatters the fine-tunes; scoring only the fenced code moves the pooled base clean rate from 6.7% to 10.0%. For this model's own base arm the two variants agree exactly (6.7% minimal, 26.7% full under both), and the base-to-fine-tune gap is not sensitive to the choice - but the variant is named for the record. Reproducibility. The head-to-head ran Ollama tags stcoder_qwen35-9b:q8 against base qwen3.5:9b; the siblings were stcoder_qwen25-7b:q8 / qwen2.5-coder:7b, stcoder_qwen25-14b:q6 / qwen2.5-coder:14b, and stcoder_gemma4-12b:q6 / gemma4:12b. The run data records no quantisation at all for the base arms, so "effectively Q4" is an inference from the Ollama library default, not a recorded value. The comparison is therefore not precision-matched.
From an earlier 2,000-prompt internal evaluation of these same fine-tunes: code composite Q8 0.836 / Q6 0.832 / Q4 0.828; ChrF 47.4 / 46.0 / 44.5. The head-to-head study on this page used the Q8_0 build. File sizes are GiB.
Important caveat. 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 within-model quantisation comparison, which is what they are cited for here — not to rank the four models against each other. For cross-model ranking use the identical-conditions table in section 2.
240 controlled generations across four model families, 15 prompts each, run twice per model
under two prompting conditions. Greedy decoding (temperature 0, seed 42),
8,192-token budget, local Ollama on an RTX 5080 (16 GB).
temperature 0,
seed 42) at
num_ctx 16384 /
num_predict 8192. The Modelfiles published with
the models default to temperature 0.2 and num_ctx 8192 — the
sequence length the models were trained at — because that is the better interactive
default. Match the three measured values to reproduce these figures; otherwise expect
run-to-run variation.stcoder-qwen35-9b removes the dialect problem for this base: R24 100.0 with an empty violation table on every delivered reply, no POU wrappers, no TIME literals, no named function-block parameters, and code that lands close to the hand-written reference (ChrF 40.9 over its 14 delivered minimal-prompt replies, 38.2 over all 15). It also cut its base's runaway reasoning from 6345 tokens to 1227 and its answer time from 54.5 s to 17.2 s. What it does not do is guarantee an answer - one reply in fifteen reasoned until the budget ran out and returned nothing - nor guarantee working logic, since three clean replies in this study contained real defects and doubled comment delimiters appeared in 2 of 15 minimal and 6 of 15 full-condition replies. It is chat-only: tool calling was trained, measured, and then dropped from this line as a product decision. If you have not already chosen, start with the line default, stcoder-qwen25-7b at Q6_K, and come here for the reasoning family. Either way, keep the context at 8192 or more, treat the output as a fast first draft in the right dialect, and compile everything in LogicStudio before it goes near a machine.
| ID | Prompt as sent to the model |
|---|---|
analog_scale_raw_to_bar | 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. |
setpoint_clamp_and_ramp | 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. |
heater_power_avg_squared | 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. |
timer_start_delay | We'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? |
timer_blink_oscillator | 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's no hardware flasher relay on this panel so it has to be done in software. |
timer_offdelay_coastdown | 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. |
io_terminal_seal_in | 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. |
status_word_bit_mask | 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 'drive healthy' 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. |
first_out_fault_annunciator | 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. |
machine_cycle_full_program | 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. |
reusable_pump_block | 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. |
analog_scale_helper | 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. |
retain_counter_power_cycle | 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. |
shared_project_value | 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. |
machine_data_grouping | 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'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's cycle-done input; EStop faults all of them. Also give me how many machines are currently faulted. Write it in ST. |