Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(ADR-125): RFGS — camera-free room reconstruction via RF Gaussian Splatting#1

Open
shaal wants to merge 2 commits into
main from
claude/ruview-rf-gaussian-splatting-ALD21
Open

feat(ADR-125): RFGS — camera-free room reconstruction via RF Gaussian Splatting #1
shaal wants to merge 2 commits into
main from
claude/ruview-rf-gaussian-splatting-ALD21

Conversation

@shaal

@shaal shaal commented May 25, 2026
edited by coderabbitai Bot
Loading

Copy link
Copy Markdown
Owner

Summary

Introduces RFGS (ADR-125) — the first true camera-free 3D room reconstruction in RuView. It optimizes complex-valued 3D Gaussians whose implied radio radiance field reproduces the CSI measured by the ESP32 mesh, replacing the existing "splat" primitives (a heat-map grid + a camera-driven point-cloud→blob converter) that are not learned radiance fields.

Foundation: GSRF (NeurIPS 2025 Spotlight, BSD-3-Clause, native complex-CSI input). RF-3DGS (Apache-2.0) is the maintained secondary reference / license-clean fallback.

What's in this PR (Phase 1 + Phase 2 reference)

  • ADR-125 (SPARC-structured decision record) + DDD model (docs/ddd/rf-gaussian-splatting-domain-model.md).
  • New pure-Python package python/wifi_densepose/rfgs/:
    • dataset.py — CSI→RF-GS loader; reconstructs complex H[antenna, subcarrier] from ADR-018 binary I/Q frames. (The .csi.jsonl recorder is amplitude-only → supported only as a degraded phase=None fallback — a key capture-format finding.)
    • geometry.pyRoomConfig/NodePose/Pose, WiFi channel→subcarrier-frequency mapping.
    • model.pyComplexGaussianField (μ, scale, quaternion, opacity, Fourier–Legendre/SH complex radiance) + random/hybrid init + 3DGS densify/prune.
    • render.py — pure-PyTorch differentiable CSI forward model (no CUDA required) behind a CsiForwardModel seam for the GSRF CUDA tracer; global-phase-invariant NCC loss (absolute CSI phase is physically unobservable at 2.4 GHz).
    • train.py — full-batch optimizer, held-out eval, --synthetic self-test gate.
    • export.py — backward-compatible /api/splats v2 JSON (adds rotation + radiance to the v1 shape) + baked .rfgs.npz.
    • configs/room.example.toml; new rfgs install extra; CHANGELOG entry.

Design notes

  • Capture path: RFGS taps the phase-preserving binary ADR-018 frames, not the lossy amplitude-only JSONL recorder.
  • Edge philosophy: training is a GPU/cloud-host step; ESP32 nodes relay CSI and never train. Only the pruned/quantized baked field ships to the edge.
  • Honest limitation: recovering Gaussian positions from absolute CSI is a hard non-convex inverse (oscillatory carrier phase) — the CPU reference reliably recovers radiance given point-cloud-seeded geometry; full position inversion is the GSRF CUDA tracer's job (P3, behind the existing seam).

Test plan

  • Torch-free verification: ADR-018 multi-antenna complex decode, bad-magic/truncation rejection, RoomConfig TOML load, length-prefixed capture round-trip.
  • Full pipeline on CPU (--synthetic): AC3 PASS — held-out CSI reconstruction improved 72.3% (≥50% gate) via hybrid point-cloud init.
  • Export produces v1-compatible splats-v2 JSON (adds rotation+radiance) and fp16 baked .rfgs.npz.
  • All new files < 500 LOC (house limit).
  • GSRF CUDA tracer backend (P3) and WebGPU viewer (P5) — follow-up commits on this branch.

Follow-ups (incoming on this branch)

  • P3: GSRF CUDA tracer backend (opt-in) + Chamfer/IoU evaluation harness + densify/prune tuning.
  • P5: /api/splats v2 server branch + anisotropic WebGPU/Three.js viewer + Rust edge-query path.

Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added RF Gaussian Splatting (RFGS) package enabling camera-free room reconstruction from WiFi channel measurements
    • Includes dataset loading, model training pipeline, and export to viewer-compatible formats
  • Documentation

    • Added architectural decision and domain model documentation for RFGS
  • Chores

    • Added RFGS optional dependency extra to project configuration

Review Change Stack

... Splatting
Adds ADR-125 (SPARC) + DDD model + first code deliverable: a pure-Python
wifi_densepose.rfgs package that optimizes complex-valued 3D Gaussians whose
implied radio radiance field reproduces CSI measured by the ESP32 mesh — the
first true camera-free 3D room reconstruction in RuView (existing splat
primitives are point-cloud blobs / heat-maps, not a learned field).
Foundation: GSRF (NeurIPS 2025 Spotlight, BSD-3-Clause, native complex CSI);
RF-3DGS (Apache-2.0) as maintained secondary reference / license-clean fallback.
- dataset.py: CSI→RF-GS loader — reconstructs complex H[ant,sub] from ADR-018
 I/Q frames (.csi.jsonl is amplitude-only → degraded phase=None fallback)
- geometry.py: RoomConfig/NodePose/Pose, WiFi channel→subcarrier frequencies
- model.py: ComplexGaussianField (μ,scale,quat,opacity,FL/SH radiance) +
 random/hybrid init + 3DGS densify/prune
- render.py: pure-PyTorch differentiable CSI forward model (no CUDA) behind a
 CsiForwardModel seam for the GSRF CUDA tracer; global-phase-invariant NCC loss
- train.py: full-batch optimizer, held-out eval, --synthetic self-test
 (AC3: held-out improvement 72.3% ≥ 50%, verified on CPU)
- export.py: backward-compatible /api/splats v2 (adds rotation+radiance) + baked
 .rfgs.npz; configs/room.example.toml; new `rfgs` install extra
Verified torch-free (decode/geometry/capture) and full pipeline on CPU.

coderabbitai Bot commented May 25, 2026
edited
Loading

Copy link
Copy Markdown

Warning

Review limit reached

@shaal, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 50 minutes and 19 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

i️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f907a56d-6fec-4457-ad42-df084d3ce746

📥 Commits

Reviewing files that changed from the base of the PR and between 3111478 and 05cda7f.

⛔ Files ignored due to path filters (1)
  • v2/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • CHANGELOG.md
  • CLAUDE.md
  • docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md
  • docs/user-guide.md
  • python/wifi_densepose/rfgs/eval.py
  • python/wifi_densepose/rfgs/model.py
  • python/wifi_densepose/rfgs/render.py
  • python/wifi_densepose/rfgs/train.py
  • ui/components/gaussian-splats.js
  • v2/Cargo.toml
  • v2/crates/wifi-densepose-pointcloud/Cargo.toml
  • v2/crates/wifi-densepose-pointcloud/src/stream.rs
  • v2/crates/wifi-densepose-rfgs/Cargo.toml
  • v2/crates/wifi-densepose-rfgs/src/lib.rs
📝 Walkthrough

Walkthrough

This PR introduces RFGS (Radio-Frequency Gaussian Splatting), a pure-Python subsystem for camera-free room reconstruction via learned complex-valued 3D Gaussian fields trained on WiFi CSI measurements. The implementation spans geometry contracts, data loading, a differentiable forward renderer, model export, and an optimization loop with adaptive density control.

Changes

RF Gaussian Splatting System

Layer / File(s) Summary
Design Documentation
docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md, docs/ddd/rf-gaussian-splatting-domain-model.md
ADR-125 specifies RFGS requirements, functional contracts, package architecture, and acceptance criteria; the domain model defines bounded contexts, aggregates, and system invariants for camera-free RF reconstruction.
Scene Geometry & Configuration
python/wifi_densepose/rfgs/geometry.py, python/wifi_densepose/rfgs/configs/room.example.toml
WiFi frequency utilities (channel_center_freq_hz, subcarrier_frequencies) and immutable dataclasses (Pose, NodePose, RoomConfig) for scene representation; TOML-backed config loading via stdlib tomllib; example 4-node mesh template.
CSI Data Loading
python/wifi_densepose/rfgs/dataset.py
Torch-free ADR-018 frame decoding (decode_adr018, iter_adr018_file) reconstructing per-antenna complex I/Q; CsiMeasurement data container; CsiMeasurementDataset aggregating captures or amplitude-only JSONL with optional phase detrending and deterministic train/holdout splitting.
Complex Gaussian Field Model
python/wifi_densepose/rfgs/model.py
ComplexGaussianField learnable parameters: means, log-scales, quaternions, opacity logits, complex radiance coefficients; quaternion-to-rotation and directional-basis helpers; initialization via random bounding-box or point-cloud; adaptive density control via prune and densify_clone with parameter reindexing.
Differentiable CSI Rendering & Loss
python/wifi_densepose/rfgs/render.py
CsiForwardModel interface and ReferenceTorchTracer implementing coherent summation of per-Gaussian CSI contributions using directional basis expansion and wavenumber-driven phase; reconstruction_loss with optional global-phase-invariant phase term and configurable amplitude/phase weighting.
Export Formats
python/wifi_densepose/rfgs/export.py
Serializes ComplexGaussianField to splats-v2 JSON (backward-compatible RGB, v2 rotation/radiance fields for viewer) and baked .rfgs.npz archives (float16 tensors for edge inference).
Training Loop & CLI
python/wifi_densepose/rfgs/train.py
End-to-end optimization: synthetic dataset generation for AC3 self-test, hybrid initialization (random or point-cloud fetch), Adam training with periodic densify/prune cycles, holdout evaluation, and CLI argument handling for capture-based or synthetic modes; AC3 gate enforces ≥50% holdout improvement.
Package Integration
python/wifi_densepose/rfgs/__init__.py, pyproject.toml, CHANGELOG.md
Lazy module routing via __getattr__ to defer torch import; optional rfgs pip extra with torch>=2.1.0 and numpy>=1.24.0; changelog entry documenting the feature and modules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

This PR introduces a substantial, multi-module feature with dense linear-algebra logic (quaternion rotations, directional basis, coherent summation, phase-invariant loss). The geometry layer is straightforward, but the rendering and training logic require careful verification of mathematical correctness and parameter flow across model/optimize/export pipelines. The adaptive density control (_reindex, _append) and phase sanitization introduce non-trivial tensor manipulation that benefits from step-by-step review.

🐰 A field of Gaussians blooms with WiFi's glow,
Complex phases dance in radio's soft flow,
No camera lens, just waves that play—
Camera-free rooms revealed in RF's way!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: introducing RFGS (RF Gaussian Splatting) for camera-free room reconstruction, which is the core feature added across all modules in this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ruview-rf-gaussian-splatting-ALD21

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (7)
docs/ddd/rf-gaussian-splatting-domain-model.md (1)

170-178: 💤 Low value

Specify language for context map diagram.

The context map at Line 170 lacks a language identifier. Add text or mermaid (if using Mermaid syntax later) to resolve the markdownlint warning.

📝 Proposed fix
-```
+```text
 [CSI Acquisition] --MeasurementBatchReady--> [Field Optimization] --uses--> [Radiance Field]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/ddd/rf-gaussian-splatting-domain-model.md` around lines 170 - 178, The
fenced code block that contains the context map diagram (the block starting with
"[CSI Acquisition] --MeasurementBatchReady--> [Field Optimization]") lacks a
language identifier; add a language tag such as "text" or "mermaid" to the
opening triple-backtick so the diagram block becomes ```text (or ```mermaid) to
satisfy markdownlint and clarify rendering for the context map.
python/wifi_densepose/rfgs/configs/room.example.toml (1)

12-34: 💤 Low value

All nodes on the same channel may limit diversity.

All four nodes operate on channel 6. While this is valid for a single-channel mesh, using different channels (where hardware supports it) could increase frequency diversity and improve reconstruction quality. Verify this is intentional for the example configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/configs/room.example.toml` around lines 12 - 34,
The example config sets channel = 6 for every [[nodes]] entry (node_id 0,1,2,3),
which reduces frequency diversity; update the node entries to use varied
channels (e.g., alternate channels or a small set supported by the hardware) or
add a clear comment explaining that same-channel operation is intentional.
Specifically, edit the channel fields for the nodes (references: node_id in each
[[nodes]] block) to different channel numbers compatible with your hardware or
add a note in the file indicating single-channel operation is deliberate.
python/wifi_densepose/rfgs/geometry.py (4)

34-41: 💤 Low value

Consider rejecting channel 15-31 explicitly.

The current logic accepts channel == 14 and channel >= 32, but channels 15-31 fall through to the else clause and raise ValueError. While correct, the error message "unsupported WiFi channel" doesn't distinguish between invalid ranges (15-31) and truly invalid values (0, negative). This is acceptable, but you could make the logic more explicit.

♻️ Optional: explicit range rejection
 elif channel == 14:
 mhz = 2484
+ elif 15 <= channel <= 31:
+ raise ValueError(f"channel {channel} is in the reserved range 15-31")
 elif channel >= 32:
 mhz = 5000 + 5 * channel
 else:
- raise ValueError(f"unsupported WiFi channel: {channel}")
+ raise ValueError(f"invalid WiFi channel: {channel} (must be 1-14 or ≥32)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/geometry.py` around lines 34 - 41, Update the
channel-to-mhz branch that assigns mhz based on channel to explicitly reject
channels 15-31: add an elif for 15 <= channel <= 31 that raises a ValueError
with a clear message like "reserved/unusable WiFi channel: {channel}", keep the
existing branches for 1-13, 14, and channel >= 32 unchanged, and preserve the
final else for other invalid values; this change should be applied in the block
where the variables channel and mhz are handled (the channel→mhz mapping).

143-143: 💤 Low value

Default tx_node_id to first node is non-deterministic with dict ordering.

Line 143 defaults tx_node_id to next(iter(nodes)). For Python 3.7+, dict insertion order is preserved, so the first node in the TOML [[nodes]] array becomes TX. However, this is implicit and may surprise users. Consider logging a warning or requiring explicit tx_node_id in the TOML.

💡 Optional: warn on implicit TX selection
+ import logging
 return RoomConfig(
 nodes=nodes,
 bounds_min=tuple(d.get("bounds_min", (-5.0, 0.0, -5.0))), # type: ignore[arg-type]
 bounds_max=tuple(d.get("bounds_max", (5.0, 3.0, 5.0))), # type: ignore[arg-type]
 bandwidth_mhz=float(d.get("bandwidth_mhz", 20.0)),
- tx_node_id=int(d.get("tx_node_id", next(iter(nodes)))),
+ tx_node_id=int(d.get("tx_node_id") or _warn_default_tx(nodes)),
 )
+
+def _warn_default_tx(nodes: dict[int, NodePose]) -> int:
+ default = next(iter(nodes))
+ logging.warning(f"tx_node_id not specified, defaulting to node {default}")
+ return default

Or simply require tx_node_id in the TOML schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/geometry.py` at line 143, The code currently
defaults tx_node_id using next(iter(nodes)) which relies on dict insertion order
and is implicit; change the logic that constructs tx_node_id (the expression
tx_node_id=int(d.get("tx_node_id", next(iter(nodes)))) ) to require or at least
warn when tx_node_id is absent: if "tx_node_id" not in d, pick a deterministic
fallback such as int(sorted(nodes)[0]) and emit a clear warning (use the
module/logger or warnings.warn) indicating that tx_node_id was implicit and
which node was chosen; otherwise use the provided d["tx_node_id"] as before.

128-137: ⚡ Quick win

Type ignore comments mask sequence-to-tuple coercion risk.

Lines 128, 130-131, 134 use # type: ignore[assignment] and # type: ignore[arg-type] to suppress mypy errors when converting lists to tuples. While the tuple() constructor handles this correctly at runtime, the type ignores hide potential mismatches if the TOML structure changes (e.g., a field becomes a scalar instead of a list). Consider validating the types explicitly.

♻️ Replace type ignores with runtime validation
 for raw in d["nodes"]:
 nid = int(raw["node_id"])
- pos = tuple(float(x) for x in raw["position"]) # type: ignore[assignment]
+ pos_raw = raw["position"]
+ if not isinstance(pos_raw, (list, tuple)) or len(pos_raw) != 3:
+ raise ValueError(f"node {nid} position must be a 3-element list/tuple")
+ pos = tuple(float(x) for x in pos_raw)
 orient = tuple(
 float(x) for x in raw.get("orientation", (1.0, 0.0, 0.0, 0.0))
 )
 nodes[nid] = NodePose(
 node_id=nid,
- pose=Pose(position=pos, orientation=orient), # type: ignore[arg-type]
+ pose=Pose(position=pos, orientation=orient), # type: ignore still needed for tuple[Any, ...] -> tuple[float, float, float]
 channel=int(raw.get("channel", 6)),
 n_antennas=int(raw.get("n_antennas", 1)),
 )

Alternatively, use a type-checking library like pydantic for TOML parsing, but that adds a dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/geometry.py` around lines 128 - 137, The code
currently silences type errors when coercing TOML sequences to tuples; replace
the type: ignore directives by validating raw values before constructing
NodePose: in the block that assigns pos, orient and nodes[nid] (referencing
variables pos, orient, nid and classes NodePose and Pose), explicitly check that
raw["position"] is a sequence of three numeric items and raw.get("orientation",
...) is a sequence of four numeric items (or default to (1.0,0.0,0.0,0.0)),
convert them to tuples of floats, and raise a clear ValueError (or
skip/continue) if the shapes/types are wrong; likewise validate channel and
n_antennas are integers (or cast safely) and then build Pose(...) and
NodePose(...) without using # type: ignore comments.

54-55: 💤 Low value

n_subcarriers validation is incomplete.

The function checks n_subcarriers <= 0 but does not validate that it is an integer. If a caller passes a float (e.g., 64.5), the spacing calculation and range will produce unexpected results. Since n_subcarriers is typed as int, runtime validation is defensive but not strictly necessary unless called from untyped contexts.

🛡️ Optional: add integer check
 if n_subcarriers <= 0:
 raise ValueError("n_subcarriers must be positive")
+ if not isinstance(n_subcarriers, int):
+ raise TypeError(f"n_subcarriers must be int, got {type(n_subcarriers).__name__}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/geometry.py` around lines 54 - 55, The current
validation only checks n_subcarriers <= 0 but misses non-integer inputs; update
the validation around n_subcarriers (the parameter used to compute spacing and
ranges) to first ensure it's an integer (e.g., isinstance(n_subcarriers, int) or
numbers.Integral) and then check > 0, and raise a TypeError for non-integers and
ValueError for non-positive values so downstream spacing/range math in this
module (geometry.py) behaves predictably.
docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md (1)

101-111: 💤 Low value

Specify language for fenced code block.

The pseudocode block at Line 101 lacks a language identifier, flagged by markdownlint. Add text or pseudocode to resolve the warning and improve rendering.

📝 Proposed fix
-```
+```text
 python/wifi_densepose/rfgs/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md` around lines
101 - 111, Update the fenced code block that contains the directory listing
starting with "python/wifi_densepose/rfgs/" to include a language identifier
(e.g., change the opening fence from ``` to ```text or ```pseudocode) so
markdownlint stops flagging it; keep the content and closing triple backticks
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md`:
- Line 9: Update ADR-125 to correct the GSRF reference by replacing or appending
the precise arXiv identifier "arXiv:2502.01826" and its full title ("GSRF:
Complex-Valued 3D Gaussian Splatting for Efficient Radio-Frequency Data
Synthesis") wherever GSRF is described (e.g., the Foundation line), verify the
stated license by reading the repository's LICENSE file and keep the
"BSD-3-Clause" only if it matches, remove or substantiate the claim in the ADR
that says "release stub (~8 commits, no active dev)" for GSRF (or replace it
with an accurate activity summary referencing recent commits/dates from the
repo), and fix the untyped markdown code fence by adding an appropriate language
tag (for example ```text) or reformatting the block in the ADR document.
In `@python/wifi_densepose/rfgs/dataset.py`:
- Around line 85-89: The header parsing uses hardcoded byte indices that don't
match ADR-018; update the channel/rssi/noise_floor/timestamp_us reads to use the
ADR-018 wire offsets (replace the literals in the struct.unpack_from calls and
the direct data index for channel with the correct offsets per ADR-018) and
centralize those offsets as named constants (e.g., ADR018_CHANNEL_OFFSET,
ADR018_RSSI_OFFSET, ADR018_NOISE_OFFSET, ADR018_TIMESTAMP_OFFSET) so the lines
that set channel, rssi, noise_floor, and timestamp_us use struct.unpack_from
with those constants instead of the current numeric indices.
- Around line 320-325: The split method currently forces at least one held-out
sample and doesn't validate the holdout fraction; update split(self, holdout:
float = 0.1) to validate that 0 <= holdout < 1 (raise ValueError otherwise),
allow holdout==0, compute k = int(n * holdout) (no max(1,...)) and ensure k is
at most n, handle n==0 by returning two empty lists if needed, and return idx[:
n - k], idx[n - k :] using the existing variables (function split, variables n,
k, idx).
- Around line 83-94: The parsing code currently reads n_antennas and
n_subcarriers from the header and then computes iq_len/unpacks without bounds;
add defensive checks before any unpacking: validate n_subcarriers > 0,
clamp/limit n_antennas and n_subcarriers to sensible protocol maxima (e.g.,
MAX_ANTENNAS, MAX_SUBCARRIERS constants) and return None when out of range, and
only compute iq_len and call struct.unpack_from when len(data) >=
CSI_HEADER_SIZE + iq_len; update all uses of n_antennas, n_subcarriers,
CSI_HEADER_SIZE, and iq_len accordingly so malformed frames cannot trigger huge
allocations or expensive unpack operations.
- Around line 283-313: The code currently picks a single rx_id before iterating
files and lines, causing every JSONL record to be assigned that same receiver;
instead, for each parsed record (rec) determine the correct receiver id (e.g.,
check rec.get("node_id") or rec.get("rx_id") and fall back to the previous
rx_ids logic if absent), validate it exists in room.nodes, then use that
per-record rx_id when computing node = room.nodes[rx_id] and freqs =
room.freqs_for_node(rx_id, n_sub) and when constructing the CsiMeasurement; if
the record lacks an rx id keep the original fallback behavior.
In `@python/wifi_densepose/rfgs/export.py`:
- Around line 73-80: The saved .npz currently writes raw field.quat which may be
non-unit; normalize quaternions before saving to match model.py's semantics. In
export.py compute a normalized_quat = field.quat / (field.quat.norm(dim=-1,
keepdim=True) + eps) (use a small eps like 1e-8 to avoid div-by-zero), move to
CPU and convert to numpy/float16, and write that normalized_quat to the "quat"
entry instead of field.quat; reference field.quat and the export block that
builds the np.savez_compressed payload.
- Around line 35-64: The code in to_splats_v2 normalizes quaternions with a raw
divide (quat = (field.quat / field.quat.norm(...)).cpu()) which can produce NaNs
if norms are zero; change this to a safe normalization: compute norms for
field.quat, clamp or replace zero/near-zero norms with a small eps (or use
torch.where to set min norm = eps) before dividing, then .cpu() the result so
write_splats_v2_json produces finite numbers for "rotation" and JSON-safe
output; update the normalization code in to_splats_v2 (and ensure any downstream
assumptions in write_splats_v2_json remain valid).
In `@python/wifi_densepose/rfgs/geometry.py`:
- Around line 149-152: RoomConfig.load currently unconditionally imports tomllib
and will fail on Python 3.9/3.10; modify the loader to support older Pythons by
adding a conditional fallback import (e.g., try to import tomllib and on
ImportError import tomli as tomllib) before calling tomllib.load, then call
RoomConfig.from_dict as before; alternatively, if you intend to require Python
>=3.11, update the package metadata instead (but do not both). Ensure the change
is applied in the function that opens the file and calls tomllib.load so
RoomConfig.from_dict continues to receive the parsed dict.
In `@python/wifi_densepose/rfgs/model.py`:
- Around line 129-136: The prune method can drop all Gaussians if every opacity
is ≤ min_opacity; update prune (and its use of opacities(), keep, and _reindex)
to ensure at least one Gaussian remains: if keep.any() is False, choose the
index of the maximum opacity (e.g., via opacities().argmax()) and set keep for
that index to True before calling _reindex; preserve the removed count semantics
(i.e., count everything except the one forced-kept Gaussian).
In `@python/wifi_densepose/rfgs/render.py`:
- Around line 66-68: The vector from Gaussian to receiver is currently computed
as to_rx = mu - rx (used to compute dirs and passed to directional_basis and the
antenna steering term), which inverts the documented g->rx direction; change the
sign to compute to_rx = rx - mu (and recompute d_rx and dirs from that) and
apply the same sign flip to any repeated computations around the antenna
steering code (references: to_rx, mu, rx, dirs, directional_basis, and the
antenna steering/phase calculation) so the directional_basis input and
per-antenna phase pattern use the correct g->rx direction.
In `@python/wifi_densepose/rfgs/train.py`:
- Around line 162-165: The code computes grad_norm then prunes before
densifying, so pruning shifts Gaussian indices and makes
densify_clone(grad_norm, ...) act on stale indices; to fix, call
field.densify_clone(grad_norm, threshold=grad_norm.mean().item() * 2) before
field.prune(min_opacity=0.005) (i.e., swap the two calls inside the
densify_every block) and then compute/inspect the boolean results
(added/removed) from those calls in the new order so densify operates on the
original index mapping.
- Around line 130-156: After calling dataset.split(holdout) save and check
train_idx before training: if train_idx is empty, avoid calling batch_loss(...,
grad=True) and loss.backward(); instead log or raise and skip the training loop.
Concretely, after train_idx, hold_idx = dataset.split(holdout) add an explicit
guard (e.g., if len(train_idx) == 0:) that either logs an error/warning and
returns/raises (so the subsequent opt.zero_grad(), batch_loss(..., grad=True),
and loss.backward() are never executed) or falls back to a safe behavior;
reference the batch_loss, train_idx, loss.backward, and opt.zero_grad symbols so
the check is inserted in the same scope.
---
Nitpick comments:
In `@docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md`:
- Around line 101-111: Update the fenced code block that contains the directory
listing starting with "python/wifi_densepose/rfgs/" to include a language
identifier (e.g., change the opening fence from ``` to ```text or ```pseudocode)
so markdownlint stops flagging it; keep the content and closing triple backticks
unchanged.
In `@docs/ddd/rf-gaussian-splatting-domain-model.md`:
- Around line 170-178: The fenced code block that contains the context map
diagram (the block starting with "[CSI Acquisition] --MeasurementBatchReady-->
[Field Optimization]") lacks a language identifier; add a language tag such as
"text" or "mermaid" to the opening triple-backtick so the diagram block becomes
```text (or ```mermaid) to satisfy markdownlint and clarify rendering for the
context map.
In `@python/wifi_densepose/rfgs/configs/room.example.toml`:
- Around line 12-34: The example config sets channel = 6 for every [[nodes]]
entry (node_id 0,1,2,3), which reduces frequency diversity; update the node
entries to use varied channels (e.g., alternate channels or a small set
supported by the hardware) or add a clear comment explaining that same-channel
operation is intentional. Specifically, edit the channel fields for the nodes
(references: node_id in each [[nodes]] block) to different channel numbers
compatible with your hardware or add a note in the file indicating
single-channel operation is deliberate.
In `@python/wifi_densepose/rfgs/geometry.py`:
- Around line 34-41: Update the channel-to-mhz branch that assigns mhz based on
channel to explicitly reject channels 15-31: add an elif for 15 <= channel <= 31
that raises a ValueError with a clear message like "reserved/unusable WiFi
channel: {channel}", keep the existing branches for 1-13, 14, and channel >= 32
unchanged, and preserve the final else for other invalid values; this change
should be applied in the block where the variables channel and mhz are handled
(the channel→mhz mapping).
- Line 143: The code currently defaults tx_node_id using next(iter(nodes)) which
relies on dict insertion order and is implicit; change the logic that constructs
tx_node_id (the expression tx_node_id=int(d.get("tx_node_id",
next(iter(nodes)))) ) to require or at least warn when tx_node_id is absent: if
"tx_node_id" not in d, pick a deterministic fallback such as
int(sorted(nodes)[0]) and emit a clear warning (use the module/logger or
warnings.warn) indicating that tx_node_id was implicit and which node was
chosen; otherwise use the provided d["tx_node_id"] as before.
- Around line 128-137: The code currently silences type errors when coercing
TOML sequences to tuples; replace the type: ignore directives by validating raw
values before constructing NodePose: in the block that assigns pos, orient and
nodes[nid] (referencing variables pos, orient, nid and classes NodePose and
Pose), explicitly check that raw["position"] is a sequence of three numeric
items and raw.get("orientation", ...) is a sequence of four numeric items (or
default to (1.0,0.0,0.0,0.0)), convert them to tuples of floats, and raise a
clear ValueError (or skip/continue) if the shapes/types are wrong; likewise
validate channel and n_antennas are integers (or cast safely) and then build
Pose(...) and NodePose(...) without using # type: ignore comments.
- Around line 54-55: The current validation only checks n_subcarriers <= 0 but
misses non-integer inputs; update the validation around n_subcarriers (the
parameter used to compute spacing and ranges) to first ensure it's an integer
(e.g., isinstance(n_subcarriers, int) or numbers.Integral) and then check > 0,
and raise a TypeError for non-integers and ValueError for non-positive values so
downstream spacing/range math in this module (geometry.py) behaves predictably.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

i️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3199d79f-24bf-431a-9a82-31cdc2f2c137

📥 Commits

Reviewing files that changed from the base of the PR and between a91004e and 3111478.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md
  • docs/ddd/rf-gaussian-splatting-domain-model.md
  • pyproject.toml
  • python/wifi_densepose/rfgs/__init__.py
  • python/wifi_densepose/rfgs/configs/room.example.toml
  • python/wifi_densepose/rfgs/dataset.py
  • python/wifi_densepose/rfgs/export.py
  • python/wifi_densepose/rfgs/geometry.py
  • python/wifi_densepose/rfgs/model.py
  • python/wifi_densepose/rfgs/render.py
  • python/wifi_densepose/rfgs/train.py

| **Date** | 2026年05月25日 |
| **Deciders** | ruv |
| **Codename** | **RFGS** — Radio-Frequency Gaussian Splatting |
| **Foundation** | [GSRF](https://github.com/nesl/GSRF) (NeurIPS 2025 Spotlight, arXiv 2502.01826) — complex-valued 3D Gaussians + Fourier–Legendre radiance basis + wavefront CUDA tracer. **License: BSD-3-Clause** (repo) — permissive, commercial-friendly. |

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify GSRF repository exists and check its license
# Check if GSRF repo is accessible and extract license info
gh repo view nesl/GSRF --json licenseInfo,name,pushedAt | jq '.'
# Also check number of commits and recent activity
gh api repos/nesl/GSRF/commits --paginate | jq '. | length'

Repository: shaal/RuView

Length of output: 266


🌐 Web query:

GSRF NeurIPS 2025 arXiv 2502.01826

💡 Result:

ArXiv 2502.01826 is titled "GSRF: Complex-Valued 3D Gaussian Splatting for Efficient Radio-Frequency Data Synthesis" and is associated with NeurIPS 2025 (spotlight and NeurIPS poster). [1][2][3] What the paper claims/contributes: - Problem: synthesizing radio-frequency (RF) data (e.g., RSSI) from transmitter/receiver positions is challenging due to complex propagation effects (reflection, diffraction, scattering). [1] - Approach: extend 3D Gaussian Splatting (3DGS) from the optical domain to the RF domain by using three main innovations: [1][4] 1) Complex-valued 3D Gaussians with a hybrid Fourier–Legendre basis to model directional and phase-dependent radiance. [1][4] 2) Orthographic splatting to efficiently identify ray–Gaussian intersections. [1][4] 3) A complex-valued ray tracing algorithm (implemented on RF-customized CUDA kernels and grounded in wavefront propagation principles) for real-time RF data synthesis. [1][4] - Targeted outcome: preserve high-fidelity RF data synthesis while improving training efficiency (shorter training time) and reducing inference latency compared to NeRF-based RF synthesis methods. [1][4] Metadata (from arXiv): - Submitted: 3 Feb 2025 (v1) - Last revised: 6 Nov 2025 (this version, v3) [1] - arXiv subject: cs.NI [1] Where to view official materials: - arXiv abstract/paper page: https://arxiv.org/abs/2502.01826 [1] - arXiv HTML (v3): https://arxiv.org/html/2502.01826v3 [4] - NeurIPS 2025 poster page: https://neurips.cc/virtual/2025/poster/119166 [3] - NeurIPS 2025 slides PDF (spotlight/poster materials): https://neurips.cc/media/neurips-2025/Slides/119166_MqEGDVE.pdf [5] - OpenReview entry for the NeurIPS 2025 spotlight: https://openreview.net/forum?id=E3oNDQ8e9r [2] NeurIPS program page reference: - NeurIPS 2025 has a general papers index at https://neurips.cc/virtual/2025/papers.html (listing all submissions). [6]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Repo identity (nesl/GSRF) =="
gh repo view nesl/GSRF --json nameWithOwner,licenseInfo,pushedAt,defaultBranchRef --jq '{nameWithOwner,licenseInfo, pushedAt, defaultBranchRef}'
echo
echo "== Recent commit timestamps (nesl/GSRF) =="
gh api repos/nesl/GSRF/commits --per_page=5 --jq 'map({sha: .sha[0:7], committedDate: .commit.author.date})'
echo
echo "== LICENSE file presence (nesl/GSRF) =="
# Use GitHub contents API; if LICENSE missing, try LICENSE.txt
( gh api repos/nesl/GSRF/contents/LICENSE --jq '{path: .path, name: .name, encoding: .encoding, sha: .sha}' ) \
 || ( gh api repos/nesl/GSRF/contents/LICENSE.txt --jq '{path: .path, name: .name, encoding: .encoding, sha: .sha}' )

Repository: shaal/RuView

Length of output: 1791


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Recent commit timestamps (nesl/GSRF, last 5) =="
gh api repos/nesl/GSRF/commits -F per_page=5 -q '.[].commit.author.date' | cat
echo
echo "== Count of commits returned by a paginated fetch (nesl/GSRF) =="
# Fetch first ~ pages but keep it bounded; compute count of all returned pages
gh api repos/nesl/GSRF/commits --paginate -F per_page=100 --jq '. | length' | python3 - <<'PY'
import sys, re
total=0
for line in sys.stdin:
 line=line.strip()
 if not line: continue
 total += int(line)
print(total)
PY
echo
echo "== LICENSE file (presence + small content check) =="
# Try LICENSE then LICENSE.txt
for p in LICENSE LICENSE.txt; do
 if gh api "repos/nesl/GSRF/contents/$p" --jq -e '.path' >/dev/null 2>&1; then
 echo "-- Found: $p"
 # Get decoded text (GitHub API returns base64 in `content`), then grep key BSD 3-Clause terms
 gh api "repos/nesl/GSRF/contents/$p" --jq '.content' | python3 - <<'PY'
import sys,base64
s=sys.stdin.read().strip()
if not s:
 sys.exit(0)
txt=base64.b64decode(s).decode('utf-8','replace')
needles=["BSD 3-Clause","Redistribution and use","THIS SOFTWARE","University","nor the names of"]
print("BSD_MATCHES:", sum(n in txt for n in needles), "/ 6")
# Print first couple lines for manual confirmation (short)
lines=txt.splitlines()
print("\n".join(lines[:6]))
PY
 exit 0
 fi
done
echo "-- No LICENSE or LICENSE.txt found via contents API"

Repository: shaal/RuView

Length of output: 256


🏁 Script executed:

#!/bin/bash
set -euo pipefail
candidates=("GSRF" "gsrf" "Gsrf" "gsRf" "GsrF")
for repo in "${candidates[@]}"; do
 echo "== gh repo view nesl/$repo =="
 gh repo view "nesl/$repo" --json nameWithOwner,licenseInfo,pushedAt,defaultBranchRef -q '.'
 echo
done
echo "== Confirm REST repo object for likely correct casing =="
# Try the original casing first
gh api "repos/nesl/GSRF" --jq '{full_name, default_branch, has_issues, license: .license, pushed_at}' || true
gh api "/repos/nesl/GSRF" --jq '{full_name, default_branch, has_issues, license: .license, pushed_at}' || true
echo
echo "== Commits endpoint test (nesl/GSRF) =="
gh api "repos/nesl/GSRF/commits" -F per_page=1 --jq '.[0].commit.author.date' || true
gh api "/repos/nesl/GSRF/commits" -F per_page=1 --jq '.[0].commit.author.date' || true
echo
echo "== LICENSE presence check (nesl/GSRF) =="
for p in LICENSE LICENSE.txt; do
 echo "-- Trying contents/$p"
 gh api "repos/nesl/GSRF/contents/$p" --jq '{path: .path, name: .name}' || true
done

Repository: shaal/RuView

Length of output: 2579


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ADR="docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md"
echo "== ADR fenced code blocks (``` lines) =="
rg -n "^```" "$ADR" || true
echo
echo "== ADR snippets around fences =="
rg -n "^```" "$ADR" | cut -d: -f1 | head -n 20 | while read -r ln; do
 start=$((ln-2)); end=$((ln+4));
 sed -n "${start},${end}p" "$ADR" | nl -ba -w2 -s": " | sed "s/^/$ln:$ /" || true
done

echo
echo "== GSRF LICENSE presence + content check (nesl/GSRF) =="
LICENSE_CONTENT_B64=$(gh api "repos/nesl/GSRF/contents/LICENSE" --jq -r '.content')
python3 - <<'PY'
import os,base64,re,sys
b64=os.environ.get("LICENSE_CONTENT_B64","")
if not b64:
 print("No LICENSE content captured")
 sys.exit(0)
txt=base64.b64decode(b64).decode('utf-8','replace')
print("Contains 'BSD 3-Clause':", "BSD 3-Clause" in txt)
print("Contains 'Redistribution and use':", "Redistribution and use" in txt)
# Print a few representative lines
lines=txt.splitlines()
for i,line in enumerate(lines[:25],1):
 print(f"{i:02d}: {line}")
PY

echo
echo "== GSRF activity proxy (branches/main) =="
gh api "repos/nesl/GSRF/branches/main" --jq '{name:.name, commit_sha:.commit.sha, commit_date:.commit.committer.date}'

echo
echo "== GSRF last 3 commits via commit SHA (avoid list endpoint) =="
MAIN_SHA=$(gh api "repos/nesl/GSRF/branches/main" --jq -r '.commit.sha')
# Get commit details for HEAD (committer date)
gh api "repos/nesl/GSRF/commits/$MAIN_SHA" --jq '{sha:.sha[0:7], commit_date:.commit.committer.date, author_date:.commit.author.date}' || true

Repository: shaal/RuView

Length of output: 402


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ADR="docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md"
echo "== ADR line count =="
wc -l "$ADR"
echo
echo "== ADR code fences (line numbers only) =="
rg -n "^```" "$ADR" || true

echo
echo "== ADR around start (lines 1-120) =="
sed -n '1,120p' "$ADR"

echo
echo "== GSRF LICENSE decode/check (nesl/GSRF) =="
b64="$(gh api "repos/nesl/GSRF/contents/LICENSE" --jq -r '.content')"
python3 - <<'PY'
import os,base64
b64=os.environ.get("B64","")
PY

Repository: shaal/RuView

Length of output: 274


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ADR="docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md"
echo "== ADR total lines =="
wc -l "$ADR"
echo
echo "== ADR code fence lines (showing exact fence markers) =="
rg -n '^\`\`\`' "$ADR" | head -n 50
echo
echo "== ADR fences with no language (``` followed by optional whitespace only) =="
rg -n '^\`\`\`[[:space:]]*$' "$ADR" || true

echo
echo "== ADR context around line ~9 =="
sed -n '1,60p' "$ADR" | nl -ba -w2 -s": "

echo
echo "== ADR context around line ~95-110 =="
sed -n '90,130p' "$ADR" | nl -ba -w2 -s": "

echo
echo "== GSRF LICENSE decode/check (nesl/GSRF) =="
b64="$(gh api repos/nesl/GSRF/contents/LICENSE --jq -r '.content' 2>/dev/null || true)"
if [ -z "${b64}" ] || [ "${b64}" = "null" ]; then
 echo "LICENSE content missing/unavailable from API."
 exit 0
fi

python3 - <<'PY'
import os,base64
b64=os.environ.get("B64","")
PY

Repository: shaal/RuView

Length of output: 295


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ADR="docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md"
echo "== ADR total lines =="
wc -l "$ADR"
echo
echo "== ADR code fence marker lines (exact) =="
rg -n '^```' "$ADR" || true
echo
echo "== ADR fences with no language (``` followed by only whitespace) =="
rg -n '^```[[:space:]]*$' "$ADR" || true

echo
echo "== ADR context around line 9 =="
sed -n '1,25p' "$ADR" | nl -ba -w2 -s": "

echo
echo "== GSRF LICENSE decode/check (nesl/GSRF) =="
b64="$(gh api repos/nesl/GSRF/contents/LICENSE --jq -r '.content' 2>/dev/null || true)"
if [ -z "$b64" ] || [ "$b64" = "null" ]; then
 echo "LICENSE content missing/unavailable from API."
 exit 0
fi

python3 - <<PY
import os,base64
b64=os.environ.get("B64","")
txt=base64.b64decode(b64).decode("utf-8","replace")
print("Contains 'BSD 3-Clause':", "BSD 3-Clause" in txt)
print("Contains 'Redistribution and use':", "Redistribution and use" in txt)
print("First 20 lines:")
for i,line in enumerate(txt.splitlines()[:20],1):
 print(f"{i:02d}: {line}")
PY

Repository: shaal/RuView

Length of output: 305


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ADR="docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md"
echo "== ADR lines 70-130 =="
nl -ba "$ADR" | sed -n '70,130p'
echo
echo "== All triple-backtick fence markers in ADR =="
rg -n '```' "$ADR"
echo
echo "== GSRF branch/main commit date (nesl/GSRF) =="
gh api repos/nesl/GSRF/branches/main --jq '{name:.name, commit_sha:.commit.sha, committed_date:.commit.committer.date}' || true
echo
echo "== GSRF LICENSE decode/check (nesl/GSRF) =="
b64="$(gh api repos/nesl/GSRF/contents/LICENSE --jq -r '.content' 2>/dev/null || true)"
python3 -c "import os,base64; b=os.environ.get('B64',''); assert b; t=base64.b64decode(b).decode('utf-8','replace'); 
print('Has BSD 3-Clause:', 'BSD 3-Clause' in t); 
print('Has Redistribution and use:', 'Redistribution and use' in t);
print('First 12 lines:');
print('\\n'.join(t.splitlines()[:12]));" 2>/dev/null

Repository: shaal/RuView

Length of output: 140


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ADR="docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md"
echo "== ADR fences (all) =="
rg -n '```' "$ADR" || true
echo
echo "== ADR lines 70-130 (with original line numbers) =="
awk 'NR>=70 && NR<=130 {printf "%4d: %s\n", NR, 0ドル}' "$ADR"
echo
echo "== GSRF branches/main metadata (nesl/GSRF) =="
gh api repos/nesl/GSRF/branches/main --jq '{name:.name, commit_sha:.commit.sha, committer_date:.commit.committer.date, author_date:.commit.commit.author.date}' || true
echo
echo "== GSRF LICENSE decode/check (nesl/GSRF) =="
b64="$(gh api repos/nesl/GSRF/contents/LICENSE --jq -r '.content' 2>/dev/null || true)"
if [ -z "${b64}" ] || [ "${b64}" = "null" ]; then
 echo "LICENSE content missing/unavailable from API."
 exit 0
fi
export B64="$b64"
python3 - <<'PY'
import os,base64
b64=os.environ["B64"]
txt=base64.b64decode(b64).decode("utf-8","replace")
print("Contains 'BSD 3-Clause':", "BSD 3-Clause" in txt)
print("Contains 'Redistribution and use':", "Redistribution and use" in txt)
print("Contains 'THIS SOFTWARE IS PROVIDED':", "THIS SOFTWARE IS PROVIDED" in txt)
print("First 12 lines:")
for i,line in enumerate(txt.splitlines()[:12],1):
 print(f"{i:02d}: {line}")
PY

Repository: shaal/RuView

Length of output: 4582


🏁 Script executed:

ADR="docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md"
awk 'NR>=140 && NR<=160 {printf "%4d: %s\n", NR, 0ドル}' "$ADR"

Repository: shaal/RuView

Length of output: 1418


Fix ADR-125 GSRF reference accuracy + markdown lint

  • arXiv:2502.01826 ("GSRF: Complex-Valued 3D Gaussian Splatting for Efficient Radio-Frequency Data Synthesis") exists and is associated with NeurIPS 2025.
  • The ADR’s "BSD-3-Clause" licensing claim matches GitHub’s reported license for nesl/GSRF; still validate against the repository LICENSE text before vendoring.
  • ADR line 149 ("release stub (~8 commits, no active dev)") is contradicted by GitHub activity showing main pushed/committed on 2026年04月16日; remove/substantiate the "no active dev" and "~8 commits" assertions.
  • Markdown lint: code fence at line 101 is untyped (just ); add a language tag (e.g., text) or adjust formatting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.md` at line 9,
Update ADR-125 to correct the GSRF reference by replacing or appending the
precise arXiv identifier "arXiv:2502.01826" and its full title ("GSRF:
Complex-Valued 3D Gaussian Splatting for Efficient Radio-Frequency Data
Synthesis") wherever GSRF is described (e.g., the Foundation line), verify the
stated license by reading the repository's LICENSE file and keep the
"BSD-3-Clause" only if it matches, remove or substantiate the claim in the ADR
that says "release stub (~8 commits, no active dev)" for GSRF (or replace it
with an accurate activity summary referencing recent commits/dates from the
repo), and fix the untyped markdown code fence by adding an appropriate language
tag (for example ```text) or reformatting the block in the ADR document.

Comment on lines +83 to +94
n_antennas = max(1, data[5])
(n_subcarriers,) = struct.unpack_from("<H", data, 6)
channel = data[8]
rssi = struct.unpack_from("<b", data, 9)[0]
noise_floor = struct.unpack_from("<b", data, 10)[0]
(timestamp_us,) = struct.unpack_from("<I", data, 16)

iq_len = n_subcarriers * 2 * n_antennas
if len(data) < CSI_HEADER_SIZE + iq_len:
return None
iq = struct.unpack_from(f"<{iq_len}b", data, CSI_HEADER_SIZE)

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add hard bounds checks for antenna/subcarrier counts before unpacking.

Line 83/84 accept unbounded values from the frame header. A malformed or hostile capture can drive very large iq_len and expensive unpack/iteration. Add protocol limits (e.g., max antennas/subcarriers) and reject zero-subcarrier frames early.

Suggested patch
- n_antennas = max(1, data[5])
- (n_subcarriers,) = struct.unpack_from("<H", data, 6)
+ n_antennas = int(data[5])
+ (n_subcarriers,) = struct.unpack_from("<H", data, 6)
+ if n_antennas < 1 or n_antennas > 4:
+ return None
+ if n_subcarriers < 1 or n_subcarriers > 256:
+ return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/dataset.py` around lines 83 - 94, The parsing code
currently reads n_antennas and n_subcarriers from the header and then computes
iq_len/unpacks without bounds; add defensive checks before any unpacking:
validate n_subcarriers > 0, clamp/limit n_antennas and n_subcarriers to sensible
protocol maxima (e.g., MAX_ANTENNAS, MAX_SUBCARRIERS constants) and return None
when out of range, and only compute iq_len and call struct.unpack_from when
len(data) >= CSI_HEADER_SIZE + iq_len; update all uses of n_antennas,
n_subcarriers, CSI_HEADER_SIZE, and iq_len accordingly so malformed frames
cannot trigger huge allocations or expensive unpack operations.

Comment on lines +85 to +89
channel = data[8]
rssi = struct.unpack_from("<b", data, 9)[0]
noise_floor = struct.unpack_from("<b", data, 10)[0]
(timestamp_us,) = struct.unpack_from("<I", data, 16)

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix ADR-018 header offsets to match the wire contract.

Line 85–89 currently parse channel/rssi/noise_floor/timestamp_us from offsets that don’t match ADR-018. This mislabels frame metadata (especially RSSI/noise/timestamp), and downstream training metadata becomes unreliable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/dataset.py` around lines 85 - 89, The header
parsing uses hardcoded byte indices that don't match ADR-018; update the
channel/rssi/noise_floor/timestamp_us reads to use the ADR-018 wire offsets
(replace the literals in the struct.unpack_from calls and the direct data index
for channel with the correct offsets per ADR-018) and centralize those offsets
as named constants (e.g., ADR018_CHANNEL_OFFSET, ADR018_RSSI_OFFSET,
ADR018_NOISE_OFFSET, ADR018_TIMESTAMP_OFFSET) so the lines that set channel,
rssi, noise_floor, and timestamp_us use struct.unpack_from with those constants
instead of the current numeric indices.

Comment on lines +283 to +313
rx_ids = room.rx_node_ids()
rx_id = rx_ids[0] if rx_ids else room.tx_node_id
node = room.nodes[rx_id]
out: list[CsiMeasurement] = []
for fp in files:
for line in fp.read_text().splitlines():
if not line.strip():
continue
rec = json.loads(line)
amp = rec.get("subcarriers") or []
if not amp:
continue
n_sub = len(amp)
# Amplitude-only -> zero-phase complex; flagged has_phase=False.
h = torch.tensor(amp, dtype=torch.float32, device=device)
h = torch.complex(h, torch.zeros_like(h)).unsqueeze(0) # [1, sub]
freqs = torch.tensor(
room.freqs_for_node(rx_id, n_sub),
dtype=torch.float32,
device=device,
)
out.append(
CsiMeasurement(
h=h,
tx_position=tx_pos,
rx_position=node.pose.position,
rx_orientation=node.pose.orientation,
freqs_hz=freqs,
timestamp_s=float(rec.get("timestamp", 0.0)),
node_id=rx_id,
has_phase=False,

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t pin all JSONL samples to a single RX node.

Line 283–285 choose one rx_id and reuse it for every JSONL row. In multi-node recordings this silently assigns wrong geometry/frequency metadata to many samples.

Suggested patch
- rx_ids = room.rx_node_ids()
- rx_id = rx_ids[0] if rx_ids else room.tx_node_id
- node = room.nodes[rx_id]
 out: list[CsiMeasurement] = []
 for fp in files:
 for line in fp.read_text().splitlines():
@@
 rec = json.loads(line)
+ rx_id = int(rec.get("node_id", room.tx_node_id))
+ if rx_id == room.tx_node_id:
+ continue
+ node = room.nodes.get(rx_id)
+ if node is None:
+ log.warning("jsonl row from unknown node_id=%s; skipping", rx_id)
+ continue
 amp = rec.get("subcarriers") or []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/dataset.py` around lines 283 - 313, The code
currently picks a single rx_id before iterating files and lines, causing every
JSONL record to be assigned that same receiver; instead, for each parsed record
(rec) determine the correct receiver id (e.g., check rec.get("node_id") or
rec.get("rx_id") and fall back to the previous rx_ids logic if absent), validate
it exists in room.nodes, then use that per-record rx_id when computing node =
room.nodes[rx_id] and freqs = room.freqs_for_node(rx_id, n_sub) and when
constructing the CsiMeasurement; if the record lacks an rx id keep the original
fallback behavior.

Comment on lines +320 to +325
def split(self, holdout: float = 0.1) -> tuple[list[int], list[int]]:
"""Deterministic train/held-out index split (last ``holdout`` fraction)."""
n = len(self.measurements)
k = max(1, int(n * holdout))
idx = list(range(n))
return idx[: n - k], idx[n - k :]

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate holdout range and allow true zero-holdout splits.

Line 323 forces at least one holdout sample even when holdout=0, and negative/>=1 values are not rejected. This creates surprising splits.

Suggested patch
 def split(self, holdout: float = 0.1) -> tuple[list[int], list[int]]:
 """Deterministic train/held-out index split (last ``holdout`` fraction)."""
 n = len(self.measurements)
- k = max(1, int(n * holdout))
+ if not (0.0 <= holdout < 1.0):
+ raise ValueError("holdout must be in [0.0, 1.0)")
+ k = int(n * holdout)
 idx = list(range(n))
 return idx[: n - k], idx[n - k :]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/dataset.py` around lines 320 - 325, The split
method currently forces at least one held-out sample and doesn't validate the
holdout fraction; update split(self, holdout: float = 0.1) to validate that 0 <=
holdout < 1 (raise ValueError otherwise), allow holdout==0, compute k = int(n *
holdout) (no max(1,...)) and ensure k is at most n, handle n==0 by returning two
empty lists if needed, and return idx[: n - k], idx[n - k :] using the existing
variables (function split, variables n, k, idx).

Comment on lines +129 to +136
def prune(self, min_opacity: float = 0.005) -> int:
"""Drop near-transparent Gaussians; returns the number removed."""
keep = self.opacities() > min_opacity
if keep.all():
return 0
removed = int((~keep).sum().item())
self._reindex(keep.nonzero(as_tuple=True)[0])
return removed

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep at least one Gaussian when pruning.

Line 135 can reindex the field down to zero Gaussians when every opacity falls below min_opacity. That leaves the model unusable and also breaks export later because python/wifi_densepose/rfgs/export.py computes rad.max() in _amplitude_rgb().

💡 Suggested guard
 `@torch.no_grad`()
 def prune(self, min_opacity: float = 0.005) -> int:
 """Drop near-transparent Gaussians; returns the number removed."""
+ if self.num_gaussians == 0:
+ return 0
 keep = self.opacities() > min_opacity
+ if not keep.any():
+ keep[self.opacity_logit.argmax()] = True
 if keep.all():
 return 0
 removed = int((~keep).sum().item())
 self._reindex(keep.nonzero(as_tuple=True)[0])
 return removed
📝 Committable suggestion

!!️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def prune(self, min_opacity: float = 0.005) -> int:
"""Drop near-transparent Gaussians; returns the number removed."""
keep = self.opacities() > min_opacity
if keep.all():
return 0
removed = int((~keep).sum().item())
self._reindex(keep.nonzero(as_tuple=True)[0])
return removed
def prune(self, min_opacity: float = 0.005) -> int:
"""Drop near-transparent Gaussians; returns the number removed."""
if self.num_gaussians == 0:
return 0
keep = self.opacities() > min_opacity
if not keep.any():
keep[self.opacity_logit.argmax()] = True
if keep.all():
return 0
removed = int((~keep).sum().item())
self._reindex(keep.nonzero(as_tuple=True)[0])
return removed
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/model.py` around lines 129 - 136, The prune method
can drop all Gaussians if every opacity is ≤ min_opacity; update prune (and its
use of opacities(), keep, and _reindex) to ensure at least one Gaussian remains:
if keep.any() is False, choose the index of the maximum opacity (e.g., via
opacities().argmax()) and set keep for that index to True before calling
_reindex; preserve the removed count semantics (i.e., count everything except
the one forced-kept Gaussian).

Comment on lines +66 to +68
to_rx = mu - rx
d_rx = to_rx.norm(dim=-1).clamp_min(1e-3) # [N]
dirs = to_rx / d_rx.unsqueeze(-1) # [N,3] g->rx direction

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Flip the Gaussian→RX direction vector.

Line 66 currently builds to_rx as mu - rx, which is the opposite of the documented g->rx direction. That sign propagates into directional_basis(...) and the antenna steering term on Line 93, so the renderer mirrors both the signed radiance lobes and the per-antenna phase pattern.

Proposed fix
- to_rx = mu - rx
+ to_rx = rx - mu
 d_rx = to_rx.norm(dim=-1).clamp_min(1e-3) # [N]
 dirs = to_rx / d_rx.unsqueeze(-1) # [N,3] g->rx direction

Also applies to: 89-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/render.py` around lines 66 - 68, The vector from
Gaussian to receiver is currently computed as to_rx = mu - rx (used to compute
dirs and passed to directional_basis and the antenna steering term), which
inverts the documented g->rx direction; change the sign to compute to_rx = rx -
mu (and recompute d_rx and dirs from that) and apply the same sign flip to any
repeated computations around the antenna steering code (references: to_rx, mu,
rx, dirs, directional_basis, and the antenna steering/phase calculation) so the
directional_basis input and per-antenna phase pattern use the correct g->rx
direction.

Comment on lines +74 to +77
opacity = field.opacities() # [N]
# Anisotropic extent response: larger Gaussians radiate more broadly.
extent = field.scales().prod(dim=-1).clamp_min(1e-6) # [N]
amp = opacity * extent * rad.abs() / (d_tx * d_rx) # [N] real magnitude

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the extent term actually depend on anisotropy/rotation.

Line 76 collapses each Gaussian to prod(scale), so any two ellipsoids with the same volume render identically. In practice that means field.quat and the axis-specific shape in field.log_scale never influence CSI, so those parameters receive no meaningful training signal even though python/wifi_densepose/rfgs/model.py defines full covariance support. The extent/visibility term needs to depend on direction and covariance; otherwise exported rotations are effectively arbitrary.

Comment on lines +130 to +156
train_idx, hold_idx = dataset.split(holdout)
opt = torch.optim.Adam(field.parameters(), lr=lr)

def batch_loss(indices: list[int], grad: bool):
ctx = torch.enable_grad() if grad else torch.no_grad()
with ctx:
total = torch.zeros((), device=device)
for i in indices:
m = dataset[i]
h = tracer.render(field, torch.tensor(m.tx_position),
torch.tensor(m.rx_position), m.freqs_hz,
m.h.shape[0])
total = total + reconstruction_loss(
h, m.h, use_phase=m.has_phase)["total"]
return total / max(1, len(indices))

base_train = float(batch_loss(train_idx, grad=False).detach())
base_hold = float(batch_loss(hold_idx, grad=False).detach())
history = {"baseline_train": base_train, "baseline_holdout": base_hold,
"steps": []}
log.info("baseline (untrained field): train=%.5f held-out=%.5f",
base_train, base_hold)

for step in range(steps):
opt.zero_grad()
loss = batch_loss(train_idx, grad=True) # full-batch GD (stable convergence)
loss.backward()

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against empty training splits before backprop.

If train_idx is empty (e.g., very small datasets), batch_loss(..., grad=True) returns a constant tensor and loss.backward() fails at runtime. Add an explicit check right after split.

🔧 Proposed fix
 tracer = ReferenceTorchTracer()
 train_idx, hold_idx = dataset.split(holdout)
+ if not train_idx:
+ raise ValueError(
+ "training split is empty; reduce holdout or provide more measurements"
+ )
 opt = torch.optim.Adam(field.parameters(), lr=lr)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/train.py` around lines 130 - 156, After calling
dataset.split(holdout) save and check train_idx before training: if train_idx is
empty, avoid calling batch_loss(..., grad=True) and loss.backward(); instead log
or raise and skip the training loop. Concretely, after train_idx, hold_idx =
dataset.split(holdout) add an explicit guard (e.g., if len(train_idx) == 0:)
that either logs an error/warning and returns/raises (so the subsequent
opt.zero_grad(), batch_loss(..., grad=True), and loss.backward() are never
executed) or falls back to a safe behavior; reference the batch_loss, train_idx,
loss.backward, and opt.zero_grad symbols so the check is inserted in the same
scope.

Comment on lines +162 to +165
if densify_every and step > 0 and step % densify_every == 0:
removed = field.prune(min_opacity=0.005)
added = field.densify_clone(grad_norm, threshold=grad_norm.mean().item() * 2)
if removed or added:

@coderabbitai coderabbitai Bot May 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Densify/prune order uses stale index mapping for gradients.

grad_norm is computed before structural edits. Pruning first changes Gaussian indices, so densify_clone(grad_norm, ...) may target shifted/invalid indices. Clone first, then prune.

🔧 Proposed fix
 if densify_every and step > 0 and step % densify_every == 0:
- removed = field.prune(min_opacity=0.005)
- added = field.densify_clone(grad_norm, threshold=grad_norm.mean().item() * 2)
+ added = field.densify_clone(
+ grad_norm, threshold=grad_norm.mean().item() * 2
+ )
+ removed = field.prune(min_opacity=0.005)
 if removed or added:
 opt = torch.optim.Adam(field.parameters(), lr=lr) # params changed
 log.info("step %d densify: +%d/-%d -> %d Gaussians",
 step, added, removed, field.num_gaussians)
📝 Committable suggestion

!!️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if densify_every and step > 0 and step % densify_every == 0:
removed = field.prune(min_opacity=0.005)
added = field.densify_clone(grad_norm, threshold=grad_norm.mean().item() * 2)
if removed or added:
if densify_every and step > 0 and step % densify_every == 0:
added = field.densify_clone(
grad_norm, threshold=grad_norm.mean().item() * 2
)
removed = field.prune(min_opacity=0.005)
if removed or added:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/wifi_densepose/rfgs/train.py` around lines 162 - 165, The code
computes grad_norm then prunes before densifying, so pruning shifts Gaussian
indices and makes densify_clone(grad_norm, ...) act on stale indices; to fix,
call field.densify_clone(grad_norm, threshold=grad_norm.mean().item() * 2)
before field.prune(min_opacity=0.005) (i.e., swap the two calls inside the
densify_every block) and then compute/inspect the boolean results
(added/removed) from those calls in the new order so densify operates on the
original index mapping.

... server + viewer)
P3 — Field optimization:
- render.py: GsrfCudaTracer backend behind make_forward_model(backend) factory
 (opt-in GSRF CUDA kernel, actionable error + pure-PyTorch fallback)
- model.py: densify_split (order-independent with clone/prune via prefix-safe masks)
- eval.py: geometry harness — Chamfer distance + occupancy IoU vs reference scan (AC5)
- train.py: --backend and --eval-cloud wired; eval reported into history
P5 — Field serving:
- new crate wifi-densepose-rfgs: immutable RfgsField loads baked splats-v2 JSON,
 query_nearest edge query, schema/count validation (4 unit tests green)
- pointcloud /api/splats?schema=rfgs-v2 serves a baked field from RUVIEW_RFGS_FIELD
 (backward compatible — v1 shape unchanged without the param)
- gaussian-splats.js: loadRfgsField()/fetchAndRenderRfgs() render anisotropic
 learned Gaussians as oriented instanced ellipsoids (quaternion + scale)
Verified: rfgs crate `cargo test` 4/4; pointcloud `cargo test` 15/15 + `cargo check`;
synthetic self-test AC3 71.5% held-out improvement; eval metrics unit-checked;
Python-export <-> Rust-struct schema parity confirmed. Docs: ADR-125 ACs/phases,
CHANGELOG, user-guide (RFGS workflow + v2 endpoint), CLAUDE.md crate table.
Browser render path and the GSRF CUDA kernel are not automated-tested here
(no display / no CUDA build); both sit behind verified seams.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@coderabbitai coderabbitai[bot] coderabbitai[bot] left review comments

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

2 participants

AltStyle によって変換されたページ (->オリジナル) /