Mikrodev/stcoder-qwen25-14b-ggufFine-tuned from Qwen/Qwen2.5-Coder-14B-Instruct apache-2.0GGUF · Ollama / llama.cpp

stcoder-qwen25-14b

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.

Base model
Qwen2.5-Coder-14B-InstructQwen/Qwen2.5-Coder-14B-Instruct
Builds
3Q4_K_M / Q6_K / Q8_0
Smallest
8.4 GiBQ4_K_M
Recommended
Q8_014.62 GiB
Dialect-clean
100.0%base: 0.0%
Tool calling
Not supportedchat only
1

Which build should you download?

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.

16.6 GiB free VRAM
Q8_0qwen25_coder_14b-tc.q8_0.gguf · 14.62 GiB
recommended needs a 24 GB-class card. A 16 GiB card cannot fit it - including the RTX 5080 this model's own evaluation ran on, which is why the measured numbers below are Q6_K. On 16 GiB or less take Q6_K
13.3 GiB free VRAM
Q6_Kqwen25_coder_14b-tc.q6_k.gguf · 11.29 GiB
fine — the build this study ran
10.4 GiB free VRAM
Q4_K_Mqwen25_coder_14b-tc.q4_k_m.gguf · 8.37 GiB
not recommended

Short answer

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.

Why not Q4_K_M

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.

FileQuantisationSizeFit on 16 GBNote
qwen25_coder_14b-tc.q4_k_m.ggufQ4_K_M8.37 GiBsolid
qwen25_coder_14b-tc.q6_k.ggufQ6_K11.29 GiBtighttested in this study
qwen25_coder_14b-tc.q8_0.ggufQ8_014.62 GiBno, 24 GB+recommended
2

Is this the right model for you?

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.

Table — the four models on identical measurements
Same 15 requests, realistic prompt, greedy decoding. ChrF is averaged over delivered replies so the denominator is the same for every model.
ModelCleanDeliveredChrFComposite Out tokPer replyRecommended build
stcoder-qwen25-7b
7B · default pick, fastest per answer
93.3%15/1538.50.8443405.8sQ8_0 · 7.54 GiB
stcoder-qwen25-14b (this model)
14B · only one 100% clean in both conditions
100.0%15/1534.90.8422888.6sQ8_0 · 14.62 GiB
stcoder-qwen35-9b
9B · reasoning family, highest ChrF when it answers
93.3%14/1540.90.845122717.2sQ8_0 · 9.11 GiB
stcoder-gemma4-12b
12B · non-Qwen lineage, weakest fine-tune case
86.7%15/1535.90.834018.5sQ8_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.

Read this before you start

  • Chat only — no tool calling. You describe what you need and the model writes Structured Text. Tool calling was trained and evaluated for this line: the Qwen3.5-9B family did learn it (tool-call composite 0.816 at Q8_0, falling to 0.642 at Q4_K_M) while the code-focused families scored 0.02–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 models. Do not build an agent on them.
  • Always compile before deployment. The model produces dialect-correct code, which is not the same as correct control logic. Compile it in LogicStudio and review the logic.
  • It is a small local model. Expect to iterate on a prompt, not to receive a finished project.
3

Set-up, step by step

  1. Install Ollama

    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
  2. Download one GGUF file

    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 .
  3. Write a Modelfile next to the GGUF

    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
  4. Register the model with Ollama

    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
  5. Smoke-test it

    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."
  6. Set temperature 0 when you need repeatability

    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
  7. Paste into LogicStudio and compile - every time

    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.
4

What to ask it first

These are ordinary single-purpose requests, the kind this model handles well. Type them as they are.

Surface grinder start sequence: when the operator presses start, sound the warning siren for 4 seconds, then energise the spindle contactor. The stop button and a coolant-low fault both drop the spindle immediately.
A body-only block with VAR_INPUT / VAR_OUTPUT / VAR CONSTANT, SIREN_MS : INT := 4000, a positional TON(...) call and an RS latch for the spindle, read back as rsSpindle.Q1.
Fresh prompt, not one of the 15 study prompts - written to exercise the same capability as the study's timer_start_delay row, where this fine-tune scored R24 100 in 193 tokens while the base broke three rule classes on the equivalent request.
Dust extractor run-on: the extraction fan runs whenever the panel saw runs and keeps running for 45 seconds after the saw stops. E-stop cuts it immediately.
A single TON with PURGE_MS : INT := 45000, a self-holding fan equation and an explicit IF bEStop THEN ... override at the end.
Fresh prompt. Same shape as the study's timer_offdelay_coastdown row (R24 100, 186 tokens). Off-delay via a TON plus a hold term is the Mikrodev idiom, since TOF conventions and named parameters from other IDEs are forbidden.
Flash the tower lamp while the line is in manual mode: lit for 300 ms, dark for 1200 ms.
BLINK_ON_MS and BLINK_OFF_MS as INT constants, a TON restarted on its own Q, and a toggled internal bool gating the lamp.
Fresh prompt, matching the study's timer_blink_oscillator row (R24 100, 180 tokens). Check the duty cycle before you deploy: on the study version the model used a single timer for the whole period, which gives an even blink rather than the asymmetric one asked for.
Scale a pressure transmitter from the analog card: 6000 raw counts is 0 barg, 30000 raw counts is 25 barg. Clamp the result to the range and raise a wire-break flag below 5400 counts. There is an enable bit from the scan logic.
REAL constants for the raw and engineering endpoints, straight REAL arithmetic with no INT_TO_REAL call, two IF clamps instead of LIMIT, and a wire-break branch that forces the output to zero.
Fresh prompt with fresh count endpoints. The study's analog_scale_raw_to_bar row scored R24 100 and ChrF 38.8. Cast functions and MIN/MAX/LIMIT are forbidden in this dialect and the fine-tune avoids both without being told.
Unpack a 16-bit soft-starter status word from Modbus into ready, running, tripped and bypass-closed flags. The manual gives the masks in hex: 16#0004 ready, 16#0010 running, 16#0040 tripped, 16#0100 bypass closed.
The hex masks rewritten as decimal INT constants (4, 16, 64, 256), bitwise AND tests compared against 0, and a shift for any packed warning code.
Fresh prompt with different masks. The study's status_word_bit_mask row was one of this model's stronger rows (R24 100, ChrF 43.8): hex literals are forbidden and the fine-tune converts them silently, while the base emitted them verbatim and lost points.
Clamp a chilled-water setpoint from the HMI to between 6.0 and 14.0 degC, set an out-of-range warning bit, and ramp the working setpoint towards the clamped value in steps of 0.5 per scan. An acknowledge button clears the warning.
LOW_LIMIT / HIGH_LIMIT / MAX_STEP as REAL constants, an IF/ELSIF clamp chain, an R_TRIG on the acknowledge input and a three-branch ramp.
Fresh prompt. On the study's setpoint_clamp_and_ramp row the fine-tune was rule-clean but scored ChrF 36.1 against its base's 37.8 - one of five rows where fidelity to the reference dropped. R_TRIG is the dialect-correct alternative to the edge helpers other IDEs provide.
Average the last 10 samples of motor line current, work out apparent power as average current times 400 V times 1.732, and set an overload flag if power stays above 15 kW for 5 seconds. A reset pushbutton clears the flag.
An ARRAY[0..9] OF REAL ring buffer, a FOR loop over a separate loop counter to sum it, multiplication instead of the forbidden ** operator, and a TON on the overload condition.
Fresh prompt. The study's closest row, heater_power_avg_squared, produced this model's highest-fidelity output (R24 100, ChrF 50.0) and also its clearest logic bug: it reused the ring-buffer index as the FOR loop counter, destroying the index every scan. Read the loop before you compile.
5

What fine-tuning changed, measured

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.

Figure 1 — dialect-clean replies out of 15
Clean means: code was delivered and it broke no dialect rule
basethis fine-tune
No rules in promptrealistic use
0.0%
100.0%
Full rule book in promptcharitable to base
40.0%
100.0%
Figure 2 — per-prompt outcome, no rules in prompt
One tick per test prompt, same order in both rows
cleanrule violation no code delivered
Base
Fine-tune
MeasureBaseThis fine-tuneBase + full rulesFine-tune + full rules
Dialect-clean replies0.0%100.0%40.0%100.0%
Delivered code at all100.0%100.0%100.0%100.0%
R24 rule compliance92.36100.097.16100.0
ChrF vs reference code32.634.932.536.8
Output tokens (mean · median)713 · 674288 · 262664 · 606313 · 292
Time per reply11.1s8.6s10.2s8.7s
Throughput84.3 t/s50.0 t/s84.1 t/s49.6 t/s
Opened with prose, not code80.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.

6

Which dialect rules the base model breaks

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.

Figure 3 — base-model rule violations, qwen2.5-coder:14b — all 9 rules it broke
bar length = share of delivered replies that broke the rule, full width = 100%
no_pou_programminimal · full rules
15/15
0/15
no_named_paramminimal · full rules
6/15
7/15
no_TIME_literalminimal · full rules
6/15
0/15
no_based_literalminimal · full rules
1/15
1/15
no_output_arrowminimal · full rules
1/15
1/15
no_structminimal · full rules
1/15
1/15
no_pou_fbminimal · full rules
1/15
1/15
no_oopminimal · full rules
1/15
0/15
no_cast_functionminimal · full rules
1/15
0/15

For comparison, this fine-tune's violations across its 15 delivered replies in the same condition: none.

7

The same request, side by side

Highlighted tokens are the ones that decide whether the code is accepted by the IDE. Both columns are verbatim model output.

Same prompt, full rule book: the base learns milliseconds but keeps writing IN :=

reusable_pump_block · program_structure · no dialect rules in the system prompt

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

Base · qwen2.5-coder:14b R24 79.41 · 1385 tok
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
STCoder fine-tune R24 100.0 · 568 tok
```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 violations

The same request with the entire rule book pasted into the system prompt, which isolates what prompting alone can buy. The base model clearly read the rules and partly complied: T#2s became a plain 2000 and T#5s became 5000, so the TIME literal rule transferred from prompt to output. Two habits did not. It still wrote FUNCTION_BLOCK PumpControl despite being told the IDE owns the shell, and it still called every timer with named parameters - tStartDelay(IN := FALSE, PT := 2000); tDryRunCheck(IN := FALSE, PT := 5000); nRestartDelay(IN := FALSE, PT := 10000); - six such calls in one reply. Across the full condition, named parameters were flagged in 7 of the 15 base replies, more often than under the minimal prompt, making it the single most prompt-resistant habit in this base model'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.

Drilling station cycle, minimal prompt: a clean state machine that still will not compile

machine_cycle_full_program · program_structure · no dialect rules in the system prompt

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

Base · qwen2.5-coder:14b R24 88.24 · 1049 tok
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
STCoder fine-tune R24 100.0 · 541 tok
```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 violations

The longest request in the set - a full drilling cycle with step sequencing, three timers, a part counter and an E-stop fault state. The base model produced a competent portable-ST program and lost 11.76 points on form alone: PROGRAM DrillingStationCycle as the wrapper, a constants block declaring CLAMP_PRESSURE_BUILD_TIME : TIME := T#1.5S; DWELL_AT_DEPTH_TIME : TIME := T#3S; CLAMP_OPEN_DELAY_TIME : TIME := T#0.8S;, and every timer driven as Timer1(IN := TRUE, PT := CLAMP_PRESSURE_BUILD_TIME);. Three rule classes, 1049 tokens. The fine-tune restructured the whole thing to the dialect in 541 tokens: constants as CLAMP_MS : INT := 1500; DWELL_MS : INT := 3000; OPEN_MS : INT := 800;, a rising-edge trigger rStartEdge(bStart); on the start button, an E-stop branch ahead of the sequencer that drops every output, and a CASE nState OF ladder in which each timer is gated by its own step - tClamp(nState = 1, CLAMP_MS);. R24 100, ChrF 48.03, one of its stronger rows. And it would not build. States 2 and 4 branch on IF bDownLimit and IF bUpLimit, and neither symbol is declared in any VAR block. This is the row to point at whenever someone reads 100% clean as 100% correct: the dialect is right, the shape is right, the structure is better than the base'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.

timer blink oscillator

timer_blink_oscillator · timer_time_based · no dialect rules in the system prompt

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.

Base · qwen2.5-coder:14b R24 88.24 · 589 tok
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
STCoder fine-tune R24 100.0 · 180 tok
```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 violations

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.

8

Every test prompt and its result

All 15 prompts, realistic condition (no rules in the system prompt). Prompt text is in the appendix below.

PromptBaseR24ChrFtok Fine-tuneR24ChrFtokRules the base broke
analog_scale_raw_to_bar
analog_math
violation97.0629.66660clean100.038.82262no_pou_program
setpoint_clamp_and_ramp
analog_math
violation97.0637.79699clean100.036.11287no_pou_program
heater_power_avg_squared
analog_math
violation97.0637.42800clean100.050.0288no_pou_program
timer_start_delay
timer_time_based
violation88.2429.93583clean100.036.18193no_TIME_literal, no_named_param, no_pou_program
timer_blink_oscillator
timer_time_based
violation88.2429.66589clean100.033.75180no_TIME_literal, no_named_param, no_pou_program
timer_offdelay_coastdown
timer_time_based
violation88.2432.43760clean100.037.17186no_TIME_literal, no_named_param, no_pou_program
io_terminal_seal_in
io_misc
violation88.2437.01347clean100.025.82157no_TIME_literal, no_named_param, no_pou_program
status_word_bit_mask
io_misc
violation91.1835.07444clean100.043.77254no_based_literal, no_cast_function, no_pou_program
first_out_fault_annunciator
io_misc
violation97.0632.56541clean100.026.18273no_pou_program
machine_cycle_full_program
program_structure
violation88.2436.361049clean100.048.03541no_TIME_literal, no_named_param, no_pou_program
reusable_pump_block
program_structure
violation79.4133.141385clean100.036.06568no_TIME_literal, no_named_param, no_oop, no_output_arrow, no_pou_fb, no_pou_program
analog_scale_helper
program_structure
violation97.0630.97770clean100.024.46298no_pou_program
retain_counter_power_cycle
data_memory
violation97.0623.2582clean100.025.7250no_pou_program
shared_project_value
data_memory
violation97.0631.36674clean100.032.69208no_pou_program
machine_data_grouping
data_memory
violation94.1231.91808clean100.028.76380no_pou_program, no_struct
9

Q4, Q6 and Q8 — what actually differs

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.

Evidence base — and its limit

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.

10

Strengths, weaknesses, and what not to use it for

Strengths

Weaknesses

Not the right tool for

11

How the numbers were produced

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).

Limitations

  • Quantisation was not matched. The fine-tune ran at higher precision than the stock base build. This plausibly helps the fine-tune on quality and demonstrably penalises it on raw throughput.
  • “Clean” is not “correct”. R24 is reference-free: it certifies that no forbidden construct appears, not that the logic works. Correctness was checked by reading the code, not by compiling it.
  • The published builds do not ship this decoding configuration. These numbers were measured greedily (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.
  • Scope. 15 prompts, one dialect, English only, no tool-calling, and the prompt set was deliberately built on the categories where this dialect diverges from standard IEC. It measures dialect fidelity on dialect-sensitive work, not general PLC competence.
  • Small sample. 15 prompts per condition per model. Treat single-prompt differences as anecdote and the aggregate as the signal.
12

In summary

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.

A

Appendix — the test prompts

IDPrompt as sent to the model
analog_scale_raw_to_barWe 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_rampOperators 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_squaredHeater 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_delayWe'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_oscillatorNeed 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_coastdownOur 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_inOn 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_maskWe 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_annunciatorI 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_programCan 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_blockWe 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_helperI 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_cycleLine 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_valueTank 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_groupingI 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.