Add a layered low-throttle fuel correction on top of the Arrow delete tune
Real-world reports: stalling/choking under ~10% throttle and while blipping the throttle at low speed, after snorkel removal with no main-table correction for the extra airflow. Confirmed via a genuine percentage-delta pulled from 20184dynoTuneSteveO2-Disable.hex (also snorkel-removed) vs its own stock baseline 20186Map.hex, in the Low-throttle fuel tables, restricted to the throttle columns actually implicated (raw throttle <= 100, ~10%). compose_lowthrottle.py composes onto the already-composed 20262-arrow-delete-composed.hex (not raw stock 20262) -- additive on top of the existing SAI/O2-off + idle-trim layer, same "respect what's already there" principle as compose_arrow_delete.py. Percentage-based per-cell, not flat additive, since Arrow's own table already has its own exhaust-specific correction baked in. Output: derived/20262-arrow-delete-lowthrottle-composed.hex. Checksum patched and verified valid; confirmed layer-1's device flags and trim bytes survive unchanged underneath. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FP2GaxS9HkUdL5sLBnjKje
This commit is contained in:
148
tunie/research/reference-maps/compose_lowthrottle.py
Normal file
148
tunie/research/reference-maps/compose_lowthrottle.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Compose a Low-throttle fuel correction on top of an already-composed map.
|
||||
|
||||
Second layer in a two-step derivation:
|
||||
20262Map.hex
|
||||
-> compose_arrow_delete.py -> 20262-arrow-delete-composed.hex (layer 1)
|
||||
-> compose_lowthrottle.py -> 20262-arrow-delete-lowthrottle-composed.hex (layer 2)
|
||||
|
||||
Layer 2 is additive on top of layer 1's own values, not a fresh edit of stock
|
||||
20262 -- it composes with whatever layer 1 already did (SAI/O2 off, idle-CO
|
||||
trim), the same "respect what's already there" principle used throughout
|
||||
this project.
|
||||
|
||||
Motivation (2026-09-10 session): real-world reports of stalling/choking
|
||||
under ~10% throttle and while blipping the throttle at low speed, after
|
||||
snorkel removal with no main-table correction for the extra airflow.
|
||||
Confirmed via a real percentage-delta pulled from a genuine reference file
|
||||
that also had its snorkel removed: `20184dynoTuneSteveO2-Disable.hex` vs
|
||||
its own stock baseline `20186Map.hex`, in the "Low-throttle fuel" tables
|
||||
(cyl 1 @ fe=15, cyl 2 @ fe=16 -- see TABLES.md), restricted to the
|
||||
throttle columns actually implicated by the reported symptom (raw
|
||||
throttle-axis value <= 100, i.e. <= 10.0%).
|
||||
|
||||
Method: percentage delta, not flat additive -- VE/fuel-mass values scale
|
||||
with magnitude, and Arrow's own low-throttle table already differs from
|
||||
stock (it has its own exhaust-specific correction baked in), so the right
|
||||
operation is "apply the same proportional richening the reference delete
|
||||
map needed" on top of whatever value is already there, not overwrite it.
|
||||
|
||||
pct(row, col) = (delete_ref[row][col] - stock_ref[row][col]) / stock_ref[row][col]
|
||||
new(row, col) = round(base[row][col] * (1 + pct(row, col)))
|
||||
|
||||
Usage:
|
||||
python3 compose_lowthrottle.py \\
|
||||
derived/20262-arrow-delete-composed.hex \\
|
||||
20186Map.hex 20184dynoTuneSteveO2-Disable.hex \\
|
||||
derived/20262-arrow-delete-lowthrottle-composed.hex
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import checksum
|
||||
from decode_map import decode, encode
|
||||
from table_map import resolve
|
||||
from toggle_devices import repack, unpack
|
||||
|
||||
LOW_THROTTLE_TABLES = ("Low-throttle fuel — cyl 1", "Low-throttle fuel — cyl 2")
|
||||
MAX_SANE_VALUE = 20000 # real VE values in every reference file top out ~11000
|
||||
|
||||
|
||||
def _table_by_name(resolved: dict, name: str) -> dict:
|
||||
for t in resolved["tables"]:
|
||||
if t["name"] == name:
|
||||
return t
|
||||
raise KeyError(name)
|
||||
|
||||
|
||||
def compose(base_raw: bytes, stock_raw: bytes, delete_raw: bytes, base_path: str, stock_path: str, delete_path: str) -> tuple[bytes, list[str]]:
|
||||
base_dec = decode(base_raw)
|
||||
base_rom, positions = unpack(base_dec)
|
||||
base_rom = bytearray(base_rom)
|
||||
|
||||
base_resolved = resolve(base_path)
|
||||
stock_resolved = resolve(stock_path)
|
||||
delete_resolved = resolve(delete_path)
|
||||
|
||||
throttle_axis = base_resolved["throttle_axis"]
|
||||
low_cols = [i for i, v in enumerate(throttle_axis) if v <= 100]
|
||||
|
||||
notes = [f"low-throttle columns touched (raw<=100): {[throttle_axis[c] for c in low_cols]}"]
|
||||
|
||||
for name in LOW_THROTTLE_TABLES:
|
||||
base_t = _table_by_name(base_resolved, name)
|
||||
stock_t = _table_by_name(stock_resolved, name)
|
||||
delete_t = _table_by_name(delete_resolved, name)
|
||||
|
||||
off = base_t["offset"] # this file's OWN resolved offset, not another file's
|
||||
rows, cols = base_t["rows"], base_t["cols"]
|
||||
|
||||
changed = 0
|
||||
pct_min, pct_max = None, None
|
||||
for row in range(rows):
|
||||
for col in low_cols:
|
||||
stock_v = stock_t["data"][row][col]
|
||||
delete_v = delete_t["data"][row][col]
|
||||
base_v = base_t["data"][row][col]
|
||||
|
||||
pct = (delete_v - stock_v) / stock_v if stock_v else 0.0
|
||||
new_v = round(base_v * (1 + pct))
|
||||
new_v = max(0, min(65535, new_v))
|
||||
|
||||
if new_v > MAX_SANE_VALUE:
|
||||
notes.append(
|
||||
f" WARNING {name} row={row} col={col}: "
|
||||
f"computed {new_v} exceeds sanity bound {MAX_SANE_VALUE}"
|
||||
)
|
||||
|
||||
if new_v != base_v:
|
||||
changed += 1
|
||||
pct_min = pct if pct_min is None else min(pct_min, pct)
|
||||
pct_max = pct if pct_max is None else max(pct_max, pct)
|
||||
|
||||
byte_off = off + (row * cols + col) * 2
|
||||
struct.pack_into(">H", base_rom, byte_off, new_v)
|
||||
|
||||
pct_range = f"{pct_min:+.1%} to {pct_max:+.1%}" if changed else "no change"
|
||||
notes.append(f"{name} (0x{off:05X}): {changed} cells touched, delta range {pct_range}")
|
||||
|
||||
before_sum = checksum.stored(bytes(base_rom))
|
||||
checksum.patch(base_rom)
|
||||
after_sum = checksum.compute(bytes(base_rom))
|
||||
notes.append(f"checksum: stored {before_sum:04x} (stale) -> patched {after_sum:04x}")
|
||||
|
||||
repacked = repack(base_dec, bytes(base_rom), positions)
|
||||
out = encode(repacked)
|
||||
assert decode(out) == repacked, "round-trip failed"
|
||||
return out, notes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 5:
|
||||
print(
|
||||
"usage: compose_lowthrottle.py <base_composed.hex> <stock.hex> "
|
||||
"<delete_reference.hex> <outfile.hex>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
base_path, stock_path, delete_path, out_path = sys.argv[1:5]
|
||||
out, notes = compose(
|
||||
open(base_path, "rb").read(),
|
||||
open(stock_path, "rb").read(),
|
||||
open(delete_path, "rb").read(),
|
||||
base_path, stock_path, delete_path,
|
||||
)
|
||||
open(out_path, "wb").write(out)
|
||||
for n in notes:
|
||||
print(" ", n)
|
||||
print(f"wrote {out_path} ({len(out)} bytes)")
|
||||
print("NOTE: only the under-~10%-throttle columns of the two Low-throttle")
|
||||
print(" fuel tables were touched. Main fuel tables (>10% throttle)")
|
||||
print(" remain untouched stock-Arrow calibration.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Binary file not shown.
Reference in New Issue
Block a user