raystrack 1.0.2__tar.gz → 2.0.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.
Files changed (86) hide show
  1. raystrack-2.0.0/PKG-INFO +283 -0
  2. raystrack-2.0.0/README.md +261 -0
  3. {raystrack-1.0.2 → raystrack-2.0.0}/pyproject.toml +8 -4
  4. raystrack-2.0.0/src/raystrack/__init__.py +11 -0
  5. raystrack-2.0.0/src/raystrack/backends/__init__.py +8 -0
  6. raystrack-2.0.0/src/raystrack/backends/area_pair.py +142 -0
  7. raystrack-2.0.0/src/raystrack/backends/calibration.py +231 -0
  8. raystrack-2.0.0/src/raystrack/backends/cuda_fused.py +251 -0
  9. raystrack-2.0.0/src/raystrack/backends/devices.py +61 -0
  10. raystrack-2.0.0/src/raystrack/backends/launch.py +22 -0
  11. raystrack-2.0.0/src/raystrack/backends/workspaces.py +185 -0
  12. raystrack-2.0.0/src/raystrack/engine/__init__.py +1 -0
  13. raystrack-2.0.0/src/raystrack/engine/controls.py +23 -0
  14. raystrack-2.0.0/src/raystrack/engine/postprocessing.py +52 -0
  15. raystrack-2.0.0/src/raystrack/engine/sampling.py +77 -0
  16. raystrack-2.0.0/src/raystrack/engine/scheduler.py +408 -0
  17. raystrack-2.0.0/src/raystrack/integrations/__init__.py +4 -0
  18. raystrack-2.0.0/src/raystrack/integrations/grasshopper/__init__.py +7 -0
  19. raystrack-2.0.0/src/raystrack/integrations/grasshopper/serialization.py +301 -0
  20. raystrack-2.0.0/src/raystrack/integrations/grasshopper/worker.py +447 -0
  21. raystrack-2.0.0/src/raystrack/integrations/rhino.py +48 -0
  22. raystrack-2.0.0/src/raystrack/io/__init__.py +245 -0
  23. raystrack-2.0.0/src/raystrack/io/_arrays.py +160 -0
  24. raystrack-2.0.0/src/raystrack/io/_v1.py +261 -0
  25. raystrack-2.0.0/src/raystrack/model/__init__.py +5 -0
  26. raystrack-2.0.0/src/raystrack/model/geometry.py +155 -0
  27. raystrack-2.0.0/src/raystrack/model/scene.py +209 -0
  28. raystrack-2.0.0/src/raystrack/solver/__init__.py +7 -0
  29. raystrack-2.0.0/src/raystrack/solver/options.py +119 -0
  30. raystrack-2.0.0/src/raystrack/solver/query.py +82 -0
  31. raystrack-2.0.0/src/raystrack/solver/result.py +204 -0
  32. raystrack-2.0.0/src/raystrack/solver/runtime.py +346 -0
  33. raystrack-2.0.0/src/raystrack/solver/snapshot.py +79 -0
  34. raystrack-2.0.0/src/raystrack/utils/__init__.py +1 -0
  35. raystrack-2.0.0/src/raystrack/utils/bvh.py +170 -0
  36. {raystrack-1.0.2 → raystrack-2.0.0}/src/raystrack/utils/cpu_trace.py +106 -0
  37. {raystrack-1.0.2 → raystrack-2.0.0}/src/raystrack/utils/cuda_trace.py +102 -6
  38. raystrack-2.0.0/src/raystrack/utils/helpers.py +35 -0
  39. raystrack-2.0.0/src/raystrack/utils/instancing.py +258 -0
  40. raystrack-2.0.0/src/raystrack/utils/prepared.py +928 -0
  41. {raystrack-1.0.2 → raystrack-2.0.0}/src/raystrack/utils/ray_builder.py +8 -6
  42. raystrack-2.0.0/src/raystrack/utils/taichi_trace.py +663 -0
  43. raystrack-2.0.0/src/raystrack.egg-info/PKG-INFO +283 -0
  44. raystrack-2.0.0/src/raystrack.egg-info/SOURCES.txt +69 -0
  45. raystrack-2.0.0/src/raystrack.egg-info/requires.txt +8 -0
  46. raystrack-2.0.0/tests/test_adaptive_execution.py +156 -0
  47. raystrack-2.0.0/tests/test_cuda_fused_v2.py +164 -0
  48. raystrack-2.0.0/tests/test_dynamic.py +270 -0
  49. raystrack-2.0.0/tests/test_execution.py +132 -0
  50. raystrack-2.0.0/tests/test_gpu_sim.py +90 -0
  51. raystrack-2.0.0/tests/test_grasshopper_contract.py +110 -0
  52. raystrack-2.0.0/tests/test_grasshopper_packaging.py +133 -0
  53. raystrack-2.0.0/tests/test_grasshopper_worker.py +537 -0
  54. raystrack-2.0.0/tests/test_instancing.py +296 -0
  55. raystrack-2.0.0/tests/test_io_v2.py +261 -0
  56. raystrack-2.0.0/tests/test_model_v2.py +272 -0
  57. raystrack-2.0.0/tests/test_preview.py +191 -0
  58. raystrack-2.0.0/tests/test_public_repr.py +74 -0
  59. raystrack-2.0.0/tests/test_refinement.py +288 -0
  60. raystrack-2.0.0/tests/test_self_viewing.py +110 -0
  61. raystrack-2.0.0/tests/test_solver_v2.py +530 -0
  62. raystrack-2.0.0/tests/test_store.py +219 -0
  63. raystrack-2.0.0/tests/test_taichi.py +402 -0
  64. raystrack-2.0.0/tests/test_tuning.py +161 -0
  65. raystrack-2.0.0/tests/test_visibility.py +135 -0
  66. raystrack-1.0.2/PKG-INFO +0 -125
  67. raystrack-1.0.2/README.md +0 -107
  68. raystrack-1.0.2/src/raystrack/__init__.py +0 -30
  69. raystrack-1.0.2/src/raystrack/api.py +0 -198
  70. raystrack-1.0.2/src/raystrack/io.py +0 -238
  71. raystrack-1.0.2/src/raystrack/main.py +0 -2194
  72. raystrack-1.0.2/src/raystrack/params.py +0 -129
  73. raystrack-1.0.2/src/raystrack/utils/__init__.py +0 -23
  74. raystrack-1.0.2/src/raystrack/utils/bvh.py +0 -74
  75. raystrack-1.0.2/src/raystrack/utils/geometry.py +0 -66
  76. raystrack-1.0.2/src/raystrack/utils/helpers.py +0 -284
  77. raystrack-1.0.2/src/raystrack/utils/prepared.py +0 -442
  78. raystrack-1.0.2/src/raystrack.egg-info/PKG-INFO +0 -125
  79. raystrack-1.0.2/src/raystrack.egg-info/SOURCES.txt +0 -23
  80. raystrack-1.0.2/src/raystrack.egg-info/requires.txt +0 -2
  81. {raystrack-1.0.2 → raystrack-2.0.0}/LICENSE +0 -0
  82. {raystrack-1.0.2 → raystrack-2.0.0}/setup.cfg +0 -0
  83. {raystrack-1.0.2 → raystrack-2.0.0}/setup.py +0 -0
  84. {raystrack-1.0.2 → raystrack-2.0.0}/src/raystrack/utils/halton.py +0 -0
  85. {raystrack-1.0.2 → raystrack-2.0.0}/src/raystrack.egg-info/dependency_links.txt +0 -0
  86. {raystrack-1.0.2 → raystrack-2.0.0}/src/raystrack.egg-info/top_level.txt +0 -0
@@ -0,0 +1,283 @@
1
+ Metadata-Version: 2.4
2
+ Name: raystrack
3
+ Version: 2.0.0
4
+ Summary: Reusable scene and solver API for Monte Carlo view factors on CPU, CUDA and portable GPUs
5
+ Author-email: Philip Balizki <philip@metis.earth>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/philip-ba/raystrack
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Requires-Python: <3.14,>=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy<3.0,>=1.24
16
+ Requires-Dist: numba<0.65,>=0.60
17
+ Provides-Extra: portable-gpu
18
+ Requires-Dist: taichi<1.8,>=1.7.4; extra == "portable-gpu"
19
+ Provides-Extra: cuda
20
+ Requires-Dist: numba-cuda>=0.20; extra == "cuda"
21
+ Dynamic: license-file
22
+
23
+ # Raystrack
24
+
25
+ <img src="https://raw.githubusercontent.com/philip-ba/raystrack/main/raystrack_icon.svg" alt="Raystrack" width="160">
26
+
27
+ Raystrack computes radiative view factors between triangulated surfaces using
28
+ quasi Monte Carlo ray tracing. Version 2.0.0 has a unified API built
29
+ around `Scene`, `Solver`, `Query`, `Run`, and immutable `Result` snapshots.
30
+ The v1 calculation functions have been replaced. See the
31
+ [migration guide](https://github.com/philip-ba/raystrack/blob/main/docs/v2-migration.md) before upgrading an existing script.
32
+
33
+ The [Grasshopper integration](https://github.com/philip-ba/raystrack/blob/main/docs/grasshopper.md) lives in this repository too.
34
+ It provides compiled **RS** components, live background solves, a Raystrack
35
+ ribbon icon, and a bundled Python runtime through Yak or a standalone ZIP.
36
+
37
+ ## Installation
38
+
39
+ ```sh
40
+ pip install "raystrack==2.0.0"
41
+ pip install "raystrack[portable-gpu]==2.0.0" # optional Taichi Vulkan/Metal
42
+ pip install "raystrack[cuda]==2.0.0" # optional NVIDIA CUDA
43
+ ```
44
+
45
+ In Rhino 8.35 or later on Windows, open **PackageManager**, search for
46
+ **raystrack**, and install version **2.0.0**. Restart Rhino, then open
47
+ Grasshopper's **Raystrack** tab. The Yak package includes its Python runtime.
48
+ The [Grasshopper guide](https://github.com/philip-ba/raystrack/blob/main/docs/grasshopper.md) covers components and examples.
49
+
50
+ To install from a source checkout:
51
+
52
+ ```sh
53
+ pip install .
54
+ pip install ".[portable-gpu]" # optional Taichi Vulkan/Metal
55
+ pip install ".[cuda]" # optional NVIDIA CUDA target for Numba
56
+ ```
57
+
58
+ The package requires Python 3.9–3.13, NumPy, and Numba. CPU tracing works without
59
+ optional GPU dependencies. `device="cpu"`, `"cuda"`, `"vulkan"`, or `"metal"`
60
+ chooses a backend explicitly; unavailable GPU requests raise availability errors.
61
+ `"gpu"` requires an available GPU, and `"taichi"` chooses a portable GPU runtime.
62
+ `"auto"` can select CPU or GPU. These are compute tracers with software BVHs;
63
+ they do not use hardware ray tracing extensions.
64
+
65
+ ```python
66
+ from raystrack import available_devices
67
+ print(available_devices())
68
+ ```
69
+
70
+ Vulkan has been tested on an AMD Radeon 860M. Metal, Intel GPUs, and physical
71
+ CUDA hardware need their own validation; CUDA simulator checks verify kernel
72
+ behavior rather than performance. Taichi reuses one compatible FP32 runtime per
73
+ process and serializes access. Set `TI_VISIBLE_DEVICE` before first GPU use when
74
+ choosing a Taichi adapter.
75
+
76
+ ## Quick start
77
+
78
+ ```python
79
+ import numpy as np
80
+ from raystrack import (
81
+ Mesh, Scene, Solver, Query, SolveOptions, Sampling, Accuracy,
82
+ Budget, Channel,
83
+ )
84
+
85
+ vertices = np.array([[-1,-1,0], [1,-1,0], [1,1,0], [-1,1,0]], np.float32)
86
+ faces = np.array([[0,1,2], [0,2,3]], np.int32)
87
+ scene = Scene.from_meshes({
88
+ "lower": Mesh(vertices, faces),
89
+ "upper": Mesh(vertices + [0,0,1], faces[:, ::-1]),
90
+ })
91
+ options = SolveOptions(
92
+ sampling=Sampling(density=16, rays_per_cell=128, seed=11),
93
+ accuracy=Accuracy(max_replicates=20, min_replicates=5, tolerance=1e-4),
94
+ batch_size=65536,
95
+ )
96
+ with Solver(scene, device="cpu", bvh="builtin") as solver:
97
+ result = solver.solve(Query.row("lower", sky="merged"), options,
98
+ Budget(rays=65536))
99
+
100
+ print(result.value("lower", Channel("surface", "upper", "front")))
101
+ print(result.value("lower", Channel("sky")))
102
+ print(result.status, result.rays_used, result.cumulative_rays)
103
+ ```
104
+
105
+ `Mesh` owns immutable float32 vertices and int32 triangle faces. Winding defines
106
+ the front side. Surface IDs are unique strings independent of display labels;
107
+ IDs containing `_front`, spaces, or Unicode stay unambiguous. `Scene` owns
108
+ ordered surfaces and rigid instance transforms. Editing a label leaves the
109
+ geometry revision unchanged.
110
+
111
+ ## Choose outputs and sampling
112
+
113
+ `Query.matrix()` requests all surface rows. `Query.row("wall")` requests one
114
+ sender, and `Query.pair("wall", "roof")` selects one receiver. Receiver selection
115
+ filters outputs; every surface can still occlude rays. `receiver_sides` can
116
+ select `("front",)`, `("back",)`, or both.
117
+
118
+ Add `sky="merged"` or `sky="tregenza145"` to a scene query to trace sky with the
119
+ same rays. `Query.sky(senders=["wall"], discrete=True)` requests only sky outputs.
120
+ Sky patches are indexed `0..144` through `Channel("sky", patch=index)`.
121
+
122
+ `SolveOptions` holds one immutable configuration for a query:
123
+
124
+ - `Sampling`: emission density, rays per cell, seed, face reversal, fair/adaptive
125
+ allocation, and estimator strategy.
126
+ - `Accuracy`: replicate cap, minimum replicate/ray floors, tolerance, and
127
+ convergence check interval.
128
+ - `Postprocessing`: explicit reciprocity transformation after complete solves.
129
+ - `batch_size`: a cap for bounded trace chunks.
130
+
131
+ The default cosine strategy uses independently shifted Halton replicates.
132
+ `mode="fair"` serves each selected sender; `"adaptive"` gives more work to rows
133
+ with larger uncertainty after initial exploration. A minimum ray floor reduces
134
+ zero-hit early stopping but does not prove that rare receivers were observed.
135
+
136
+ For a small selected receiver, use `Sampling(strategy="area_pair",
137
+ pair_samples=8192, sequence="shifted_halton")` with `Query.pair(...)`.
138
+ Unsupported estimator/output combinations raise `CapabilityError`. Area-pair
139
+ sampling weights uniformly sampled surface pairs by their cosine and
140
+ inverse-distance factors and checks visibility against all occluders. It supports
141
+ CPU only; unsupported GPU, sky, or reciprocity combinations raise explicitly.
142
+ `sequence="random"` is also available for this estimator.
143
+
144
+ Meshes can see and occlude themselves: other faces of the emitting surface
145
+ participate in tracing, and `Query.pair("box", "box")` selects its self view
146
+ factor. Matrix/row queries include the same channels. For a closed box with
147
+ outward mesh normals, use `Sampling(flip_faces=True)` to emit into its interior;
148
+ the self factor is 1 on `Channel("surface", "box", "back")`, with zero escape.
149
+ With inward normals, use the front channel and leave `flip_faces=False`.
150
+ Receiver side labels always follow the stored mesh winding.
151
+
152
+ Reciprocity is opt-in through `Postprocessing("bidirectional")` or
153
+ `Postprocessing("shortcut")`, both requiring complete sender/receiver tables.
154
+ The shortcut transformation preserves the v1 upper-direction estimate but v2
155
+ still traces every requested row; it does not halve the workload. Transformed
156
+ uncertainties are unknown and the transformation is recorded in provenance.
157
+ `"rowsum"` additionally requires a closed enclosure without escape or sky.
158
+ Raw previews use `Postprocessing("none")`, the default.
159
+
160
+ ## Moving scenes and continued work
161
+
162
+ ```python
163
+ with Solver(scene, device="cpu", acceleration="instanced") as solver:
164
+ query = Query.row("lower", sky="merged")
165
+ run = solver.start(query, options)
166
+ first = run.advance(Budget(rays=4096))
167
+ refined = run.advance(Budget(rays=12288)) # only additional rays
168
+ assert refined.cumulative_rays == 16384
169
+
170
+ transform = np.eye(4)
171
+ transform[0, 3] = 2
172
+ scene.update_transform("upper", transform)
173
+ new_frame = solver.start(query, options).advance(Budget(rays=4096))
174
+ ```
175
+
176
+ Transforms are absolute proper rotations and translations relative to the local
177
+ mesh; scale, shear, reflection, invalid matrices, and float32 overflow are
178
+ rejected. `scene.update_vertices(id, world_vertices)` retains topology and resets
179
+ the transform. `scene.update_mesh(id, Mesh(...))` replaces geometry and resets
180
+ the transform. Updates invalidate existing runs before waiting for active work.
181
+ A new run starts a new stream for the new `scene.revision`; old results remain
182
+ immutable snapshots labeled with their original revision.
183
+
184
+ Flat acceleration refits packed triangle bounds when topology is unchanged.
185
+ `acceleration="instanced"` keeps local object BVHs and updates instance transforms
186
+ and a top-level hierarchy. Its hierarchy refits and rebuilds when its measured
187
+ bound cost deteriorates. World emitter frames are prepared lazily; emitter work
188
+ can still scale with changed triangle counts. Share local geometry explicitly:
189
+
190
+ ```python
191
+ prototype = Mesh(vertices, faces)
192
+ instances = Scene.from_instances({"panel": prototype}, [
193
+ ("panel0", "panel", np.eye(4)),
194
+ ("panel1", "panel", transform),
195
+ ])
196
+ ```
197
+
198
+ Rigid updates retain local geometry/BVH buffers on CPU, Vulkan/Metal, and CUDA.
199
+ Deformation refreshes the affected object, and topology changes rebuild it.
200
+ Instancing reduces update/storage work; tracing speed depends on the scene.
201
+
202
+ `Run.advance` preserves exact ray offsets and completed replicate statistics.
203
+ Accuracy, postprocessing, and batch size can change on a stationary run; sampling
204
+ settings require a new run. Solver configuration and run queries are read-only;
205
+ create a new solver to change the backend. Use `advance(options=...)` to adjust
206
+ accuracy. Ray budgets are additional work. Time budgets are
207
+ soft deadlines checked between chunks and include preparation; cold compilation
208
+ can exceed them. `Budget(rays=0)` returns the current estimate without tracing.
209
+
210
+ `Run.submit(...)` returns a future. Repeated submissions queue additive advances
211
+ through one solver worker. Cancel an obsolete run explicitly before replacing
212
+ it. `Run.cancel()` is terminal: later advances return a cancelled snapshot with
213
+ zero additional rays. Cancellation retains partial estimates. A geometry change
214
+ marks the run `invalidated`; later advances raise and require a new run. Solver
215
+ and run context managers close resources.
216
+
217
+ ## Read results and uncertainty
218
+
219
+ `Result.row(sender)` maps typed channels to estimates. Surface, sky, `rest`, and
220
+ `unrequested` channels avoid suffix parsing. Rest contains escaped rays outside
221
+ requested sky; unrequested contains hits omitted by receiver/side selection.
222
+ For cosine queries these channels preserve raw energy accounting.
223
+
224
+ `coverage` aligns with `sender_ids`: `0` means not started, `1` means sampled,
225
+ and `-1` means coverage is unknown in imported data. A requested zero in a
226
+ sampled row is known zero; an unsampled value is `None`. Sparse arrays hold
227
+ estimates and errors, while `.dense()` exports a mutable copy.
228
+
229
+ Estimates include partial replicates. `Result.error(...)` is `None` until there
230
+ are at least two complete randomized replicates and no pending partial replicate.
231
+ `statistics["emitters"]` retains completed-replicate standard errors, sample
232
+ counts, pending rays, and convergence details. Partial work cannot claim
233
+ convergence. Error estimates describe sampling variation, not mesh or floating
234
+ point error, and zero observed hits can underestimate rare-event uncertainty.
235
+
236
+ ## Calibration and storage
237
+
238
+ `solver.warmup(query, options)` compiles and measures representative synchronized
239
+ CPU/GPU tracing. Probe rays are diagnostic and do not enter results or ray
240
+ budgets. An unrestricted `Solver.solve` may calibrate automatically; bounded
241
+ runs use cached/provisional plans. Resumed runs retain their numerical backend.
242
+ Warm up before interactive deadlines, or use an explicit device with
243
+ `auto_tune=False` for a fixed execution choice.
244
+
245
+ ```python
246
+ from raystrack.io import save, load, import_v1_json
247
+
248
+ save("case.raystrack", scene, new_frame, metadata={"case": "moving wall"})
249
+ stored = load("case.raystrack")
250
+ legacy = import_v1_json("examples/street_canyon.json", "examples/vf_matrix.json")
251
+ ```
252
+
253
+ Stores are versioned directories with chunked arrays and a manifest published
254
+ last. Existing stores are never overwritten. V2 preserves stable IDs, shared
255
+ geometry, transforms, sparse channels, statistics, execution, and provenance.
256
+ `load` also reads v1 stores. Legacy imports preserve unknown sampling coverage
257
+ and uncertainty instead of inventing counts. V2 currently loads a full immutable
258
+ snapshot; selective on-disk row loading and appendable writers are unavailable.
259
+
260
+ Rhino conversion is isolated in `raystrack.integrations.rhino` through
261
+ `from_rhino_mesh` and `from_rhino_scene`; importing the numerical package does
262
+ not require Rhino. The converter triangulates quads and copies vertices.
263
+
264
+ ## Examples and verification
265
+
266
+ Run examples directly from this checkout, beginning with
267
+ `python examples/ex00_street_canyon_geometry.py`. Examples also build the canyon
268
+ in memory, so generated geometry files are optional. `ex06_dynamic_preview.py`
269
+ accepts `--device`, `--acceleration`, `--budget`, `--frames`, and `--warmup`.
270
+
271
+ ```sh
272
+ PYTHONPATH=src python -m pytest tests -q
273
+ python validation/validate_dynamic_backends.py --devices cpu vulkan --accelerations flat instanced
274
+ python validation/benchmark_v2_vs_v1.py --gpu-device vulkan
275
+ ```
276
+
277
+ The [v2 accuracy report](validation/results/v2_dynamic_accuracy.json) passes 72
278
+ closed-form comparisons across CPU/Vulkan and flat/instanced traversal. The
279
+ [validation guide](validation/readme.md) describes larger analytical checks,
280
+ optional benchmarks and their limits. Performance timing validation was skipped
281
+ at the user's request because background simulations made measurements
282
+ unreliable; this change makes no speedup claim. The [migration guide](https://github.com/philip-ba/raystrack/blob/main/docs/v2-migration.md)
283
+ shows v1-to-v2 replacements.
@@ -0,0 +1,261 @@
1
+ # Raystrack
2
+
3
+ <img src="https://raw.githubusercontent.com/philip-ba/raystrack/main/raystrack_icon.svg" alt="Raystrack" width="160">
4
+
5
+ Raystrack computes radiative view factors between triangulated surfaces using
6
+ quasi Monte Carlo ray tracing. Version 2.0.0 has a unified API built
7
+ around `Scene`, `Solver`, `Query`, `Run`, and immutable `Result` snapshots.
8
+ The v1 calculation functions have been replaced. See the
9
+ [migration guide](https://github.com/philip-ba/raystrack/blob/main/docs/v2-migration.md) before upgrading an existing script.
10
+
11
+ The [Grasshopper integration](https://github.com/philip-ba/raystrack/blob/main/docs/grasshopper.md) lives in this repository too.
12
+ It provides compiled **RS** components, live background solves, a Raystrack
13
+ ribbon icon, and a bundled Python runtime through Yak or a standalone ZIP.
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ pip install "raystrack==2.0.0"
19
+ pip install "raystrack[portable-gpu]==2.0.0" # optional Taichi Vulkan/Metal
20
+ pip install "raystrack[cuda]==2.0.0" # optional NVIDIA CUDA
21
+ ```
22
+
23
+ In Rhino 8.35 or later on Windows, open **PackageManager**, search for
24
+ **raystrack**, and install version **2.0.0**. Restart Rhino, then open
25
+ Grasshopper's **Raystrack** tab. The Yak package includes its Python runtime.
26
+ The [Grasshopper guide](https://github.com/philip-ba/raystrack/blob/main/docs/grasshopper.md) covers components and examples.
27
+
28
+ To install from a source checkout:
29
+
30
+ ```sh
31
+ pip install .
32
+ pip install ".[portable-gpu]" # optional Taichi Vulkan/Metal
33
+ pip install ".[cuda]" # optional NVIDIA CUDA target for Numba
34
+ ```
35
+
36
+ The package requires Python 3.9–3.13, NumPy, and Numba. CPU tracing works without
37
+ optional GPU dependencies. `device="cpu"`, `"cuda"`, `"vulkan"`, or `"metal"`
38
+ chooses a backend explicitly; unavailable GPU requests raise availability errors.
39
+ `"gpu"` requires an available GPU, and `"taichi"` chooses a portable GPU runtime.
40
+ `"auto"` can select CPU or GPU. These are compute tracers with software BVHs;
41
+ they do not use hardware ray tracing extensions.
42
+
43
+ ```python
44
+ from raystrack import available_devices
45
+ print(available_devices())
46
+ ```
47
+
48
+ Vulkan has been tested on an AMD Radeon 860M. Metal, Intel GPUs, and physical
49
+ CUDA hardware need their own validation; CUDA simulator checks verify kernel
50
+ behavior rather than performance. Taichi reuses one compatible FP32 runtime per
51
+ process and serializes access. Set `TI_VISIBLE_DEVICE` before first GPU use when
52
+ choosing a Taichi adapter.
53
+
54
+ ## Quick start
55
+
56
+ ```python
57
+ import numpy as np
58
+ from raystrack import (
59
+ Mesh, Scene, Solver, Query, SolveOptions, Sampling, Accuracy,
60
+ Budget, Channel,
61
+ )
62
+
63
+ vertices = np.array([[-1,-1,0], [1,-1,0], [1,1,0], [-1,1,0]], np.float32)
64
+ faces = np.array([[0,1,2], [0,2,3]], np.int32)
65
+ scene = Scene.from_meshes({
66
+ "lower": Mesh(vertices, faces),
67
+ "upper": Mesh(vertices + [0,0,1], faces[:, ::-1]),
68
+ })
69
+ options = SolveOptions(
70
+ sampling=Sampling(density=16, rays_per_cell=128, seed=11),
71
+ accuracy=Accuracy(max_replicates=20, min_replicates=5, tolerance=1e-4),
72
+ batch_size=65536,
73
+ )
74
+ with Solver(scene, device="cpu", bvh="builtin") as solver:
75
+ result = solver.solve(Query.row("lower", sky="merged"), options,
76
+ Budget(rays=65536))
77
+
78
+ print(result.value("lower", Channel("surface", "upper", "front")))
79
+ print(result.value("lower", Channel("sky")))
80
+ print(result.status, result.rays_used, result.cumulative_rays)
81
+ ```
82
+
83
+ `Mesh` owns immutable float32 vertices and int32 triangle faces. Winding defines
84
+ the front side. Surface IDs are unique strings independent of display labels;
85
+ IDs containing `_front`, spaces, or Unicode stay unambiguous. `Scene` owns
86
+ ordered surfaces and rigid instance transforms. Editing a label leaves the
87
+ geometry revision unchanged.
88
+
89
+ ## Choose outputs and sampling
90
+
91
+ `Query.matrix()` requests all surface rows. `Query.row("wall")` requests one
92
+ sender, and `Query.pair("wall", "roof")` selects one receiver. Receiver selection
93
+ filters outputs; every surface can still occlude rays. `receiver_sides` can
94
+ select `("front",)`, `("back",)`, or both.
95
+
96
+ Add `sky="merged"` or `sky="tregenza145"` to a scene query to trace sky with the
97
+ same rays. `Query.sky(senders=["wall"], discrete=True)` requests only sky outputs.
98
+ Sky patches are indexed `0..144` through `Channel("sky", patch=index)`.
99
+
100
+ `SolveOptions` holds one immutable configuration for a query:
101
+
102
+ - `Sampling`: emission density, rays per cell, seed, face reversal, fair/adaptive
103
+ allocation, and estimator strategy.
104
+ - `Accuracy`: replicate cap, minimum replicate/ray floors, tolerance, and
105
+ convergence check interval.
106
+ - `Postprocessing`: explicit reciprocity transformation after complete solves.
107
+ - `batch_size`: a cap for bounded trace chunks.
108
+
109
+ The default cosine strategy uses independently shifted Halton replicates.
110
+ `mode="fair"` serves each selected sender; `"adaptive"` gives more work to rows
111
+ with larger uncertainty after initial exploration. A minimum ray floor reduces
112
+ zero-hit early stopping but does not prove that rare receivers were observed.
113
+
114
+ For a small selected receiver, use `Sampling(strategy="area_pair",
115
+ pair_samples=8192, sequence="shifted_halton")` with `Query.pair(...)`.
116
+ Unsupported estimator/output combinations raise `CapabilityError`. Area-pair
117
+ sampling weights uniformly sampled surface pairs by their cosine and
118
+ inverse-distance factors and checks visibility against all occluders. It supports
119
+ CPU only; unsupported GPU, sky, or reciprocity combinations raise explicitly.
120
+ `sequence="random"` is also available for this estimator.
121
+
122
+ Meshes can see and occlude themselves: other faces of the emitting surface
123
+ participate in tracing, and `Query.pair("box", "box")` selects its self view
124
+ factor. Matrix/row queries include the same channels. For a closed box with
125
+ outward mesh normals, use `Sampling(flip_faces=True)` to emit into its interior;
126
+ the self factor is 1 on `Channel("surface", "box", "back")`, with zero escape.
127
+ With inward normals, use the front channel and leave `flip_faces=False`.
128
+ Receiver side labels always follow the stored mesh winding.
129
+
130
+ Reciprocity is opt-in through `Postprocessing("bidirectional")` or
131
+ `Postprocessing("shortcut")`, both requiring complete sender/receiver tables.
132
+ The shortcut transformation preserves the v1 upper-direction estimate but v2
133
+ still traces every requested row; it does not halve the workload. Transformed
134
+ uncertainties are unknown and the transformation is recorded in provenance.
135
+ `"rowsum"` additionally requires a closed enclosure without escape or sky.
136
+ Raw previews use `Postprocessing("none")`, the default.
137
+
138
+ ## Moving scenes and continued work
139
+
140
+ ```python
141
+ with Solver(scene, device="cpu", acceleration="instanced") as solver:
142
+ query = Query.row("lower", sky="merged")
143
+ run = solver.start(query, options)
144
+ first = run.advance(Budget(rays=4096))
145
+ refined = run.advance(Budget(rays=12288)) # only additional rays
146
+ assert refined.cumulative_rays == 16384
147
+
148
+ transform = np.eye(4)
149
+ transform[0, 3] = 2
150
+ scene.update_transform("upper", transform)
151
+ new_frame = solver.start(query, options).advance(Budget(rays=4096))
152
+ ```
153
+
154
+ Transforms are absolute proper rotations and translations relative to the local
155
+ mesh; scale, shear, reflection, invalid matrices, and float32 overflow are
156
+ rejected. `scene.update_vertices(id, world_vertices)` retains topology and resets
157
+ the transform. `scene.update_mesh(id, Mesh(...))` replaces geometry and resets
158
+ the transform. Updates invalidate existing runs before waiting for active work.
159
+ A new run starts a new stream for the new `scene.revision`; old results remain
160
+ immutable snapshots labeled with their original revision.
161
+
162
+ Flat acceleration refits packed triangle bounds when topology is unchanged.
163
+ `acceleration="instanced"` keeps local object BVHs and updates instance transforms
164
+ and a top-level hierarchy. Its hierarchy refits and rebuilds when its measured
165
+ bound cost deteriorates. World emitter frames are prepared lazily; emitter work
166
+ can still scale with changed triangle counts. Share local geometry explicitly:
167
+
168
+ ```python
169
+ prototype = Mesh(vertices, faces)
170
+ instances = Scene.from_instances({"panel": prototype}, [
171
+ ("panel0", "panel", np.eye(4)),
172
+ ("panel1", "panel", transform),
173
+ ])
174
+ ```
175
+
176
+ Rigid updates retain local geometry/BVH buffers on CPU, Vulkan/Metal, and CUDA.
177
+ Deformation refreshes the affected object, and topology changes rebuild it.
178
+ Instancing reduces update/storage work; tracing speed depends on the scene.
179
+
180
+ `Run.advance` preserves exact ray offsets and completed replicate statistics.
181
+ Accuracy, postprocessing, and batch size can change on a stationary run; sampling
182
+ settings require a new run. Solver configuration and run queries are read-only;
183
+ create a new solver to change the backend. Use `advance(options=...)` to adjust
184
+ accuracy. Ray budgets are additional work. Time budgets are
185
+ soft deadlines checked between chunks and include preparation; cold compilation
186
+ can exceed them. `Budget(rays=0)` returns the current estimate without tracing.
187
+
188
+ `Run.submit(...)` returns a future. Repeated submissions queue additive advances
189
+ through one solver worker. Cancel an obsolete run explicitly before replacing
190
+ it. `Run.cancel()` is terminal: later advances return a cancelled snapshot with
191
+ zero additional rays. Cancellation retains partial estimates. A geometry change
192
+ marks the run `invalidated`; later advances raise and require a new run. Solver
193
+ and run context managers close resources.
194
+
195
+ ## Read results and uncertainty
196
+
197
+ `Result.row(sender)` maps typed channels to estimates. Surface, sky, `rest`, and
198
+ `unrequested` channels avoid suffix parsing. Rest contains escaped rays outside
199
+ requested sky; unrequested contains hits omitted by receiver/side selection.
200
+ For cosine queries these channels preserve raw energy accounting.
201
+
202
+ `coverage` aligns with `sender_ids`: `0` means not started, `1` means sampled,
203
+ and `-1` means coverage is unknown in imported data. A requested zero in a
204
+ sampled row is known zero; an unsampled value is `None`. Sparse arrays hold
205
+ estimates and errors, while `.dense()` exports a mutable copy.
206
+
207
+ Estimates include partial replicates. `Result.error(...)` is `None` until there
208
+ are at least two complete randomized replicates and no pending partial replicate.
209
+ `statistics["emitters"]` retains completed-replicate standard errors, sample
210
+ counts, pending rays, and convergence details. Partial work cannot claim
211
+ convergence. Error estimates describe sampling variation, not mesh or floating
212
+ point error, and zero observed hits can underestimate rare-event uncertainty.
213
+
214
+ ## Calibration and storage
215
+
216
+ `solver.warmup(query, options)` compiles and measures representative synchronized
217
+ CPU/GPU tracing. Probe rays are diagnostic and do not enter results or ray
218
+ budgets. An unrestricted `Solver.solve` may calibrate automatically; bounded
219
+ runs use cached/provisional plans. Resumed runs retain their numerical backend.
220
+ Warm up before interactive deadlines, or use an explicit device with
221
+ `auto_tune=False` for a fixed execution choice.
222
+
223
+ ```python
224
+ from raystrack.io import save, load, import_v1_json
225
+
226
+ save("case.raystrack", scene, new_frame, metadata={"case": "moving wall"})
227
+ stored = load("case.raystrack")
228
+ legacy = import_v1_json("examples/street_canyon.json", "examples/vf_matrix.json")
229
+ ```
230
+
231
+ Stores are versioned directories with chunked arrays and a manifest published
232
+ last. Existing stores are never overwritten. V2 preserves stable IDs, shared
233
+ geometry, transforms, sparse channels, statistics, execution, and provenance.
234
+ `load` also reads v1 stores. Legacy imports preserve unknown sampling coverage
235
+ and uncertainty instead of inventing counts. V2 currently loads a full immutable
236
+ snapshot; selective on-disk row loading and appendable writers are unavailable.
237
+
238
+ Rhino conversion is isolated in `raystrack.integrations.rhino` through
239
+ `from_rhino_mesh` and `from_rhino_scene`; importing the numerical package does
240
+ not require Rhino. The converter triangulates quads and copies vertices.
241
+
242
+ ## Examples and verification
243
+
244
+ Run examples directly from this checkout, beginning with
245
+ `python examples/ex00_street_canyon_geometry.py`. Examples also build the canyon
246
+ in memory, so generated geometry files are optional. `ex06_dynamic_preview.py`
247
+ accepts `--device`, `--acceleration`, `--budget`, `--frames`, and `--warmup`.
248
+
249
+ ```sh
250
+ PYTHONPATH=src python -m pytest tests -q
251
+ python validation/validate_dynamic_backends.py --devices cpu vulkan --accelerations flat instanced
252
+ python validation/benchmark_v2_vs_v1.py --gpu-device vulkan
253
+ ```
254
+
255
+ The [v2 accuracy report](validation/results/v2_dynamic_accuracy.json) passes 72
256
+ closed-form comparisons across CPU/Vulkan and flat/instanced traversal. The
257
+ [validation guide](validation/readme.md) describes larger analytical checks,
258
+ optional benchmarks and their limits. Performance timing validation was skipped
259
+ at the user's request because background simulations made measurements
260
+ unreliable; this change makes no speedup claim. The [migration guide](https://github.com/philip-ba/raystrack/blob/main/docs/v2-migration.md)
261
+ shows v1-to-v2 replacements.
@@ -4,17 +4,17 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "raystrack"
7
- version = "1.0.2"
8
- description = "Lightweight Monte-Carlo view-factor solver with CPU, CUDA and BVH paths"
7
+ version = "2.0.0"
8
+ description = "Reusable scene and solver API for Monte Carlo view factors on CPU, CUDA and portable GPUs"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
12
12
  authors = [{ name = "Philip Balizki", email = "philip@metis.earth" }]
13
- requires-python = ">=3.9,<3.13"
13
+ requires-python = ">=3.9,<3.14"
14
14
 
15
15
  dependencies = [
16
16
  "numpy>=1.24,<3.0",
17
- "numba>=0.59,<0.60",
17
+ "numba>=0.60,<0.65",
18
18
  ]
19
19
 
20
20
  classifiers = [
@@ -24,6 +24,10 @@ classifiers = [
24
24
  "Topic :: Scientific/Engineering",
25
25
  ]
26
26
 
27
+ [project.optional-dependencies]
28
+ portable-gpu = ["taichi>=1.7.4,<1.8"]
29
+ cuda = ["numba-cuda>=0.20"]
30
+
27
31
  [project.urls]
28
32
  Homepage = "https://github.com/philip-ba/raystrack"
29
33
 
@@ -0,0 +1,11 @@
1
+ """Raystrack v2: one scene, one solver, one execution pipeline."""
2
+ from .model import Mesh, Surface, Scene
3
+ from .solver import (Solver, Run, Query, SolveOptions, Sampling, Accuracy,
4
+ Postprocessing, Budget, Channel, SparseValues, Result)
5
+ from .backends import available_devices, ExecutionPlan, CapabilityError
6
+ from .io import save, load, StoredRun, import_v1_json
7
+
8
+ __all__ = ["Mesh", "Surface", "Scene", "Solver", "Run", "Query", "SolveOptions",
9
+ "Sampling", "Accuracy", "Postprocessing", "Budget", "Channel",
10
+ "SparseValues", "Result", "available_devices", "ExecutionPlan",
11
+ "CapabilityError", "save", "load", "StoredRun", "import_v1_json"]
@@ -0,0 +1,8 @@
1
+ """Execution backends and capability reporting."""
2
+ from .devices import available_devices
3
+ from .calibration import ExecutionPlan
4
+
5
+ class CapabilityError(ValueError):
6
+ """The requested estimator/output is unsupported by this backend."""
7
+
8
+ __all__ = ["available_devices", "ExecutionPlan", "CapabilityError"]