vizmo 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vizmo-0.1.0/PKG-INFO +114 -0
- vizmo-0.1.0/README.md +99 -0
- vizmo-0.1.0/pyproject.toml +23 -0
- vizmo-0.1.0/setup.cfg +4 -0
- vizmo-0.1.0/tests/test_surface_density_vs_sinkvis.py +248 -0
- vizmo-0.1.0/vizmo/__init__.py +3 -0
- vizmo-0.1.0/vizmo/app.py +66 -0
- vizmo-0.1.0/vizmo/camera.py +222 -0
- vizmo-0.1.0/vizmo/colormaps.py +23 -0
- vizmo-0.1.0/vizmo/data_manager.py +205 -0
- vizmo-0.1.0/vizmo/field_ops.py +353 -0
- vizmo-0.1.0/vizmo/gpu_compute.py +215 -0
- vizmo-0.1.0/vizmo/overlay.py +799 -0
- vizmo-0.1.0/vizmo/wgpu_app.py +1133 -0
- vizmo-0.1.0/vizmo/wgpu_overlay.py +207 -0
- vizmo-0.1.0/vizmo/wgpu_renderer.py +1716 -0
- vizmo-0.1.0/vizmo.egg-info/PKG-INFO +114 -0
- vizmo-0.1.0/vizmo.egg-info/SOURCES.txt +20 -0
- vizmo-0.1.0/vizmo.egg-info/dependency_links.txt +1 -0
- vizmo-0.1.0/vizmo.egg-info/entry_points.txt +2 -0
- vizmo-0.1.0/vizmo.egg-info/requires.txt +7 -0
- vizmo-0.1.0/vizmo.egg-info/top_level.txt +1 -0
vizmo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vizmo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Real-time 3D fly-through explorer for mesh-free/unstructured simulation data
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: h5py
|
|
10
|
+
Requires-Dist: glfw>=2.5
|
|
11
|
+
Requires-Dist: matplotlib
|
|
12
|
+
Requires-Dist: natsort
|
|
13
|
+
Requires-Dist: Pillow
|
|
14
|
+
Requires-Dist: wgpu>=0.19
|
|
15
|
+
|
|
16
|
+
# vizmo
|
|
17
|
+
|
|
18
|
+
Real-time 3D fly-through explorer for unstructured simulation data. Loads HDF5 snapshots from GIZMO/Gadget simulations and renders interactive surface density maps, mass-weighted averages, velocity dispersions, and composite CoolMap visualizations on the GPU via WebGPU.
|
|
19
|
+
|
|
20
|
+
This is a **vibecoded** app. I have no idea how its front- or backends work, and am not sure if I could really properly support or get it running for you 10 years from now! But it would not exist otherwise. Software is weird now. It is what it is.
|
|
21
|
+
|
|
22
|
+
But I hope you enjoy it and please feel free to report bugs or request features.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Requirements
|
|
31
|
+
|
|
32
|
+
- Python >= 3.9
|
|
33
|
+
- WebGPU-capable GPU (Metal on macOS, Vulkan on Linux, D3D12 on Windows)
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
Point `vizmo` at an HDF5 snapshot file:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
vizmo path/to/snapshot.hdf5
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The snapshot must contain gas particle data (`PartType0`) with at minimum `Coordinates`, `Masses`, and `KernelMaxRadius` (or `SmoothingLength`) fields. Star particles (`PartType5`) are also supported if present.
|
|
44
|
+
|
|
45
|
+
### CLI options
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
vizmo snapshot.hdf5 [--width 1920] [--height 1080] [--fov 90]
|
|
49
|
+
[--fullscreen] [--screenshot OUT.png]
|
|
50
|
+
[--profile OUT.pstats]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Rendering backend
|
|
54
|
+
|
|
55
|
+
vizmo uses a WebGPU backend ([wgpu-py](https://github.com/pygfx/wgpu-py)) with GPU-resident particle data. Compute shaders perform frustum culling, LOD selection, and per-cell summary gathering with zero CPU↔GPU per-frame transfer. On unified-memory systems (Apple Silicon) field switches are also near-zero copy.
|
|
56
|
+
|
|
57
|
+
The renderer uses progressive refinement and an auto-LOD subsample cap that adapts within a user-controlled ceiling to keep interaction smooth during motion and sharpen on idle.
|
|
58
|
+
|
|
59
|
+
## Controls
|
|
60
|
+
|
|
61
|
+
**Camera:**
|
|
62
|
+
- `W/A/S/D` — Move forward/left/back/right
|
|
63
|
+
- `Z/X` — Move up/down
|
|
64
|
+
- `Q/E` — Roll left/right
|
|
65
|
+
- Mouse (click + drag) — Look around
|
|
66
|
+
- Scroll wheel — Adjust speed
|
|
67
|
+
|
|
68
|
+
**Visualization:**
|
|
69
|
+
- `Tab` — Hide/show all UI
|
|
70
|
+
- `C` — Cycle colormap
|
|
71
|
+
- `L` — Toggle log/linear scale
|
|
72
|
+
- `R` — Auto-range color scale (composite: Color slot)
|
|
73
|
+
- `T` — Auto-range composite Lightness slot
|
|
74
|
+
- `+/-` — Contract/expand color range
|
|
75
|
+
- `[/]` — Coarser/finer LOD pixel size
|
|
76
|
+
- `,/.` — Lower/raise the auto-LOD subsample-cap ceiling
|
|
77
|
+
- `P` — Save screenshot
|
|
78
|
+
- `F1` or `\` — Toggle dev overlay
|
|
79
|
+
- `Esc` — Quit
|
|
80
|
+
|
|
81
|
+
## Render Modes
|
|
82
|
+
|
|
83
|
+
Select from the **Mode** dropdown in the user menu:
|
|
84
|
+
|
|
85
|
+
- **SurfaceDensity** — Projected surface density of a weight field. Supports combining two fields with arithmetic operators (Op / Field 2).
|
|
86
|
+
- **WeightedAverage** — Mass-weighted line-of-sight average of a data field.
|
|
87
|
+
- **WeightedVariance** — Mass-weighted line-of-sight standard deviation (e.g. velocity dispersion).
|
|
88
|
+
- **Composite** — CoolMap-style dual-field visualization. Encodes one field in lightness and another in colormap hue. Each channel has independent render mode, field selection, limits, and scaling.
|
|
89
|
+
|
|
90
|
+
### Vector Fields
|
|
91
|
+
|
|
92
|
+
3D vector fields (e.g. Velocities) are automatically detected. When selected as a weight or data field, a **Proj** dropdown appears:
|
|
93
|
+
|
|
94
|
+
- **LOS** — Per-particle line-of-sight component (dot product with the unit vector from the camera to the particle). Invariant under camera rotation; recomputed when the camera translates past a small threshold.
|
|
95
|
+
- **|v|** — Euclidean norm.
|
|
96
|
+
- **|v|^2** — Squared norm.
|
|
97
|
+
|
|
98
|
+
## Architecture
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
vizmo/
|
|
102
|
+
app.py - CLI entry point
|
|
103
|
+
wgpu_app.py - Main loop, key actions, progressive refinement, auto-LOD
|
|
104
|
+
wgpu_renderer.py - WGPURenderer: RenderMode, accumulate + resolve + composite passes
|
|
105
|
+
gpu_compute.py - GPUCompute: GPU-resident data, compute cull/LOD/gather
|
|
106
|
+
wgpu_overlay.py - WGPUDevOverlay, WGPUUserMenu (wgpu panel rendering)
|
|
107
|
+
overlay.py - Panel/PanelStyle base, DevOverlay, UserMenu
|
|
108
|
+
camera.py - 6DOF camera with cached basis vectors
|
|
109
|
+
data_manager.py - HDF5 I/O with lazy loading and cosmological corrections
|
|
110
|
+
field_ops.py - Field arithmetic and vector projections
|
|
111
|
+
colormaps.py - Matplotlib colormap to GPU texture
|
|
112
|
+
shaders/ - WGSL shaders (common, splat_subsample, resolve, composite,
|
|
113
|
+
star, text)
|
|
114
|
+
```
|
vizmo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# vizmo
|
|
2
|
+
|
|
3
|
+
Real-time 3D fly-through explorer for unstructured simulation data. Loads HDF5 snapshots from GIZMO/Gadget simulations and renders interactive surface density maps, mass-weighted averages, velocity dispersions, and composite CoolMap visualizations on the GPU via WebGPU.
|
|
4
|
+
|
|
5
|
+
This is a **vibecoded** app. I have no idea how its front- or backends work, and am not sure if I could really properly support or get it running for you 10 years from now! But it would not exist otherwise. Software is weird now. It is what it is.
|
|
6
|
+
|
|
7
|
+
But I hope you enjoy it and please feel free to report bugs or request features.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install -e .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Requirements
|
|
16
|
+
|
|
17
|
+
- Python >= 3.9
|
|
18
|
+
- WebGPU-capable GPU (Metal on macOS, Vulkan on Linux, D3D12 on Windows)
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
Point `vizmo` at an HDF5 snapshot file:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
vizmo path/to/snapshot.hdf5
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The snapshot must contain gas particle data (`PartType0`) with at minimum `Coordinates`, `Masses`, and `KernelMaxRadius` (or `SmoothingLength`) fields. Star particles (`PartType5`) are also supported if present.
|
|
29
|
+
|
|
30
|
+
### CLI options
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
vizmo snapshot.hdf5 [--width 1920] [--height 1080] [--fov 90]
|
|
34
|
+
[--fullscreen] [--screenshot OUT.png]
|
|
35
|
+
[--profile OUT.pstats]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Rendering backend
|
|
39
|
+
|
|
40
|
+
vizmo uses a WebGPU backend ([wgpu-py](https://github.com/pygfx/wgpu-py)) with GPU-resident particle data. Compute shaders perform frustum culling, LOD selection, and per-cell summary gathering with zero CPU↔GPU per-frame transfer. On unified-memory systems (Apple Silicon) field switches are also near-zero copy.
|
|
41
|
+
|
|
42
|
+
The renderer uses progressive refinement and an auto-LOD subsample cap that adapts within a user-controlled ceiling to keep interaction smooth during motion and sharpen on idle.
|
|
43
|
+
|
|
44
|
+
## Controls
|
|
45
|
+
|
|
46
|
+
**Camera:**
|
|
47
|
+
- `W/A/S/D` — Move forward/left/back/right
|
|
48
|
+
- `Z/X` — Move up/down
|
|
49
|
+
- `Q/E` — Roll left/right
|
|
50
|
+
- Mouse (click + drag) — Look around
|
|
51
|
+
- Scroll wheel — Adjust speed
|
|
52
|
+
|
|
53
|
+
**Visualization:**
|
|
54
|
+
- `Tab` — Hide/show all UI
|
|
55
|
+
- `C` — Cycle colormap
|
|
56
|
+
- `L` — Toggle log/linear scale
|
|
57
|
+
- `R` — Auto-range color scale (composite: Color slot)
|
|
58
|
+
- `T` — Auto-range composite Lightness slot
|
|
59
|
+
- `+/-` — Contract/expand color range
|
|
60
|
+
- `[/]` — Coarser/finer LOD pixel size
|
|
61
|
+
- `,/.` — Lower/raise the auto-LOD subsample-cap ceiling
|
|
62
|
+
- `P` — Save screenshot
|
|
63
|
+
- `F1` or `\` — Toggle dev overlay
|
|
64
|
+
- `Esc` — Quit
|
|
65
|
+
|
|
66
|
+
## Render Modes
|
|
67
|
+
|
|
68
|
+
Select from the **Mode** dropdown in the user menu:
|
|
69
|
+
|
|
70
|
+
- **SurfaceDensity** — Projected surface density of a weight field. Supports combining two fields with arithmetic operators (Op / Field 2).
|
|
71
|
+
- **WeightedAverage** — Mass-weighted line-of-sight average of a data field.
|
|
72
|
+
- **WeightedVariance** — Mass-weighted line-of-sight standard deviation (e.g. velocity dispersion).
|
|
73
|
+
- **Composite** — CoolMap-style dual-field visualization. Encodes one field in lightness and another in colormap hue. Each channel has independent render mode, field selection, limits, and scaling.
|
|
74
|
+
|
|
75
|
+
### Vector Fields
|
|
76
|
+
|
|
77
|
+
3D vector fields (e.g. Velocities) are automatically detected. When selected as a weight or data field, a **Proj** dropdown appears:
|
|
78
|
+
|
|
79
|
+
- **LOS** — Per-particle line-of-sight component (dot product with the unit vector from the camera to the particle). Invariant under camera rotation; recomputed when the camera translates past a small threshold.
|
|
80
|
+
- **|v|** — Euclidean norm.
|
|
81
|
+
- **|v|^2** — Squared norm.
|
|
82
|
+
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
vizmo/
|
|
87
|
+
app.py - CLI entry point
|
|
88
|
+
wgpu_app.py - Main loop, key actions, progressive refinement, auto-LOD
|
|
89
|
+
wgpu_renderer.py - WGPURenderer: RenderMode, accumulate + resolve + composite passes
|
|
90
|
+
gpu_compute.py - GPUCompute: GPU-resident data, compute cull/LOD/gather
|
|
91
|
+
wgpu_overlay.py - WGPUDevOverlay, WGPUUserMenu (wgpu panel rendering)
|
|
92
|
+
overlay.py - Panel/PanelStyle base, DevOverlay, UserMenu
|
|
93
|
+
camera.py - 6DOF camera with cached basis vectors
|
|
94
|
+
data_manager.py - HDF5 I/O with lazy loading and cosmological corrections
|
|
95
|
+
field_ops.py - Field arithmetic and vector projections
|
|
96
|
+
colormaps.py - Matplotlib colormap to GPU texture
|
|
97
|
+
shaders/ - WGSL shaders (common, splat_subsample, resolve, composite,
|
|
98
|
+
star, text)
|
|
99
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vizmo"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Real-time 3D fly-through explorer for mesh-free/unstructured simulation data"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy",
|
|
14
|
+
"h5py",
|
|
15
|
+
"glfw>=2.5",
|
|
16
|
+
"matplotlib",
|
|
17
|
+
"natsort",
|
|
18
|
+
"Pillow",
|
|
19
|
+
"wgpu>=0.19",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
vizmo = "vizmo.app:main"
|
vizmo-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Compare vizmo wgpu surface density against CrunchSnaps/SinkVis.
|
|
2
|
+
|
|
3
|
+
Generates random particles in a unit cube, computes smoothing lengths with
|
|
4
|
+
Meshoid, renders surface density with both pipelines, and checks
|
|
5
|
+
quantitative agreement (mass ratio, log-pixel correlation, median |Δlog|).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pytest
|
|
12
|
+
from meshoid import Meshoid, GridSurfaceDensity
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Reference: SinkVis-style perspective surface density
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def sinkvis_surface_density(positions, masses, hsml, center, camera_distance, res=128, fov=90):
|
|
21
|
+
"""Reproduces SinkVis.SetupCoordsAndWeights + SinkVisSigmaGas.GenerateMaps
|
|
22
|
+
without needing an HDF5 file. Camera looks down -z from
|
|
23
|
+
center + (0, 0, camera_distance), with a perspective projection onto a
|
|
24
|
+
unit-distance plane spanning [-rmax, rmax] in tan-angle, where
|
|
25
|
+
rmax = fov/90.
|
|
26
|
+
"""
|
|
27
|
+
pos = positions - center
|
|
28
|
+
pos[:, 2] -= camera_distance
|
|
29
|
+
r = np.abs(pos[:, 2])
|
|
30
|
+
m = masses.copy()
|
|
31
|
+
h = hsml.copy()
|
|
32
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
33
|
+
pos[:, :2] = pos[:, :2] / (-pos[:, 2][:, None])
|
|
34
|
+
h[:] = h / r
|
|
35
|
+
m[:] = m / r**2
|
|
36
|
+
behind = pos[:, 2] >= 0
|
|
37
|
+
h[behind] = 0
|
|
38
|
+
m[behind] = 0
|
|
39
|
+
|
|
40
|
+
rmax = fov / 90.0
|
|
41
|
+
h = np.clip(h, 2 * rmax / res, np.inf)
|
|
42
|
+
|
|
43
|
+
sigma = GridSurfaceDensity(
|
|
44
|
+
m,
|
|
45
|
+
pos,
|
|
46
|
+
h,
|
|
47
|
+
np.zeros(3),
|
|
48
|
+
2 * rmax,
|
|
49
|
+
res=res,
|
|
50
|
+
parallel=True,
|
|
51
|
+
).T
|
|
52
|
+
return sigma
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Subject under test: vizmo wgpu accumulation
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def vizmo_surface_density(
|
|
61
|
+
positions, masses, hsml, center, camera_distance, boxsize=1.0, res=128, fov=90, multigrid_levels=1
|
|
62
|
+
):
|
|
63
|
+
"""Render the surface density (denominator accumulation texture)
|
|
64
|
+
using the wgpu splat path, with the camera matched to SinkVis.
|
|
65
|
+
"""
|
|
66
|
+
import wgpu
|
|
67
|
+
from vizmo.wgpu_renderer import WGPURenderer
|
|
68
|
+
from vizmo.gpu_compute import GPUCompute
|
|
69
|
+
from vizmo.colormaps import colormap_to_texture_data
|
|
70
|
+
from vizmo.camera import Camera
|
|
71
|
+
|
|
72
|
+
adapter = wgpu.gpu.request_adapter_sync(power_preference="high-performance")
|
|
73
|
+
req_features = set()
|
|
74
|
+
if "float32-blendable" in adapter.features:
|
|
75
|
+
req_features.add("float32-blendable")
|
|
76
|
+
device = adapter.request_device_sync(required_features=req_features)
|
|
77
|
+
|
|
78
|
+
renderer = WGPURenderer(device, canvas_context=None, present_format="bgra8unorm")
|
|
79
|
+
renderer.set_colormap(colormap_to_texture_data("magma"))
|
|
80
|
+
renderer.kernel = "cubic_spline"
|
|
81
|
+
renderer.resolve_mode = 0
|
|
82
|
+
renderer.log_scale = 0
|
|
83
|
+
renderer.multigrid_levels = multigrid_levels
|
|
84
|
+
|
|
85
|
+
pos32 = positions.astype(np.float32)
|
|
86
|
+
hsml32 = hsml.astype(np.float32)
|
|
87
|
+
mass32 = masses.astype(np.float32)
|
|
88
|
+
renderer.set_particles(pos32, hsml32, mass32)
|
|
89
|
+
|
|
90
|
+
gpu_compute = GPUCompute(device)
|
|
91
|
+
gpu_compute.upload_subsample_only(pos32, hsml32, mass32, mass32)
|
|
92
|
+
renderer.set_subsample_chunks(gpu_compute.get_chunk_bufs(), world_offset=gpu_compute.get_pos_offset())
|
|
93
|
+
# Render every particle once: cap = N, so eff_stride = 1 and h_scale = 1.
|
|
94
|
+
renderer.set_subsample_max_per_frame(len(pos32) + 1)
|
|
95
|
+
|
|
96
|
+
camera = Camera(fov=fov, aspect=1.0)
|
|
97
|
+
camera.position = np.array([center[0], center[1], center[2] + camera_distance], dtype=np.float32)
|
|
98
|
+
camera._forward = np.array([0, 0, -1], dtype=np.float32)
|
|
99
|
+
camera._up = np.array([0, 1, 0], dtype=np.float32)
|
|
100
|
+
camera._dirty = True
|
|
101
|
+
extent = boxsize
|
|
102
|
+
camera.near = extent * 1e-4
|
|
103
|
+
camera.far = extent * 10
|
|
104
|
+
|
|
105
|
+
renderer._ensure_fbo(res, res, which=1)
|
|
106
|
+
renderer._render_accum(camera, res, res, renderer._accum_textures)
|
|
107
|
+
|
|
108
|
+
# Denominator texture = Σ mass·W(r)/h² = surface density.
|
|
109
|
+
# wgpu textures use a top-left origin (row 0 is the top of the
|
|
110
|
+
# framebuffer); SinkVis's GridSurfaceDensity returns a map with
|
|
111
|
+
# row 0 at the bottom. Flip vertically to match.
|
|
112
|
+
den_flat = renderer._read_accum_texture_r(renderer._accum_textures["textures"][1], size=(res, res))
|
|
113
|
+
sigma = np.flipud(den_flat.reshape(res, res))
|
|
114
|
+
|
|
115
|
+
renderer.release()
|
|
116
|
+
return sigma
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Tests
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.fixture
|
|
125
|
+
def particle_data():
|
|
126
|
+
"""Random particles in a unit cube with Meshoid smoothing lengths."""
|
|
127
|
+
rng = np.random.default_rng(42)
|
|
128
|
+
N = 10_000
|
|
129
|
+
boxsize = 1.0
|
|
130
|
+
positions = rng.uniform(0, boxsize, (N, 3)).astype(np.float64)
|
|
131
|
+
masses = np.ones(N, dtype=np.float64) / N # uniform mass, total = 1
|
|
132
|
+
hsml = Meshoid(positions, boxsize=boxsize).SmoothingLength()
|
|
133
|
+
return positions, masses, hsml, boxsize
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.parametrize("multigrid_levels", [1, 4])
|
|
137
|
+
def test_surface_density_perspective(particle_data, multigrid_levels):
|
|
138
|
+
"""The vizmo wgpu surface density should agree with SinkVis's
|
|
139
|
+
perspective projection at fov=90, camera_distance=1, to within a
|
|
140
|
+
few percent on integrated mass and ~0.1 dex per pixel."""
|
|
141
|
+
positions, masses, hsml, boxsize = particle_data
|
|
142
|
+
center = np.array([0.5, 0.5, 0.5])
|
|
143
|
+
camera_distance = 1.0
|
|
144
|
+
res = 128
|
|
145
|
+
fov = 90
|
|
146
|
+
|
|
147
|
+
sigma_sinkvis = sinkvis_surface_density(
|
|
148
|
+
positions.copy(),
|
|
149
|
+
masses.copy(),
|
|
150
|
+
hsml.copy(),
|
|
151
|
+
center,
|
|
152
|
+
camera_distance,
|
|
153
|
+
res=res,
|
|
154
|
+
fov=fov,
|
|
155
|
+
)
|
|
156
|
+
sigma_vizmo = vizmo_surface_density(
|
|
157
|
+
positions.copy(),
|
|
158
|
+
masses.copy(),
|
|
159
|
+
hsml.copy(),
|
|
160
|
+
center,
|
|
161
|
+
camera_distance,
|
|
162
|
+
boxsize=boxsize,
|
|
163
|
+
res=res,
|
|
164
|
+
fov=fov,
|
|
165
|
+
multigrid_levels=multigrid_levels,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Same units (mass per unit world area on the unit-distance plane), so
|
|
169
|
+
# both maps should integrate to the same total mass.
|
|
170
|
+
pixel_area = (2 * fov / 90.0 / res) ** 2
|
|
171
|
+
total_sinkvis = sigma_sinkvis.sum() * pixel_area
|
|
172
|
+
total_vizmo = sigma_vizmo.sum() * pixel_area
|
|
173
|
+
|
|
174
|
+
assert total_vizmo > 0, "vizmo produced an empty map"
|
|
175
|
+
assert total_sinkvis > 0, "SinkVis produced an empty map"
|
|
176
|
+
mass_ratio = total_vizmo / total_sinkvis
|
|
177
|
+
assert 0.9 < mass_ratio < 1.1, (
|
|
178
|
+
f"Total mass mismatch: vizmo={total_vizmo:.4g}, " f"sinkvis={total_sinkvis:.4g}, ratio={mass_ratio:.3f}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
mask = (sigma_sinkvis > 0) & (sigma_vizmo > 0)
|
|
182
|
+
assert mask.sum() > res * res * 0.5, "Too few overlapping pixels with signal"
|
|
183
|
+
|
|
184
|
+
log_sv = np.log10(sigma_sinkvis[mask])
|
|
185
|
+
log_df = np.log10(sigma_vizmo[mask])
|
|
186
|
+
correlation = np.corrcoef(log_sv, log_df)[0, 1]
|
|
187
|
+
log_ratio = np.abs(log_sv - log_df)
|
|
188
|
+
median_log_ratio = np.median(log_ratio)
|
|
189
|
+
|
|
190
|
+
# Always save the comparison image — it's the most useful diagnostic
|
|
191
|
+
# when the assertions below trip.
|
|
192
|
+
_save_comparison(sigma_sinkvis, sigma_vizmo, correlation, median_log_ratio, mass_ratio)
|
|
193
|
+
|
|
194
|
+
assert correlation > 0.99, f"Log surface density correlation too low: {correlation:.3f}"
|
|
195
|
+
assert median_log_ratio < 0.1, f"Median |log10(sinkvis/vizmo)| = {median_log_ratio:.3f}, " "expected < 0.1 dex"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _save_comparison(sigma_sv, sigma_df, correlation, median_log_ratio, mass_ratio):
|
|
199
|
+
"""3-panel diagnostic image: SinkVis | vizmo | log ratio."""
|
|
200
|
+
import matplotlib
|
|
201
|
+
|
|
202
|
+
matplotlib.use("Agg")
|
|
203
|
+
import matplotlib.pyplot as plt
|
|
204
|
+
from matplotlib.colors import LogNorm
|
|
205
|
+
|
|
206
|
+
pos_sv = sigma_sv[sigma_sv > 0]
|
|
207
|
+
pos_df = sigma_df[sigma_df > 0]
|
|
208
|
+
if len(pos_sv) == 0 or len(pos_df) == 0:
|
|
209
|
+
return
|
|
210
|
+
vmin = min(pos_sv.min(), pos_df.min())
|
|
211
|
+
vmax = max(pos_sv.max(), pos_df.max())
|
|
212
|
+
|
|
213
|
+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
|
214
|
+
|
|
215
|
+
im0 = axes[0].imshow(sigma_sv, norm=LogNorm(vmin=vmin, vmax=vmax), cmap="magma", origin="lower")
|
|
216
|
+
axes[0].set_title("SinkVis (GridSurfaceDensity)")
|
|
217
|
+
fig.colorbar(im0, ax=axes[0], fraction=0.046, pad=0.04)
|
|
218
|
+
|
|
219
|
+
im1 = axes[1].imshow(sigma_df, norm=LogNorm(vmin=vmin, vmax=vmax), cmap="magma", origin="lower")
|
|
220
|
+
axes[1].set_title("vizmo (wgpu splats)")
|
|
221
|
+
fig.colorbar(im1, ax=axes[1], fraction=0.046, pad=0.04)
|
|
222
|
+
|
|
223
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
224
|
+
ratio = np.where(
|
|
225
|
+
(sigma_sv > 0) & (sigma_df > 0),
|
|
226
|
+
np.log10(sigma_df / sigma_sv),
|
|
227
|
+
0.0,
|
|
228
|
+
)
|
|
229
|
+
vlim = max(abs(ratio.min()), abs(ratio.max()), 0.05)
|
|
230
|
+
im2 = axes[2].imshow(ratio, vmin=-vlim, vmax=vlim, cmap="coolwarm", origin="lower")
|
|
231
|
+
axes[2].set_title(r"$\log_{10}$(vizmo / SinkVis)")
|
|
232
|
+
fig.colorbar(im2, ax=axes[2], fraction=0.046, pad=0.04)
|
|
233
|
+
|
|
234
|
+
fig.suptitle(
|
|
235
|
+
f"corr={correlation:.4f} "
|
|
236
|
+
r"median $|\Delta\log_{10}|$" + f"={median_log_ratio:.4f} dex"
|
|
237
|
+
f" mass ratio={mass_ratio:.4f}",
|
|
238
|
+
fontsize=11,
|
|
239
|
+
)
|
|
240
|
+
for ax in axes:
|
|
241
|
+
ax.set_xticks([])
|
|
242
|
+
ax.set_yticks([])
|
|
243
|
+
|
|
244
|
+
fig.tight_layout()
|
|
245
|
+
out = os.path.join(os.path.dirname(__file__), "surface_density_comparison.png")
|
|
246
|
+
fig.savefig(out, dpi=150)
|
|
247
|
+
plt.close(fig)
|
|
248
|
+
print(f"\nComparison image saved to {out}")
|
vizmo-0.1.0/vizmo/app.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""vizmo entry point."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(description="vizmo - Real-time mesh-free data explorer")
|
|
8
|
+
parser.add_argument("snapshot", help="Path to HDF5 snapshot file")
|
|
9
|
+
parser.add_argument("--width", type=int, default=1920, help="Window width")
|
|
10
|
+
parser.add_argument("--height", type=int, default=1080, help="Window height")
|
|
11
|
+
parser.add_argument("--fov", type=float, default=90.0, help="Field of view in degrees")
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"--screenshot",
|
|
14
|
+
type=str,
|
|
15
|
+
default=None,
|
|
16
|
+
metavar="OUT",
|
|
17
|
+
help="Render one frame to OUT (PNG) after GPU init " "+ auto-range complete, then exit",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("--fullscreen", action="store_true", help="Run in fullscreen mode at specified resolution")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--profile",
|
|
22
|
+
type=str,
|
|
23
|
+
default=None,
|
|
24
|
+
metavar="OUT",
|
|
25
|
+
help="Profile the whole run with cProfile and dump " "stats to OUT (.pstats). View with snakeviz.",
|
|
26
|
+
)
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
from .wgpu_app import run_wgpu_app
|
|
30
|
+
|
|
31
|
+
if args.profile:
|
|
32
|
+
import cProfile
|
|
33
|
+
import pstats
|
|
34
|
+
|
|
35
|
+
pr = cProfile.Profile()
|
|
36
|
+
pr.enable()
|
|
37
|
+
try:
|
|
38
|
+
run_wgpu_app(
|
|
39
|
+
args.snapshot,
|
|
40
|
+
width=args.width,
|
|
41
|
+
height=args.height,
|
|
42
|
+
fov=args.fov,
|
|
43
|
+
fullscreen=args.fullscreen,
|
|
44
|
+
screenshot=args.screenshot,
|
|
45
|
+
)
|
|
46
|
+
finally:
|
|
47
|
+
pr.disable()
|
|
48
|
+
pr.dump_stats(args.profile)
|
|
49
|
+
stats = pstats.Stats(pr).sort_stats("cumulative")
|
|
50
|
+
print("\n=== top 40 by cumulative time ===")
|
|
51
|
+
stats.print_stats(40)
|
|
52
|
+
print(f"\nFull profile written to {args.profile}")
|
|
53
|
+
print(f"View with: snakeviz {args.profile}")
|
|
54
|
+
else:
|
|
55
|
+
run_wgpu_app(
|
|
56
|
+
args.snapshot,
|
|
57
|
+
width=args.width,
|
|
58
|
+
height=args.height,
|
|
59
|
+
fov=args.fov,
|
|
60
|
+
fullscreen=args.fullscreen,
|
|
61
|
+
screenshot=args.screenshot,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|