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