stcoder-qwen25-7b is a chat-only model fine-tuned from Qwen2.5-Coder-7B-Instruct that writes IEC 61131-3 Structured Text in the Mikrodev LogicStudio dialect. It is the default recommendation of this four-model line, at Q8_0 (7.54 GiB): in the head-to-head study it was the fastest per answer of the four (mean 5.8 s per reply with a short system prompt, 5.1 s with the full rule book) and it had the highest ChrF of the models that delivered all 15 replies (38.5).
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_7b-tc.q8_0.gguf, 7.54 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
9.5 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_7b-tc.q4_k_m.gguf | Q4_K_M | 4.36 GiB | solid | |
qwen25_coder_7b-tc.q6_k.gguf | Q6_K | 5.82 GiB | solid | |
qwen25_coder_7b-tc.q8_0.gguf | Q8_0 | 7.54 GiB | solid | tested in this study |
Start here unless you have a specific reason not to. First, the boundary: this is a chat model. You describe a plant requirement, it replies with a Structured Text body. Tool calling and function calling were trained and evaluated for this line, but the capability was not usable across the four models, so it was dropped as a product decision and these builds are published and supported as chat-only models. There are no tool or function calls, no JSON actions, no MCP schemas. Head-to-head standing, minimal (short) system prompt, this study, ChrF averaged over delivered replies so the denominator is identical for all four: stcoder-qwen25-7b 93.3% rule-clean, 100% delivery (15/15), ChrF 38.5, composite 0.844, mean 340 output tokens, 5.8 s per reply. Its actual standing is: highest ChrF among the models that delivered every reply (38.5; stcoder-qwen35-9b scored 40.9 but delivered 14 of 15); joint second on clean rate with stcoder-qwen35-9b (both 93.3%), behind stcoder-qwen25-14b's 100.0%; composite 0.844, which is inside the noise band with stcoder-qwen35-9b (0.845) and stcoder-qwen25-14b (0.842); fastest per answer of the four. Which one should I download? Default: stcoder-qwen25-7b at Q6_K (5.82 GiB), or Q4_K_M (4.36 GiB) on an 8 GB card, because it is the fastest per answer, the smallest to run, and at the top of the head-to-head on ChrF at full delivery. Choose stcoder-qwen25-14b at Q6_K (11.29 GiB) only on 16 GB or more, if you want the one model that was 100% rule-clean in both prompt conditions, accepting the lowest head-to-head ChrF (34.9) and roughly half the generation rate per token (about 50 tok/s against this model's 103.6). Choose stcoder-qwen35-9b only if you want the reasoning family, accepting about 3.6x the output tokens per answer (1227 against 340) and one reply in fifteen that spent its entire 8,192-token budget on reasoning and returned no code at all after 99 seconds, which is why it is the slowest per answer at 17.2 s. Choose stcoder-gemma4-12b only for non-Qwen lineage diversity; it is the weakest fine-tuning case in this study (80.0% clean in the full condition against its fully-prompted base's 73.3%, and its base scored a higher mean R24 than the fine-tune in that condition). At matched quantisation this model is roughly half the size of the 14B, not a third: 7B Q6_K 5.82 GiB against 14B Q6_K 11.29 GiB. Three disclosures apply to all four cards: no tool calling in this release; compile every block in LogicStudio before deployment; and quantisation was not controlled in the head-to-head (fine-tunes ran at Q8_0 or Q6_K, the stock bases at whatever the Ollama library default is).
| Model | Clean | Delivered | ChrF | Composite | Out tok | Per reply | Recommended build |
|---|---|---|---|---|---|---|---|
stcoder-qwen25-7b (this model)7B · 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-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.
Ollama 0.3 or later. llama.cpp works equally well if you prefer it; the GGUF files are standard.
winget install Ollama.Ollama # or: brew install ollama
Take Q6_K for a 12 GB card, Q4_K_M for 8 GB, Q8_0 for 16 GB or more. You need exactly one file, not the whole repository.
hf download Mikrodev/stcoder-qwen25-7b-gguf qwen25_coder_7b-tc.q6_k.gguf --local-dir .
Keep the system prompt short. In this study the short 'minimal' system prompt gave a higher ChrF (38.5) than pasting the entire rule book (35.3) - the dialect is in the weights, and restating it adds nothing. Greedy decoding matches how the model was measured.
cat > Modelfile <<'EOF' FROM ./qwen25_coder_7b-tc.q6_k.gguf PARAMETER temperature 0 PARAMETER top_p 1 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 a PLC programming assistant for the Mikrodev LogicStudio IDE. You write IEC 61131-3 Structured Text for Mikrodev controllers. Reply with the code body only." EOF
Names the model locally. Use a tag that records the quant so you can keep more than one. For reference, the tags actually run in the study were stcoder_qwen25-7b:q8 (fine-tune) and qwen2.5-coder:7b (stock base); the sibling arms were stcoder_qwen25-14b:q6, stcoder_qwen35-9b:q8, stcoder_gemma4-12b:q6 against qwen2.5-coder:14b, qwen3.5:9b and gemma4:12b.
ollama create stcoder-qwen25-7b:q6_k -f Modelfile
Describe the plant requirement in plain language, as you would to a colleague. Do not list dialect rules.
ollama run stcoder-qwen25-7b:q6_k "Silo inlet: on a fill request with the high-level switch clear, open the inlet valve; 3 seconds after the valve open-confirm is made, start the blower. Any fault stops the blower at once and closes the valve 2 seconds later."
The model returns a code body - variable blocks plus statements, no PROGRAM or FUNCTION_BLOCK wrapper, because the IDE owns the shell. Compile before you go anywhere near a controller. In the 15-prompt study the fine-tune broke no dialect rule on 14 of 15 prompts, but rule-clean is not the same as logically correct: the study found an undeclared enumeration member, an unused constant and a reset branch that counted up instead of clearing.
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:7b) 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 | 13.3% | 93.3% | 33.3% | 100.0% |
| Delivered code at all | 100.0% | 100.0% | 100.0% | 100.0% |
| R24 rule compliance | 93.34 | 99.8 | 97.16 | 100.0 |
| ChrF vs reference code | 32.4 | 38.5 | 29.5 | 35.3 |
| Output tokens (mean · median) | 728 · 680 | 340 · 269 | 670 · 621 | 304 · 254 |
| Time per reply | 7.2s | 5.8s | 6.6s | 5.1s |
| Throughput | 154.1 t/s | 103.6 t/s | 153.0 t/s | 104.4 t/s |
| Opened with prose, not code | 100.0% | 6.7% | 100.0% | 6.7% |
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_named_paramminimal · full rulesno_TIME_literalminimal · full rulesno_pou_programminimal · full rulesno_pou_fbminimal · full rulesno_based_literalminimal · full rulesno_structminimal · full rulesno_single_quoteminimal · full rulesno_var_persistentminimal · full rulesno_STRING_typeminimal · full rulesno_user_functionminimal · full rulesno_cast_functionminimal · full rulesFor comparison, this fine-tune's violations across its 15 delivered
replies in the same condition: no_based_literal×1.
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 an example of a reusable pump control block in IEC 61131-3 Structured Text (ST) that you can instantiate once per pump. The block includes the necessary inputs and outputs, as well as the logic to handle the behavior described. ### Pump Control Block ```st FUNCTION_BLOCK FB_PumpControl VAR_INPUT RunRequest : BOOL; // Request to start the pump MotorThermalOverload : BOOL; // Motor thermal overload contact DryRunSwitch : BOOL; // Low suction switch (dry-run) FaultReset : BOOL; // Fault reset signal END_VAR VAR_OUTPUT MotorRunCommand : BOOL; // Command to run the motor FaultFlag : BOOL; // Fault flag indicating if a fault has occurred FaultCode : INT; // Fault code for HMI display END_VAR VAR StartDelayTimer : TON; // Timer for start delay DryRunTimer : TON; // Timer to evaluate dry-run switch MotorRunning : BOOL; // Flag indicating if the motor is running LastFaultTime : TIME; // Time when a fault occurred END_VAR // Main logic IF RunRequest AND NOT FaultFlag THEN StartDelayTimer(IN := TRUE, PT := T#2s); ELSE StartDelayTimer(IN := FALSE); END_IF; IF MotorThermalOverload OR DryRunSwitch THEN FaultFlag := TRUE; FaultCode := 1; // Thermal overload or dry-run fault code LastFaultTime := GET_TIME(); ELSE IF FaultReset THEN FaultFlag := FALSE; FaultCode := 0; // No fault code END_IF; IF StartDelayTimer.Q AND NOT MotorRunning THEN DryRunTimer(IN := TRUE, PT := T#5s); ELS
no_TIME_literalno_named_paramno_pou_fbno_pou_program```iecst
VAR_INPUT
bRunReq : BOOL;
bOverload : BOOL;
bDryRun : BOOL;
bResetFault : BOOL;
END_VAR
VAR_OUTPUT
bMotorRun : BOOL;
bFault : BOOL;
nFaultCode : INT;
END_VAR
VAR
tStartDelay : TON;
tDryRunDelay : TON;
tStopDelay : TON;
rsFault : RS;
bFaultLatched : BOOL;
END_VAR
VAR CONSTANT
START_DELAY_MS : INT := 2000;
DRY_RUN_DELAY_MS : INT := 5000;
STOP_DELAY_MS : INT := 10000;
END_VAR
(* 2 s start delay *)
tStartDelay(bRunReq AND NOT bOverload AND NOT bDryRun, START_DELAY_MS);
(* Dry-run only after the motor has started *)
tDryRunDelay(tStartDelay.Q, DRY_RUN_DELAY_MS);
(* Stop delay: 10 s after the pump stops *)
tStopDelay(NOT bMotorRun, STOP_DELAY_MS);
(* Latch fault on overload or dry-run, reset only when reset button pressed *)
rsFault(bOverload OR (tDryRunDelay.Q AND bDryRun), bResetFault);
bFaultLatched := rsFault.Q1;
(* Motor run: start delay, no dry-run while stopping *)
bMotorRun := tStartDelay.Q AND NOT bDryRun AND NOT tStopDelay.Q AND NOT bFaultLatched;
(* Fault flag and code *)
bFault := bFaultLatched;
IF bFaultLatched THEN
IF bOverload THEN
nFaultCode := 1;
ELSIF tDryRunDelay.Q AND bDryRun THEN
nFaultCode := 2;
END_IF;
ELSE
nFaultCode := 0;
END_IF;
```
Each pump instance is called with a unique `bRunReq`, `bOverload`, `bDryRun`, and `bResetFault` input, and the same `bMotorRun`, `bFault`, and `nFaultCode` output. The three instances are instantiated in the main program with different input and output names.
no violationsThis is the case for fine-tuning over prompting. Given the full rule book, the base model did absorb the millisecond rule - PT := 2000 and PT := 5000 instead of T#2s and T#5s - but kept the named-parameter call style it had just been told to drop, StartDelayTimer(IN := TRUE, PT := 2000) and DryRunTimer(IN := TRUE, PT := 5000), kept FUNCTION_BLOCK PumpControl, and still used GET_TIME() arithmetic for the restart inhibit. R24 92.65 with the complete rule book in front of it; the scorer located six named-parameter violations, two of them in the block's own instantiation example, plus the FUNCTION_BLOCK wrapper. The fine-tune, given the same prompt, produced the same body-only positional form as before at R24 100 in 514 tokens against the base's 1154. One honest caveat visible in this pair: the fine-tune's trailing paragraph claims the fault latch is shared across all three pump instances, which is not what its own code does - the latch is per-instance. Trust the code panel, not the paragraph under it.
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 IEC 61131-3 standards.
```structured-text
PROGRAM DrillingStationCycle
VAR
GuardClosed : BOOL;
PartPresent : BOOL;
NoAlarms : BOOL;
StartPressed : BOOL;
ClampClosed : BOOL;
DrillMotorRunning : BOOL;
HeadFeedingDown : BOOL;
DownLimitSwitch : BOOL;
UpLimitSwitch : BOOL;
EStopPressed : BOOL;
DrillOverload : BOOL;
PartCounter : INT := 0;
ClampTimer : TON; // Timer for clamp pressure build
DrillMotorTimer : TON; // Timer for drill motor dwell
RetractTimer : TON; // Timer for retraction after drilling
FaultState : BOOL := FALSE;
END_VAR
// Step Sequencing and Timers
IF NOT FaultState THEN
IF GuardClosed AND PartPresent AND NoAlarms THEN
IF StartPressed THEN
ClampClosed := TRUE;
ClampTimer(IN := TRUE, PT := T#1500ms); // 1.5 seconds for clamp pressure build
IF ClampTimer.Q THEN
DrillMotorRunning := TRUE;
DrillMotorTimer(IN := TRUE, PT := T#3s); // 3 seconds dwell at depth
IF DrillMotorTimer.Q THEN
HeadFeedingDown := TRUE;
IF DownLimitSwitch THEN
HeadFeedingDown := FALSE;
RetractTimer(IN := TRUE, PT := T#800ms); // 0.8 seconds retraction after drilling
IF Re
no_TIME_literalno_named_paramno_pou_programHere is the complete drilling station cycle program. I used the IEC 61131-3 Structured Text standard function blocks for the timers and the logic. The station is in one of four states: IDLE, CLAMPING, DRILLING, and RETRACTING. The E-stop and drill overload are monitored with a priority check — the E-stop wins. The part counter increments only when the part is successfully drilled and the clamp is opened. ```iecst (* Drilling station cycle with clamp, drill, and retract sequence. *) TYPE E_Station : (IDLE, CLAMPING, DRILLING, RETRACTING); END_TYPE VAR_INPUT bGuardClosed : BOOL; bPartPresent : BOOL; bStart : BOOL; bEStop : BOOL; bDrillOverload : BOOL; bReset : BOOL; END_VAR VAR_OUTPUT bClamp : BOOL; bDrill : BOOL; bHeadFeed : BOOL; bFault : BOOL; nPartCount : INT; END_VAR VAR eState : E_Station := IDLE; tClamp : TON; tDrill : TON; tRetract : TON; tDwell : TON; bClampCmd : BOOL; bDrillCmd : BOOL; bHeadCmd : BOOL; bFaultSet : BOOL; bFaultClr : BOOL; END_VAR VAR CONSTANT CLAMP_MS : INT := 1500; DRILL_MS : INT := 10000; RETRACT_MS : INT := 2000; DWELL_MS : INT := 3000; DWELL_S : INT := 3; END_VAR (* Priority: E-stop > drill overload > start *) bClampCmd := FALSE; bDrillCmd := FALSE; bHeadCmd := FALSE; (* Fault detection *) bFaultSet := bEStop OR bDrillOverload; bFaultClr := bReset AND NOT bEStop AND NOT bDrillOverload; (* Fault latch *) IF bFaultSet THEN bFault :=
no violationsThe base emitted PROGRAM DrillingStationCycle and expressed the whole sequence as nested IF statements - clamp inside guard inside no-fault - with each duration as a TIME literal in a named-parameter call: ClampTimer(IN := TRUE, PT := T#1500ms), DrillMotorTimer(IN := TRUE, PT := T#3s), RetractTimer(IN := TRUE, PT := T#800ms). R24 88.24. The fine-tune replaced the nesting with an enumerated state variable and a CASE eState OF sequencer, put every duration in VAR CONSTANT as milliseconds (CLAMP_MS : INT := 1500 and so on) and called timers positionally, tClamp(TRUE, CLAMP_MS). It also separated command variables from outputs and gated all three outputs on NOT bFault in one place. R24 100, ChrF 47.18 against the base's 31.36 - one of this model's largest ChrF gains of the fifteen prompts (the largest is the drying-oven setpoint row, 38.00 to 55.07). It is also the clearest illustration of why you compile: the CASE branches to eState := DWELLING, but DWELLING is missing from the TYPE E_Station : (IDLE, CLAMPING, DRILLING, RETRACTING) declaration, and nPartCount is zeroed on every IDLE scan immediately after being incremented. Rule-clean, not correct.
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: 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 repo
To solve this problem, you need to maintain a persistent storage for the lifetime total count across power cycles. One way to achieve this is by using non-volatile memory (NVM) or a file system on an external storage device like an SD card. However, since Mikrodev LogicStudio IDE does not directly support NVM or file systems, we can use a workaround by storing the lifetime total in a variable that is saved and restored from a configuration file when the PLC starts.
Here's how you can implement this logic using Structured Text (ST):
1. **Define Variables:**
- `bPartDetect`: Input signal from the photo eye.
- `nShiftTotal`: Shift total counter.
- `nLifetimeTotal`: Lifetime total counter.
- `bShiftReset`: Input signal to reset the shift total.
2. **Configuration File Handling:**
- Use a configuration file to store and load the lifetime total when the PLC starts and stops.
3. **Counting Logic:**
- Increment the shift total on each bottle detection.
- Reset the shift total when `bShiftReset` is active.
- Save the lifetime total to the configuration file before stopping the PLC.
- Load the lifetime total from the configuration file when starting the PLC.
Here's an example of how you can implement this in ST:
```st
VAR
bPartDetect : BOOL; // Input signal from the photo eye
nShiftTotal : INT := 0; // Shift total counter
nLifetimeTotal : INT := 0; // Lifetime total counter
bShiftReset : BOOL; // Input signal to reset the shift total
no_STRING_typeno_cast_functionno_single_quoteno_user_function```iecst
VAR_INPUT
bPartDetect : BOOL;
bShiftReset : BOOL;
END_VAR
VAR_OUTPUT
nShiftCount : DINT;
nLifetimeCount : DINT;
END_VAR
VAR
rEdge : R_TRIG;
bResetEdge : BOOL;
bResetActive : BOOL;
bResetDone : BOOL;
END_VAR
VAR CONSTANT
MAX_LIFETIME : DINT := 3000000000;
END_VAR
rEdge(bPartDetect);
bResetEdge := bShiftReset;
(* Shift counter - reset on rising edge of bShiftReset *)
IF bResetEdge THEN
bResetActive := TRUE;
END_IF;
IF NOT bShiftReset THEN
bResetActive := FALSE;
END_IF;
IF bResetActive THEN
nShiftCount := nShiftCount + 1;
IF nShiftCount >= MAX_LIFETIME THEN
nShiftCount := 0;
END_IF;
END_IF;
(* Lifetime counter - never resets *)
IF rEdge.Q THEN
nLifetimeCount := nLifetimeCount + 1;
IF nLifetimeCount >= MAX_LIFETIME THEN
nLifetimeCount := 0;
END_IF;
END_IF;
```
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 | clean | 100.0 | 28.71 | 637 | clean | 100.0 | 34.59 | 195 | — |
setpoint_clamp_and_rampanalog_math | clean | 100.0 | 38.0 | 671 | clean | 100.0 | 55.07 | 282 | — |
heater_power_avg_squaredanalog_math | violation | 88.24 | 36.3 | 703 | clean | 100.0 | 43.37 | 286 | no_TIME_literal, no_named_param, no_pou_program |
timer_start_delaytimer_time_based | violation | 91.18 | 30.26 | 535 | clean | 100.0 | 41.96 | 242 | no_TIME_literal, no_named_param |
timer_blink_oscillatortimer_time_based | violation | 91.18 | 31.92 | 560 | clean | 100.0 | 34.3 | 189 | no_TIME_literal, no_named_param |
timer_offdelay_coastdowntimer_time_based | violation | 91.18 | 30.07 | 590 | clean | 100.0 | 38.41 | 206 | no_TIME_literal, no_named_param |
io_terminal_seal_inio_misc | violation | 91.18 | 36.06 | 680 | clean | 100.0 | 42.67 | 269 | no_TIME_literal, no_named_param |
status_word_bit_maskio_misc | violation | 97.06 | 34.39 | 642 | violation | 97.06 | 41.29 | 386 | no_based_literal |
first_out_fault_annunciatorio_misc | violation | 97.06 | 35.92 | 807 | clean | 100.0 | 32.23 | 442 | no_pou_program |
machine_cycle_full_programprogram_structure | violation | 88.24 | 31.36 | 949 | clean | 100.0 | 47.18 | 972 | no_TIME_literal, no_named_param, no_pou_program |
reusable_pump_blockprogram_structure | violation | 85.29 | 32.3 | 1178 | clean | 100.0 | 40.78 | 468 | no_TIME_literal, no_named_param, no_pou_fb, no_pou_program |
analog_scale_helperprogram_structure | violation | 97.06 | 27.76 | 774 | clean | 100.0 | 25.94 | 233 | no_pou_fb |
retain_counter_power_cycledata_memory | violation | 88.24 | 30.96 | 805 | clean | 100.0 | 35.25 | 266 | no_STRING_type, no_cast_function, no_single_quote, no_user_function |
shared_project_valuedata_memory | violation | 97.06 | 31.97 | 647 | clean | 100.0 | 34.96 | 255 | no_pou_program |
machine_data_groupingdata_memory | violation | 97.06 | 29.83 | 749 | clean | 100.0 | 29.54 | 404 | no_struct |
Quantisation is how many bits each weight keeps. Q8_0 is close to the original precision, Q6_K is a modest reduction, Q4_K_M roughly halves the file again. For this model the published files are 7.54 GiB, 5.82 GiB and 4.36 GiB. All file sizes in this card are GiB, as reported by the build tooling. Which quant to take depends on the task you care about. On an earlier internal 2000-prompt evaluation of these same fine-tunes, this model's code task showed a code composite of 0.844-0.846 and ChrF in the 38-52 band across Q4_K_M, Q6_K and Q8_0 - a spread of 0.002 or less. Important caveat: 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. It is quoted here purely for the within-model quant comparison and must not be used to rank the four models against each other; for cross-model comparison use the head-to-head figures in this card. With that caveat attached, the practical reading holds: for writing Structured Text the quantisation choice is close to irrelevant, so pick the largest file your card holds comfortably and spend the rest on context. The large Q4 penalty in that older material was on the tool-calling task, which needs exact JSON argument structure and degrades sharply at 4 bits. Tool calling is not part of this release. It was trained and measured across the line: the Qwen3.5-9B family did learn it (tool-call composite 0.816 at Q8_0, 0.805 at Q6_K, collapsing to 0.642 at Q4_K_M), while the code-focused families including this one scored 0.02-0.06 and the Gemma family exactly 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. So the Q4 tool-calling warning does not apply to how you will use this model. Practical reading: Q6_K is the default, Q4_K_M is the 8 GB fallback with no measurable code-quality cost, Q8_0 is for reproducing the exact numbers in this card. One further caveat: the base-versus-fine-tune comparison below was not quantisation-controlled. The fine-tune ran at Q8_0 (tag stcoder_qwen25-7b:q8); the run data records no quantisation at all for the base arm (tag qwen2.5-coder:7b), so calling it "effectively Q4" is an inference from the Ollama library default, not a recorded value.
From an earlier 2,000-prompt internal evaluation of these same fine-tunes: code composite 0.844-0.846 and ChrF ~50 across Q4/Q6/Q8 (spread under 0.002). 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-qwen25-7b buys you one thing reliably: Structured Text that obeys the Mikrodev LogicStudio dialect without being told the rules - R24 99.80 and 14 of 15 replies rule-clean from a short system prompt (whole-reply scoring), where the same base weights managed R24 93.34 and 2 of 15, and where pasting the entire rule book into the base still left two thirds of its replies non-compliant. It does that in under half the tokens and about 5-6 seconds a reply, from a 5.82 GiB file, which is why it is the default recommendation of the four at Q6_K: fastest per answer, smallest to run, 100% delivery in all four arms, and the highest ChrF among the models that delivered every reply. What it does not buy you is correct logic: it produced an undeclared enumeration member, a counter that incremented on reset and an oscillator that reuses one timer instance twice per scan, all while scoring a perfect dialect score. It is also a chat-only model - tool calling was dropped for this release. Compile every block in LogicStudio and read the declarations before anything reaches a controller.
| 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. |