Skip to content

Dedupe triplicated wire-size/pack/unpack field lists via WireRecord#1

Open
gavdoubleu wants to merge 9 commits into
event_logging_consistent_sentinelsfrom
wire_record_dedup
Open

Dedupe triplicated wire-size/pack/unpack field lists via WireRecord#1
gavdoubleu wants to merge 9 commits into
event_logging_consistent_sentinelsfrom
wire_record_dedup

Conversation

@gavdoubleu

Copy link
Copy Markdown
Owner

Why

A /code-review medium pass over event_logging_consistent_sentinels (finding IDAS-Durham#4, PLAUSIBLE) flagged that the MPI wire-format code in src/parallel/domain_communicator*.cpp hand-lists each record type's fields three times: once in a *_WIRE_SIZE/magic-number constant, once in pack*, once in unpack*. A recent fix on that branch made the size constants derive from field sizeof instead of hand-maintained literals, closing the risk of the size silently desyncing from field types — but the field list itself was still duplicated by hand across three call sites per record type. A field added to a struct but forgotten in one of those three places breaks wire format undetected until a rank hangs or garbles data.

Two more instances of the same pattern turned up beyond the original three: the Visitor exchange in domain_communicator.cpp (VISITOR_WIRE_HEADER = 33, an unverified magic number), and CoordinatedEncounter's finalized-encounter exchange (entry_size = 29 + participant_count * 4).

What

Adds WireRecord<T, Members...> (include/parallel/domain_communicator_detail.h), a reusable seam that derives wire size, pack, and unpack from a single field list per record type, built on the existing packField/unpackField helpers. Fields needing custom handling (the EncounterReply::status enum transcode, and the variable-length tails on Visitor/CoordinatedEncounter) stay outside WireRecord and are composed manually around it.

Refactors all five record types onto this seam: EncounterProposal, EncounterReply, PendingInfection, CoordinatedEncounter (finalized-encounter), Domain::VisitorData. Also changes applyOnePendingInfection to take const PendingInfection& instead of 8 positional args that mirrored the struct's fields by hand (same duplication risk, one level up).

Written test-first (TDD): tests/test_domain_communicator_detail.cpp unit-tests WireRecord in isolation, no mpirun needed.

Stacked on event_logging_consistent_sentinels (IDAS-Durham#16) since this refactor depends on the sentinel/narrowing changes already on that branch (e.g. infector_symptom_id as uint8_t, kNoSymptomId). Retarget to main once IDAS-Durham#16 merges.

Test plan

  • test_domain_communicator_detail (new, plain ctest, no MPI runtime)
  • test_mpi_communication, test_mpi_advanced, test_mpi_transmission_modes, test_coordinated_encounters under mpirun -np 2 — confirm byte-identical wire format post-refactor
  • Full ctest suite (44/44 pass except a pre-existing, unrelated failure: test_mpi_full_reproducibility fails on missing world_2021.h5 fixture)

gavdoubleu and others added 6 commits July 15, 2026 11:50
…IDAS-Durham#16)

* Add conda environment.yml

Reproducible conda environment for building and running JUNE2. Includes cxx-compiler (GCC on Linux), cmake, hdf5, yaml-cpp, openmpi, Python test deps (h5py, numpy), and optional deps metis and gperftools.

* Consistent sentinels for event-logging IDs, encounter types, symptoms

Event-logging sentinel/fallback choices had drifted: bare -1/255 literals
repeated with no named constant, and infector_symptom_id used 0 both as
the real "recovered" registry id and as Disease::getSymptomId's own
error fallback, so seed-infection rows (no real infector) were
indistinguishable from a recovered infector.

- Add kInvalidPersonId/kInvalidVenueId (-1), kUnknownEncounterTypeId (255),
  kNoSymptomId (255) in core/types.h; replace bare literals across
  PendingInfection, PersonLocation, config.h encounter-type fields.
- Narrow infector_symptom_id/old_symptom_id/new_symptom_id uint16_t -> uint8_t
  end to end (event structs, HDF5 CompType registrations, logger API),
  freeing symptom id 0 to mean only "recovered". Disease's internal
  uint16_t representation is untouched; narrowing happens only at the
  logInfection/logSymptomChange call boundary via explicit static_cast.
- Standardise "nothing to report" string fallbacks in lookup tables:
  "unknown" -> "could not resolve" (value existed but couldn't be
  resolved), "" -> "not applicable" (nothing to report).

Verified: full build passes, test_integration and test_simulator both
pass, and a standalone EventLogger spot-check confirms the HDF5 output
now has uint8 symptom-id columns with 255 (not 0) for seed infections.

* Fix leftover uint16_t/default-value gaps from event-logging sentinel narrowing

Commit dbccfd1 narrowed infector_symptom_id (and related event-logging
fields) to uint8_t and introduced kUnknownEncounterTypeId/kNoSymptomId
sentinels, but two call sites weren't updated to match:

- interaction_manager_partial_presence.cpp: unpacking MPI-packed
  candidate data cast infector_symptom_id to uint16_t, then relied on an
  implicit second narrowing to fit PendingInfection's now-uint8_t field.
  Cast directly to uint8_t instead, matching the field type and the
  explicit style used at the sibling call site in
  interaction_manager_venue.cpp.

- infection_seed.cpp: logInfection call for seed infections omitted the
  encounter-type and symptom-id sentinel arguments added by dbccfd1
  (no infector exists for seeded infections), leaving them at
  compiler/default-initialised values instead of the intended
  kUnknownEncounterTypeId/kNoSymptomId sentinels.

* Finish sentinel-consistency cleanup from dbccfd1

Leftover raw `255` comparisons (interaction_manager.cpp,
interaction_manager_partial_presence.cpp, activity_manager_selection.cpp)
and duplicated fallback strings ("could not resolve", "not applicable" in
event_writer_lookups.cpp, event_merger_lookups.cpp) sat outside the single
point of definition dbccfd1 meant to establish, reintroducing the same
drift risk it fixed elsewhere. Replace with kUnknownEncounterTypeId and
new kCouldNotResolve/kNotApplicable constants (event_types.h).

* Rename kUnknownEncounterTypeId to kDefaultEncounterTypeId; split kUnknownVenueTypeId

encounter_type_id is 255 for every ordinary, non-coordinated encounter
(the vast majority), only getting a real encounter_type_names index for
coordinated encounters. "Unknown" read as an error/undefined state, but
this is the expected default for the staple case, so rename to
kDefaultEncounterTypeId (still 255, purely a naming fix, no behaviour
change).

Considered promoting "uncoordinated" to a real registry entry instead,
but rejected: interaction_manager.cpp and interaction_manager_venue_bins.cpp
use encounter_type_id < encounter_type_names.size() as an implicit
"is this coordinated" test, and the partial-presence v1 invariant relies
on the sentinel staying out of the registry's range, so a real in-range
default id would corrupt encounter stats.

Also split kUnknownEncounterTypeId's accidental reuse in
activity_manager_selection.cpp (testing a venue-type lookup miss from
getVenueTypeId, an unrelated concept) into its own kUnknownVenueTypeId
constant.

* Finish sentinel-constant rollout in interaction_manager

Three venue_type_id defaults in the interaction_manager module still used
the raw literal 255, left over from the event-logging sentinel-consistency
cleanup (dbccfd1..ac7ad42) that introduced kUnknownVenueTypeId. Swap them
in for consistency: ParentAggregate::parent_venue_type_id,
processVenueTransmissions' local venue_type_id, and
resolveVenueTypeAndMatrix's output default.

* Fix MPI wire-format desync in PendingInfection pack/unpack

infector_symptom_id narrowed uint16_t->uint8_t (dbccfd1..ac7ad42) shrank
the packed record by 1 byte, but INFECTION_SIZE stayed a hardcoded 25 and
unpackAndApplyIncoming's local still declared uint16_t. Alltoallv slots
stayed 25 bytes/record while packing wrote 24, so any batch of >1
PendingInfection to the same destination rank desynced from the second
record onwards, corrupting person_id/infector_id/venue etc with no crash.

Fix: derive INFECTION_SIZE/PROPOSAL_WIRE_SIZE/REPLY_WIRE_SIZE from
sizeof(decltype(field)) of the actual struct members instead of literals,
and make unpackAndApplyIncoming's locals decltype-derived too, so a future
struct field-type change can't silently drift pack/unpack apart again.
EncounterReply's status_byte stays a manual uint8_t (deliberate enum
transcode, not drift).

Add regression test: two PendingInfection records routed to the same
destination rank in one round, asserting both round-trip intact.

* Fix resolveInfectorSymptomId no-infector sentinel mismatch

Was returning raw 0 for "no infector", ambiguous with a real symptom
stage 0 from getCurrentSymptomId. infection_seed.cpp already logs
kNoSymptomId (255) for the equivalent seed-infection case; align
resolveInfectorSymptomId's two no-infector paths (infector_id < 0,
visitor not found) to the same sentinel so venue/partial-presence
infections aren't misread by downstream tooling.

* Shorten kCouldNotResolve to fit PersonRecord::sex[16]

"could not resolve" (18 chars) truncated to "could not resol" when
strncpy'd into the 16-byte sex field, silently corrupting HDF5 output
for Sex::UNKNOWN. Shortened sentinel to "unresolved" (10 chars) so it
fits all fixed-size buffer call sites without truncation.

* Finish sentinel rollout for cached_virtual_venue_type_id

Sibling field cached_encounter_type_id was converted to kDefaultEncounterTypeId
in this branch's rollout, but cached_virtual_venue_type_id was left as bare
255 despite serving the same "not yet resolved" role. Use kUnknownVenueTypeId
instead. Field stays int, not uint8_t: it's compared against
EncounterProposal::venue_type_id and populated from
ContactMatrices::matrix_name_to_id, both int, so int is the correct type for
this subsystem.

* Guard uint16_t->uint8_t symptom-id narrowing at Disease load

infector_symptom_id/old_symptom_id/new_symptom_id were narrowed to
uint8_t for event logging and cross-rank transmission, but symptom-tag
ids are assigned unbounded from YAML config with no cap. >255 tags
would silently wrap the id mod 256 at every downstream call site.
Throw at load time instead if symptom_tags count reaches kNoSymptomId
(255, the "not applicable" sentinel).

* Apply clang-format to fix CI lint failure

Reflow arg lists in infectPerson, filterAndSortActiveLocations,
resolvePartialPresenceInfections, buildPersonRecords to satisfy
80-col clang-format check; no semantic change.

---------

Co-authored-by: gavdoubleu <gavin.a.woolman@durham.ac.uk>
Co-authored-by: Martha Correa-Delval <165719743+mtcorread@users.noreply.github.com>
Proposal/Reply/PendingInfection/CoordinatedEncounter/Visitor each hand-listed
their fields separately in a size constant, pack, and unpack. Add WireRecord
seam (single field list -> size/pack/unpack derived); refactor all five to
use it. status_byte, variable-length tails stay hand-composed around it.
applyOnePendingInfection now takes const PendingInfection& instead of 8 args.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ngInfection

Comment claimed a post-unpack lookup set home_array_index; no such lookup
exists, it was always hardcoded to -1 (already its default, since it's
excluded from kInfectionWire). Corrected comment, dropped the redundant
assignment/copy.
WireRecord (f8ca6ae) derives each MPI record's wire size/pack/unpack from a
single field list, but nothing checked that list still covers every
wire-relevant field of its struct - a field added later and forgotten in
the list would silently stop going over the wire, with no build or test
failure.

Add a static_assert next to each kXWire definition:
- EncounterProposal/EncounterReply/PendingInfection are all-scalar, so
  sizeof(T) is a workable drift proxy.
- CoordinatedEncounter and VisitorData end in a std::set/std::vector tail
  packed manually outside WireRecord, so sizeof(T) is dominated by the
  container's own layout instead; use offsetof(T, tail_member) up to where
  the WireRecord-covered header ends instead.

Coarser than an exact field-by-field checksum (a same-or-smaller field
landing in existing trailing padding could still slip through unnoticed),
but needs no hand-maintained field list, so it does not reintroduce the
triplicated-listing failure mode WireRecord was written to remove. Verified
by temporarily adding an unlisted field to EncounterProposal and confirming
the build breaks.
…time check

CoordinatedEncounter is non-standard-layout (std::set<PersonId> member), so
static_assert(offsetof(CoordinatedEncounter, participants) == 32) relied on
conditionally-supported behaviour: compiles under GCC/Clang w/ a
-Winvalid-offsetof warning today, but the computed offset isn't a portable
guarantee across standard libraries (README lists Clang 10+ as supported,
not just GCC/libstdc++). Swap it for a runtime check in
test_domain_communicator_detail.cpp using well-defined pointer subtraction
on a real object, which needs no standard-layout assumption. Also add a
static_assert(is_standard_layout_v<VisitorData>) guard next to the
still-legitimate offsetof tripwire in domain_communicator.cpp, so VisitorData
gaining a similarly non-standard-layout member fails loudly at compile time
instead of quietly regressing the same way.
Reflow arg/initialiser-list wrapping and static_assert indentation
across wire-record and event-logging call sites; no semantic change.
gavdoubleu and others added 3 commits July 23, 2026 15:04
* Add conda environment.yml

Reproducible conda environment for building and running JUNE2. Includes cxx-compiler (GCC on Linux), cmake, hdf5, yaml-cpp, openmpi, Python test deps (h5py, numpy), and optional deps metis and gperftools.

* Guard MPI-only venue-ownership check in runSlotTransmission

Non-MPI builds only forward-declare DomainManager, so the unguarded
domain_mgr_->getDomain() call fails to compile. Wrap in #ifdef USE_MPI,
matching every other domain_mgr_ call site in this file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: gavdoubleu <gavin.a.woolman@durham.ac.uk>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
… rollout (IDAS-Durham#19)

* Add conda environment.yml

Reproducible conda environment for building and running JUNE2. Includes cxx-compiler (GCC on Linux), cmake, hdf5, yaml-cpp, openmpi, Python test deps (h5py, numpy), and optional deps metis and gperftools.

* Align june_events sentinel handling w/ engine's consistent-sentinel rollout

IDAS-Durham#16 narrowed infector_symptom_id uint16_t->uint8_t & made the engine write
kNoSymptomId (255) directly for no-infector rows (seed/fomite/compartmental),
replacing the old ambiguous 0 ("recovered"). june_events predated that fix:
decode/sentinels.py only had one generic UNSET_REGISTRY_INDEX=255, and
load_enriched.py worked around the ambiguity with a manual NaN-masking pass
keyed on infector_id==-1 before registry decode.

- decode/sentinels.py, decode/__init__.py: split UNSET_REGISTRY_INDEX into
  DEFAULT_ENCOUNTER_TYPE_ID and NO_SYMPTOM_ID, mirroring kDefaultEncounterTypeId
  and kNoSymptomId in include/core/types.h even though both are 255 - the
  engine no longer treats them as one concept, so neither should this module.
- load_enriched.py: drop _mask_infector_symptom_for_no_infector; register
  infector_symptom_id's sentinel/label in REGISTRY_SENTINELS/
  UNSET_LABEL_OVERRIDES instead, same path already used for encounter_type_id.
- tests/test_load_enriched.py: update no-infector fixture row to uint8/255
  (was uint16/0) to match the engine's actual wire format post-IDAS-Durham#16.
- CONTEXT.md, PDR.md: document the two distinct 255 sentinels and flag that
  tests/fixtures/simulation_events_fixture.h5 still predates IDAS-Durham#16 (uint16
  infector_symptom_id, 0 for no-infector) until rebuilt via build_fixture.py
  against a current simulation_events.h5.

---------

Co-authored-by: gavdoubleu <gavin.a.woolman@durham.ac.uk>
Co-authored-by: Martha Correa-Delval <165719743+mtcorread@users.noreply.github.com>
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.

2 participants