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.
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 (qwen25_coder_14b-tc.q8_0.gguf, 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 — 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 |
|---|---|---|---|---|
qwen25_coder_14b-tc.q4_k_m.gguf | Q4_K_M | 8.37 GiB | solid | |
qwen25_coder_14b-tc.q6_k.gguf | Q6_K | 11.29 GiB | tight | tested in this study |
qwen25_coder_14b-tc.q8_0.gguf | Q8_0 | 14.62 GiB | no, 24 GB+ | recommended |
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'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'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.
| 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-14b (this model)14B · 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-9b9B · 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. 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.
# Windows / macOS: download the installer from ollama.com # Linux: curl -fsSL https://ollama.com/install.sh | sh
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.
pip install -U "huggingface_hub[cli]" # 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 .
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.
cat > Modelfile <<'EOF' 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 """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'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.""" EOF
This copies the GGUF into Ollama'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 "effectively Q4" is an inference from the Ollama library default, not a recorded value.
ollama create stcoder-qwen25-14b:q4 -f Modelfile
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.
ollama run stcoder-qwen25-14b:q4 "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."
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.
ollama run stcoder-qwen25-14b:q4 --verbose >>> /set parameter temperature 0
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'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.
# In Mikrodev LogicStudio: paste into the block body, then Compile and read the diagnostics.
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
(qwen2.5-coder:14b) 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 | 0.0% | 100.0% | 40.0% | 100.0% |
| Delivered code at all | 100.0% | 100.0% | 100.0% | 100.0% |
| R24 rule compliance | 92.36 | 100.0 | 97.16 | 100.0 |
| ChrF vs reference code | 32.6 | 34.9 | 32.5 | 36.8 |
| Output tokens (mean · median) | 713 · 674 | 288 · 262 | 664 · 606 | 313 · 292 |
| Time per reply | 11.1s | 8.6s | 10.2s | 8.7s |
| Throughput | 84.3 t/s | 50.0 t/s | 84.1 t/s | 49.6 t/s |
| Opened with prose, not code | 80.0% | 0.0% | 100.0% | 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_pou_programminimal · full rulesno_named_paramminimal · full rulesno_TIME_literalminimal · full rulesno_based_literalminimal · full rulesno_output_arrowminimal · full rulesno_structminimal · full rulesno_pou_fbminimal · full rulesno_oopminimal · full rulesno_cast_functionminimal · full rulesFor comparison, this fine-tune's violations across its 15 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: 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
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 FUNCTION_BLOCK 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 METHOD Run : VOID VAR DryRunDetected : BOOL; END_VAR // Start delay timer logic IF NOT MotorRunning AND RunRequest THEN StartDelayTimer(IN := TRUE, PT := T#2s); ELSE StartDelayTimer(IN := FALSE); END_IF; // Check if the motor can start after the delay IF StartDelayT
no_TIME_literalno_named_paramno_oopno_output_arrowno_pou_fbno_pou_program```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
no violationsThe 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'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'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.
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
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 PROGRAM DrillingStationCycle 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 := T#1.5S; DWELL_AT_DEPTH_TIME : TIME := T#3S; CLAMP_OPEN_DELAY_TIME : TIME := T#0.8S; END_VAR // State machine logic CASE State OF 0: // Idle state ClampClose := FALSE; DrillMotorStart := FALSE; HeadFeedDown := FALSE;
no_TIME_literalno_named_paramno_pou_program```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 :=
no violationsThe 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'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.
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: 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.
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 PROGRAM FlashingBeacon 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 := T#2s THEN Timer.PT := T#2s; END_IF; // Main logic for flashing beacon IF WarningFlag THEN // Start the timer if it's not running IF NOT Timer.Q THEN Timer(ET:=T#0ms); END_IF; // Alternate blink state based on timer elapsed time IF Timer.ET >= T#1500ms THEN BlinkState := NOT BlinkState; Timer(ET:=T#0ms); // 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's state (on or off). - `Timer`: A timer object (`TON`) used to manage the timing of the bl
no_TIME_literalno_named_paramno_pou_program```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;
```
no violationsRed 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 | 97.06 | 29.66 | 660 | clean | 100.0 | 38.82 | 262 | no_pou_program |
setpoint_clamp_and_rampanalog_math | violation | 97.06 | 37.79 | 699 | clean | 100.0 | 36.11 | 287 | no_pou_program |
heater_power_avg_squaredanalog_math | violation | 97.06 | 37.42 | 800 | clean | 100.0 | 50.0 | 288 | no_pou_program |
timer_start_delaytimer_time_based | violation | 88.24 | 29.93 | 583 | clean | 100.0 | 36.18 | 193 | no_TIME_literal, no_named_param, no_pou_program |
timer_blink_oscillatortimer_time_based | violation | 88.24 | 29.66 | 589 | clean | 100.0 | 33.75 | 180 | no_TIME_literal, no_named_param, no_pou_program |
timer_offdelay_coastdowntimer_time_based | violation | 88.24 | 32.43 | 760 | clean | 100.0 | 37.17 | 186 | no_TIME_literal, no_named_param, no_pou_program |
io_terminal_seal_inio_misc | violation | 88.24 | 37.01 | 347 | clean | 100.0 | 25.82 | 157 | no_TIME_literal, no_named_param, no_pou_program |
status_word_bit_maskio_misc | violation | 91.18 | 35.07 | 444 | clean | 100.0 | 43.77 | 254 | no_based_literal, no_cast_function, no_pou_program |
first_out_fault_annunciatorio_misc | violation | 97.06 | 32.56 | 541 | clean | 100.0 | 26.18 | 273 | no_pou_program |
machine_cycle_full_programprogram_structure | violation | 88.24 | 36.36 | 1049 | clean | 100.0 | 48.03 | 541 | no_TIME_literal, no_named_param, no_pou_program |
reusable_pump_blockprogram_structure | violation | 79.41 | 33.14 | 1385 | clean | 100.0 | 36.06 | 568 | no_TIME_literal, no_named_param, no_oop, no_output_arrow, no_pou_fb, no_pou_program |
analog_scale_helperprogram_structure | violation | 97.06 | 30.97 | 770 | clean | 100.0 | 24.46 | 298 | no_pou_program |
retain_counter_power_cycledata_memory | violation | 97.06 | 23.2 | 582 | clean | 100.0 | 25.7 | 250 | no_pou_program |
shared_project_valuedata_memory | violation | 97.06 | 31.36 | 674 | clean | 100.0 | 32.69 | 208 | no_pou_program |
machine_data_groupingdata_memory | violation | 94.12 | 31.91 | 808 | clean | 100.0 | 28.76 | 380 | no_pou_program, no_struct |
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'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.
From an earlier 2,000-prompt internal evaluation of these same fine-tunes: code composite 0.851-0.852 and ChrF 52.0 across Q4/Q6/Q8 (spread under 0.001). The head-to-head study on this page used the Q6_K 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.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.
| 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. |