Skip to content

Make cpp-mode parameters writable after construction (fix #350) - #352

Merged
toruseo merged 3 commits into
toruseo:mainfrom
toruseoagent:pr/posthoc-parameter-write
Jul 31, 2026
Merged

Make cpp-mode parameters writable after construction (fix #350)#352
toruseo merged 3 commits into
toruseo:mainfrom
toruseoagent:pr/posthoc-parameter-write

Conversation

@toruseoagent

Copy link
Copy Markdown
Contributor

Summary

Fixes #350.

With the C++ backend, assigning node.flow_capacity (or most other Node/Link/Vehicle/World parameters) after construction was silently ignored: the assignment only rebound a Python attribute on the wrapper object and never reached the C++ engine. This PR makes the wrapper parameters live read/write properties that forward to the C++ engine, so post-construction assignment behaves consistently with the Python backend.

Design

Two small descriptors in uxsim_cpp_wrapper.py remove the need for per-attribute getter/setter boilerplate:

  • CppProperty: one-line declaration of a write-through attribute forwarding to the underlying C++ object (e.g. u = CppProperty('vmax'))
  • CppWorldProperty: same for CppWorld parameters that must also work before the C++ world is created (value lives in a Python backing attribute until then)

Parameters with Python-mode-specific semantics remain explicit properties, so the special cases are visible at a glance.

Now readable/writable after construction

Object Parameters
Node x, y, signal_offset, flow_capacity, flow_capacity_remain, number_of_lanes (in addition to existing signal, signal_phase, signal_t)
Link u, kappa, tau, w, capacity, delta, delta_per_lane, merge_priority, number_of_lanes, signal_group, length (in addition to existing capacity_in/out(_remain))
Vehicle orig, dest (accept Node object or name string)
World DUO_UPDATE_TIME, DUO_UPDATE_WEIGHT, DUO_NOISE (read dynamically by the C++ engine, matching Python's runtime use)

Python-mode compatibility details:

  • node.flow_capacity = v alone is sufficient: it also (re)initializes flow_capacity_remain and derives number_of_lanes when unset, mirroring Node.__init__ (as suggested in the issue)
  • Unset node.flow_capacity / node.number_of_lanes now read as None as in Python mode (previously -1.0 / 0)
  • free_flow_speed, jam_density, jam_density_per_lane, q_star, k_star remain inert bookkeeping attributes, exactly as in Python mode (use u/kappa or change_free_flow_speed()/change_jam_density() to affect the simulation)

No C++ side changes (all needed fields were already exposed via def_rw).

Tests

Validation

  • Simulation results are bit-identical to the pre-change wrapper on a 10x10 grid scenario (~48k trips, 5 seeds): same TTT to the last digit in every run
  • Cross-mode check (Python vs C++, 5 seeds): completed trips identical in all seeds; TTT ratio mean 1.027 with no statistically significant difference (seed-level variation from differing route-choice RNG streams, unchanged by this PR)

Benchmark

1 thread (OMP_NUM_THREADS=1), 10x10 grid, ~9.5k platoons, 1080 timesteps, includes scenario build + simulation + basic analysis:

  • Interleaved A/B vs pre-change wrapper (12 alternating rounds, same seed): median 0.745 s (old) vs 0.726 s (new) — no measurable overhead (difference within VM noise). An initial hot-loop regression in init_after_tmax_fix (~780k property reads) was found by profiling and fixed in the second commit.
  • C++ mode vs Python mode on the same scenario (5 seeds): median 0.83 s vs 12.4 s — ~15x speedup, unchanged by this PR.

🤖 Generated with Claude Code

toruseoagent and others added 3 commits July 31, 2026 01:45
…ruction

Fixes the silent no-op reported in issue toruseo#350: assigning
Node.flow_capacity (and other parameters) after construction was only
rebinding a Python attribute on the wrapper and never reached the C++
engine.

- Add CppProperty descriptor: write-through attribute forwarding to the
  underlying C++ object, declared as one-liners on the wrapper classes
- Add CppWorldProperty descriptor: same for CppWorld parameters that
  must also work before the C++ world is created (DUO_UPDATE_TIME,
  DUO_UPDATE_WEIGHT, DUO_NOISE)
- Node: x, y, signal_offset, flow_capacity, flow_capacity_remain,
  number_of_lanes are now live read/write properties; setting
  flow_capacity alone also initializes flow_capacity_remain and derives
  number_of_lanes when unset, mirroring Python Node.__init__; unset
  values read as None as in Python mode
- Link: u, kappa, tau, w, capacity, delta, delta_per_lane,
  merge_priority, number_of_lanes, signal_group, length are now live
  read/write properties; free_flow_speed/jam_density/q_star/k_star stay
  inert bookkeeping attributes exactly as in Python mode
- Vehicle: orig/dest are now live read/write properties (accept Node or
  name string)
- Remove write-only backing attributes _capacity_out_remain /
  _capacity_in_remain
- Add 7 tests covering post-hoc assignment (identity with
  at-construction setting, cross-mode consistency with Python backend,
  and knob effectiveness)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- init_after_tmax_fix read length/u (now live C++ properties) inside a
  per-timestep list comprehension, causing ~780k C++ attribute accesses
  on a 360-link/1080-step scenario; hoist the free-flow travel time and
  use np.full
- Cache Vehicle orig/dest on the Python side (the C++ engine never
  changes them itself); writes still go through to C++

Interleaved A/B benchmark (12 rounds, 1 thread) shows no measurable
overhead vs the pre-property wrapper (median -2.6%, within VM noise),
with bit-identical simulation results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Python-side caches (Vehicle._orig/_dest, Link._length) could go silently
stale if the C++ object is modified directly via the raw API or by
future engine features, reintroducing the issue toruseo#350 failure mode in
the read direction. Read live from C++ instead:

- Vehicle.orig/dest resolve the wrapper node by integer id list index
  (same pattern as Vehicle.link), avoiding name-string conversion
- Link.length becomes a plain CppProperty (now always reads as float)

Profiling showed the caches had no measurable benefit; the earlier
regression was the init_after_tmax_fix hot loop, which remains fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@toruseo
toruseo enabled auto-merge (squash) July 31, 2026 04:07
@toruseo
toruseo merged commit 0d57ebb into toruseo:main Jul 31, 2026
10 checks passed
@toruseoagent
toruseoagent deleted the pr/posthoc-parameter-write branch July 31, 2026 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cpp=True: assigning Node.flow_capacity after construction is silently ignored (inconsistent with Python backend)

2 participants