Skip to content

Commit bad381b

Browse files
authored
Merge pull request #159 from chrishavlin/sph_vol_render_working
Volume Rendering datasets in spherical coordinates
2 parents 4a07d2f + 5346bd7 commit bad381b

26 files changed

Lines changed: 1799 additions & 67 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# temp files
2+
*.swp
13
# Byte-compiled / optimized / DLL files
24
__pycache__/
35
*.py[cod]
@@ -8,6 +10,8 @@ __pycache__/
810

911
# Cythonized files
1012
yt_idv/utilities.c
13+
yt_idv/coordinate_utilities.c
14+
1115

1216
# imgui files
1317
imgui.ini

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ loaded in yt. It is written to provide both scripting and interactive access.
2929
* Block and grid outlines
3030
* Support for sub-selections of data via the yt data selection interface
3131
* Integration with the [ipywidgets](https://ipywidgets.readthedocs.org/) ``Image`` widget.
32+
* Direct volume rendering of block-AMR data in spherical coordinates.
3233

3334
## Examples
3435

docs/coordinate_systems.rst

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
.. highlight:: shell
2+
3+
================================
4+
Non-Cartesian Coordinate Systems
5+
================================
6+
7+
Initial support for volume rendering of data defined in 3D non-cartesian coordinate systems
8+
was added in yt_idv 0.5.0 for block-AMR data in spherical coordinates. While not all
9+
rendering methods and annotations are supported for spherical coordinates, yt_idv can
10+
directly calculate maximum intensity projections, integrative projections and projection with custom
11+
transfer functions without any pre-interpolation or re-gridding.
12+
13+
.. image:: resources/sph-vr-example.png
14+
:align: center
15+
16+
The approach for handling data defined in non-cartesian coordinates is to pre-calculate cartesian
17+
bounding boxes of the data blocks. The rendering pipeline then uses the cartesian bounding boxes
18+
to calculate ray entry/exit points during ray tracing. Between the entry/exit points, the
19+
cartesian coordinates of the ray position are converted to the native coordinates of the data,
20+
which is used to sample the texture maps (which are stored in native coordinates). The initial
21+
implementaiton of the algorithm uses the ray entry/exit points of the cartesian bounding boxes,
22+
and so not all points along the ray are gauranteed to lie within the bounds of the data -- though
23+
these data points will be discarded during ray tracing, it does mean that a larger number of samples
24+
along the ray may be required to ensure the underlying non-cartesian element is properly sampled compared
25+
to the standard cartesian data ray tracing (this is controlled by the ``sample_factor`` attribute of
26+
the ``BlockRendering`` component).
27+
28+
At present, supported non-cartesian coordinate systems include Spherical Coordinates with (r, theta, phi), where r is radius, theta is co-latitude (between 0, pi)
29+
and phi is azimuth (between 0, 2pi), following yt conventions.
30+
31+
----------------------------
32+
Notes on further development
33+
----------------------------
34+
35+
Further contributions are welcome for adding support for the remaining 3d non-cartesian coordinate systems
36+
that yt supports that are not yet supported here (3d cylindrical, 3d geographic) as well as for adding
37+
support for non-cartesian coordinate systems in additional yt_idv components.
38+
39+
To add support for additional non-cartesian coordinate systems requires two steps:
40+
41+
#. Add methods for calcualting cartesian bounding boxes
42+
#. Add support to the rendering pipeline
43+
44+
****************************************************
45+
Add methods for calcualting cartesian bounding boxes
46+
****************************************************
47+
48+
The cartesian bounding box methods are defined in ``yt_idv/coordinate_utilities.pyx``. When adding support
49+
for a new coordiante system, implement a new ``MixedCoordBBox`` child class, following the
50+
``SphericalMixedCoordBBox`` example. The main method is ``get_cartesian_bbox``, which returns the
51+
cartesian bounding box for a single non-cartesian element. Once implemented, ``yt_idv.cartesian_bboxes``
52+
can be used to calculate bounding boxes for arrays of elements.
53+
54+
Next, ``yt_idv.scene_data.block_collection.BlockCollection`` should be updated (mostly in
55+
``add_data`` and ``_set_geometry_attributes``) to handle the new coordinate system following the
56+
example for spherical coordinates.
57+
58+
These two steps will handle the CPU-side of the calculations, next you need to update
59+
the rendering pipeline.
60+
61+
*************************************
62+
Add support to the rendering pipeline
63+
*************************************
64+
65+
A number of changes are required related to the rendering pipeline for the ``block_rendering``
66+
shader program. In order to avoid shader code duplication for different coordinate systems and
67+
also avoid overly branching code within the shader itself, pre-processor directives are used
68+
to provide switches between shader behavior for different coordinate systems.
69+
70+
The ``NONCARTESIAN_GEOM`` directive is for functionality that should cover shared behavior
71+
between all non-cartesian coordinate systems. For example, ``grid_position.vert.glsl``,
72+
``grid_expand.geom.glsl`` and ``ray_tracing.frag.glsl`` all use ``NONCARTESIAN_GEOM`` for
73+
passing along the additional vertex attributes related to the cartesian bounding boxes.
74+
75+
Functionality specific to a coordinate system should be defined with a new pre-processor
76+
directive flag. For spherical coordinates, this is ``SPHERICAL_GEOM``, and is used in
77+
``ray_tracing.frag.glsl`` for defining and using the functions for transforming from
78+
cartesian to spherical coordinates. To add a new one for use in the shaders, add a new
79+
entry to the ``yt_idv.scene_components.base_component._geom_directives`` dictionary and
80+
then wrap any functionality specific to your new coordinate system within pre-processor checks,
81+
for example::
82+
83+
#ifdef SPHERICAL_GEOM
84+
vec3 cart_to_sphere_vec3(vec3 v) {
85+
// transform a single point in cartesian coords to spherical
86+
vec3 vout = vec3(0.,0.,0.);
87+
88+
// ---- code trimmed for clarity ---- //
89+
90+
return vout;
91+
92+
}
93+
#endif
94+
95+
When the shader program compiles, the above function will only be defined for rendering
96+
data in spherical coordinates.
97+
98+
At a minimum, you will need to add a function to handle the coordinate conversion from the
99+
cartesian position of the ray to the native coordinates of your data.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Welcome to yt_idv's documentation!
99
installation
1010
usage
1111
scene
12+
coordinate_systems
1213
examples
1314
modules
1415
contributing

docs/resources/sph-vr-example.png

182 KB
Loading
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import argparse
2+
3+
import numpy as np
4+
import yt
5+
6+
import yt_idv
7+
8+
# yt reminder: phi is the azimuthal angle (0 to 2pi)
9+
# theta is the co-latitude, the angle from north (0 to pi)
10+
# coord ordering here will be r, phi, theta
11+
12+
ax_order = ("r", "phi", "theta")
13+
14+
bbox_options = {
15+
"partial": np.array([[0.5, 1.0], [0.0, np.pi / 3], [np.pi / 4, np.pi / 2]]),
16+
"whole": np.array([[0.0, 1.0], [0.0, 2 * np.pi], [0, np.pi]]),
17+
"shell": np.array([[0.5, 1.0], [0.0, 2 * np.pi], [0, np.pi]]),
18+
"north_hemi": np.array([[0.0, 1.0], [0.0, 2 * np.pi], [0, 0.5 * np.pi]]),
19+
"north_shell": np.array([[0.5, 1.0], [0.0, 2 * np.pi], [0, 0.5 * np.pi]]),
20+
"south_hemi": np.array([[0.0, 1.0], [0.0, 2 * np.pi], [0.5 * np.pi, np.pi]]),
21+
"ew_hemi": np.array([[0.0, 1.0], [0.0, np.pi], [0.0, np.pi]]),
22+
}
23+
24+
max_levs = {
25+
"partial": 4,
26+
"whole": 5,
27+
"shell": 4,
28+
"north_hemi": 5,
29+
"north_shell": 2,
30+
"south_hemi": 5,
31+
"ew_hemi": 5,
32+
}
33+
34+
35+
def _build_ds(bbox_key):
36+
bbox = bbox_options[bbox_key]
37+
38+
bbox_wid = bbox[:, 1] - bbox[:, 0]
39+
bbox_c = np.mean(bbox, axis=1)
40+
sz_0 = np.array((64, 64, 64))
41+
max_lev = max_levs[bbox_key]
42+
43+
# divide grid by 2 every time
44+
dd0 = bbox_wid / sz_0
45+
sz_i = sz_0.copy()
46+
grids = []
47+
for lev in range(max_lev):
48+
49+
box_wid_factor = 2.0 * int(lev > 0) + int(lev == 0) * 1.0
50+
bbox_wid = bbox_wid / box_wid_factor
51+
le_i = bbox_c - bbox_wid / 2.0
52+
re_i = bbox_c + bbox_wid / 2.0
53+
54+
# find closest start/end index in lev 0 grid
55+
start_i = np.round(le_i / dd0).astype(int)
56+
end_i = np.round(re_i / dd0).astype(int)
57+
sz_0 = end_i - start_i
58+
59+
# recompute for rounding errors
60+
le_i = start_i * dd0
61+
re_i = le_i + sz_0 * dd0
62+
63+
sz_i = sz_0 * 2**lev
64+
65+
levp1 = np.full(sz_i, lev + 1.0)
66+
grid = {
67+
"left_edge": le_i,
68+
"right_edge": re_i,
69+
"dimensions": sz_i,
70+
"level": lev,
71+
("stream", "density"): np.random.random(sz_i) * (lev + 1),
72+
("stream", "lev_p1"): levp1,
73+
}
74+
grids.append(grid)
75+
76+
ds = yt.load_amr_grids(
77+
grids,
78+
sz_0,
79+
bbox=bbox,
80+
length_unit=1,
81+
geometry="spherical",
82+
axis_order=ax_order,
83+
)
84+
85+
return ds
86+
87+
88+
if __name__ == "__main__":
89+
90+
parser = argparse.ArgumentParser(
91+
prog="spherical_amr_rendering_with_refinement",
92+
description="Loads an example spherical dataset with grid refinement in yt_idv",
93+
)
94+
95+
msg = f"The geometry subset to generate: one of {list(bbox_options.keys())}"
96+
parser.add_argument("-d", "--domain", default="partial", help=msg)
97+
98+
msg = (
99+
"The field to plot. Provide a comma-separated string with field_type,field "
100+
"e.g., to plot the field tuple ('index', 'phi'): \n "
101+
" $ python amr_spherical_volume_rendering.py -f index,x "
102+
"\nIf a single string is provided, a field type of gas is assumed."
103+
)
104+
parser.add_argument("-f", "--field", default="stream,density", help=msg)
105+
106+
parser.add_argument(
107+
"--listfields", action=argparse.BooleanOptionalAction, default=True
108+
)
109+
110+
args = parser.parse_args()
111+
112+
if args.domain not in bbox_options:
113+
raise RuntimeError(
114+
f"domain must be one of {list(bbox_options.keys())}, found {args.domain}"
115+
)
116+
117+
ds = _build_ds(args.domain)
118+
119+
if args.listfields:
120+
print("available fields:")
121+
print(ds.field_list)
122+
123+
fld = tuple(str(args.field).split(","))
124+
125+
rc = yt_idv.render_context(height=800, width=800, gui=True)
126+
sg = rc.add_scene(ds, fld, no_ghost=True)
127+
rc.scene.components[0].cmap_log = False
128+
rc.scene.components[0].sample_factor = 5.0
129+
130+
rc.run()
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import argparse
2+
3+
import numpy as np
4+
import yt
5+
6+
import yt_idv
7+
8+
# yt reminder: phi is the azimuthal angle (0 to 2pi)
9+
# theta is the co-latitude, the angle from north (0 to pi)
10+
# coord ordering here will be r, phi, theta
11+
bbox_options = {
12+
"partial": np.array([[0.5, 1.0], [0.0, np.pi / 3], [np.pi / 4, np.pi / 2]]),
13+
"whole": np.array([[0.0, 1.0], [0.0, 2 * np.pi], [0, np.pi]]),
14+
"shell": np.array([[0.7, 1.0], [0.0, 2 * np.pi], [0, np.pi]]),
15+
"north_hemi": np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0, 0.5 * np.pi]]),
16+
"north_shell": np.array([[0.8, 1.0], [0.0, 2 * np.pi], [0, 0.5 * np.pi]]),
17+
"south_hemi": np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0.5 * np.pi, np.pi]]),
18+
"ew_hemi": np.array([[0.1, 1.0], [0.0, np.pi], [0.0, np.pi]]),
19+
"quadrant_shell": np.array([[0.6, 1.0], [0.0, np.pi / 2], [0.0, np.pi / 2]]),
20+
}
21+
22+
if __name__ == "__main__":
23+
24+
parser = argparse.ArgumentParser(
25+
prog="spherical_amr_rendering",
26+
description="Loads an example spherical dataset in yt_idv",
27+
)
28+
29+
msg = f"The geometry subset to generate: one of {list(bbox_options.keys())}"
30+
parser.add_argument("-d", "--domain", default="partial", help=msg)
31+
msg = (
32+
"The field to plot. Provide a comma-separated string with field_type,field "
33+
"e.g., to plot the field tuple ('index', 'phi'): \n "
34+
" $ python amr_spherical_volume_rendering.py -f index,x "
35+
"\nIf a single string is provided, a field type of gas is assumed."
36+
)
37+
parser.add_argument("-f", "--field", default="index,phi", help=msg)
38+
parser.add_argument(
39+
"-np", "--nprocs", default=64, help="number of grids to decompose domain to"
40+
)
41+
parser.add_argument(
42+
"-sz", "--size", default=256, help="dimensions, will be (size, size size)"
43+
)
44+
45+
args = parser.parse_args()
46+
47+
sz = (int(args.size),) * 3
48+
fake_data = {"density": np.random.random(sz)}
49+
50+
field = str(args.field).split(",")
51+
if len(field) == 1:
52+
field = ("gas", str(field).strip())
53+
elif len(field) == 2:
54+
field = (field[0].strip(), field[1].strip())
55+
else:
56+
raise RuntimeError(
57+
"Unexpected field formatting. Provide a single string"
58+
" to provide just a field (will assume field type "
59+
" of 'gas', or a comma separated string to provide a "
60+
"field type and a field"
61+
)
62+
63+
if args.domain not in bbox_options:
64+
raise RuntimeError(
65+
f"domain must be one of {list(bbox_options.keys())}, found {args.domain}"
66+
)
67+
bbox = bbox_options[args.domain]
68+
69+
nprocs = int(args.nprocs)
70+
71+
ds = yt.load_uniform_grid(
72+
fake_data,
73+
sz,
74+
bbox=bbox,
75+
nprocs=nprocs,
76+
geometry="spherical",
77+
axis_order=("r", "phi", "theta"),
78+
length_unit=1,
79+
)
80+
81+
phi_c = ds.quan(ds.domain_center[ds.coordinates.axis_id["phi"]].d, "")
82+
theta_c = ds.quan(ds.domain_center[ds.coordinates.axis_id["theta"]].d, "")
83+
rmax = ds.domain_right_edge[ds.coordinates.axis_id["r"]]
84+
phi_f = ds.quan(15.0 * np.pi / 180.0, "")
85+
theta_f = ds.quan(15.0 * np.pi / 180.0, "")
86+
min_val = ds.quan(0.1, "")
87+
88+
def _tube(field, data):
89+
phi = data["index", "phi"]
90+
theta = data["index", "theta"]
91+
tube = (1 - min_val) * np.exp(-(((theta - theta_c) / theta_f) ** 2))
92+
tube = tube * np.exp(-(((phi - phi_c) / phi_f) ** 2))
93+
return tube + min_val
94+
95+
ds.add_field(
96+
name=("stream", "tube"),
97+
function=_tube,
98+
sampling_type="local",
99+
)
100+
101+
def _r_rev(field, data):
102+
r = data["index", "r"]
103+
return rmax - r
104+
105+
ds.add_field(
106+
name=("stream", "r_rev"),
107+
function=_r_rev,
108+
sampling_type="local",
109+
)
110+
111+
if field not in ds.field_list + ds.derived_field_list:
112+
spaces = " " * 8
113+
fld_list_str = f"\n{spaces}".join(str(fld) for fld in ds.field_list)
114+
drv_fld_list_str = f"\n{spaces}".join(str(fld) for fld in ds.derived_field_list)
115+
raise RuntimeError(
116+
f"field {field} not in field_list or derived_field_list:\n"
117+
f"\n ds.field_list:\n{spaces}{fld_list_str}"
118+
f"\n ds.derived_field_list:\n{spaces}{drv_fld_list_str}"
119+
)
120+
121+
rc = yt_idv.render_context(height=800, width=800, gui=True)
122+
sg = rc.add_scene(ds, field, no_ghost=True)
123+
rc.scene.components[0].sample_factor = 5.0
124+
rc.scene.components[0].cmap_log = False
125+
rc.scene.components[0]._reset_cmap_bounds()
126+
127+
rc.run()

0 commit comments

Comments
 (0)