-
Notifications
You must be signed in to change notification settings - Fork 0
feat(ADR-125): RFGS — camera-free room reconstruction via RF Gaussian Splatting#1
feat(ADR-125): RFGS — camera-free room reconstruction via RF Gaussian Splatting #1shaal wants to merge 2 commits into
Conversation
... 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.
|
Warning Review limit reached
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (14)
📝 WalkthroughWalkthroughThis 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. ChangesRF Gaussian Splatting System
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.
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 valueSpecify language for context map diagram.
The context map at Line 170 lacks a language identifier. Add
textormermaid(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 valueAll 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 valueConsider rejecting channel 15-31 explicitly.
The current logic accepts
channel == 14andchannel >= 32, but channels 15-31 fall through to theelseclause and raiseValueError. 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 valueDefault tx_node_id to first node is non-deterministic with dict ordering.
Line 143 defaults
tx_node_idtonext(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 explicittx_node_idin 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 defaultOr simply require
tx_node_idin 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 winType 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 thetuple()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
pydanticfor 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 valuen_subcarriers validation is incomplete.
The function checks
n_subcarriers <= 0but 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. Sincen_subcarriersis typed asint, 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 valueSpecify language for fenced code block.
The pseudocode block at Line 101 lacks a language identifier, flagged by markdownlint. Add
textorpseudocodeto 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
📒 Files selected for processing (12)
CHANGELOG.mddocs/adr/ADR-125-rf-gaussian-splatting-room-reconstruction.mddocs/ddd/rf-gaussian-splatting-domain-model.mdpyproject.tomlpython/wifi_densepose/rfgs/__init__.pypython/wifi_densepose/rfgs/configs/room.example.tomlpython/wifi_densepose/rfgs/dataset.pypython/wifi_densepose/rfgs/export.pypython/wifi_densepose/rfgs/geometry.pypython/wifi_densepose/rfgs/model.pypython/wifi_densepose/rfgs/render.pypython/wifi_densepose/rfgs/train.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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:
- 1: https://arxiv.org/abs/2502.01826
- 2: https://openreview.net/forum?id=E3oNDQ8e9r
- 3: https://neurips.cc/virtual/2025/poster/119166
- 4: https://arxiv.org/html/2502.01826v3
- 5: https://neurips.cc/media/neurips-2025/Slides/119166_MqEGDVE.pdf
- 6: https://neurips.cc/virtual/2025/papers.html
🏁 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 repositoryLICENSEtext before vendoring. - ADR line 149 ("release stub (~8 commits, no active dev)") is contradicted by GitHub activity showing
mainpushed/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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
🤖 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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
🤖 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.
Uh oh!
There was an error while loading. Please reload this page.
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)
docs/ddd/rf-gaussian-splatting-domain-model.md).python/wifi_densepose/rfgs/:dataset.py— CSI→RF-GS loader; reconstructs complexH[antenna, subcarrier]from ADR-018 binary I/Q frames. (The.csi.jsonlrecorder is amplitude-only → supported only as a degradedphase=Nonefallback — a key capture-format finding.)geometry.py—RoomConfig/NodePose/Pose, WiFi channel→subcarrier-frequency mapping.model.py—ComplexGaussianField(μ, 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 aCsiForwardModelseam 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,--syntheticself-test gate.export.py— backward-compatible/api/splatsv2 JSON (addsrotation+radianceto the v1 shape) + baked.rfgs.npz.configs/room.example.toml; newrfgsinstall extra; CHANGELOG entry.Design notes
Test plan
RoomConfigTOML load, length-prefixed capture round-trip.--synthetic): AC3 PASS — held-out CSI reconstruction improved 72.3% (≥50% gate) via hybrid point-cloud init.splats-v2JSON (addsrotation+radiance) and fp16 baked.rfgs.npz.Follow-ups (incoming on this branch)
/api/splatsv2 server branch + anisotropic WebGPU/Three.js viewer + Rust edge-query path.Generated by Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores
Review Change Stack