-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Spherical volume rendering #64
base: main
Are you sure you want to change the base?
Changes from all commits
f4e9c3b
ab78cab
af7663d
ba3ed48
ca6dbfb
d840d3f
83d793f
4cd83b3
726aa82
3cf8186
c9a5bff
065eb6d
ddb69fd
0501c6f
2cb14a8
ad96eb3
fb80cea
65a6abf
f43d577
d9a9771
1ffffb5
0474454
0031ee8
4e4fdec
dbd7240
afcef53
4f73cdd
ac9cd00
3cef5b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
# temp files | ||
*.swp | ||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
*.py[cod] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import sys | ||
|
||
import numpy as np | ||
import yt | ||
|
||
import yt_idv | ||
|
||
# yt reminder: phi is the polar angle (0 to 2pi) | ||
# theta is the angle from north (0 to pi) | ||
|
||
|
||
# coord ordering here will be r, phi, theta | ||
|
||
bbox_options = { | ||
"partial": np.array([[0.5, 1.0], [0.0, np.pi / 4], [np.pi / 4, np.pi / 2]]), | ||
"whole": np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0, np.pi]]), | ||
"north_hemi": np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0, 0.5 * np.pi]]), | ||
"south_hemi": np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0.5 * np.pi, np.pi]]), | ||
"ew_hemi": np.array([[0.1, 1.0], [0.0, np.pi], [0.0, np.pi]]), | ||
} | ||
|
||
|
||
sz = (256, 256, 256) | ||
fake_data = {"density": np.random.random(sz)} | ||
|
||
if __name__ == "__main__": | ||
if len(sys.argv) > 1: | ||
bbox_type = sys.argv[1] | ||
else: | ||
bbox_type = "partial" | ||
|
||
bbox = bbox_options[bbox_type] | ||
|
||
ds = yt.load_uniform_grid( | ||
fake_data, | ||
sz, | ||
bbox=bbox, | ||
nprocs=4096, | ||
geometry=("spherical", ("r", "phi", "theta")), | ||
length_unit="m", | ||
) | ||
|
||
rc = yt_idv.render_context(height=800, width=800, gui=True) | ||
dd = ds.all_data() | ||
dd.max_level = 1 | ||
sg = rc.add_scene(ds, ("index", "r"), no_ghost=True) | ||
# sg = rc.add_scene(ds, ("index", "theta"), no_ghost=True) | ||
# sg = rc.add_scene(ds, ("index", "phi"), no_ghost=True) | ||
sg.camera.focus = [0.0, 0.0, 0.0] | ||
rc.run() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import numpy as np | ||
import yt | ||
|
||
import yt_idv | ||
|
||
# Spherical Test (to line 20) | ||
|
||
NDIM = 32 | ||
|
||
bbox = np.array( | ||
[[0.0, 0.5], [np.pi / 8, 2 * np.pi / 8], [2 * np.pi / 8, 3 * np.pi / 8]] | ||
) | ||
|
||
fake_data = {"density": np.random.random((NDIM, NDIM, NDIM))} | ||
ds = yt.load_uniform_grid( | ||
fake_data, | ||
[NDIM, NDIM, NDIM], | ||
bbox=bbox, | ||
geometry="spherical", | ||
) | ||
|
||
rc = yt_idv.render_context(height=800, width=800, gui=True) | ||
dd = ds.all_data() | ||
dd.max_level = 1 | ||
sg = rc.add_scene(ds, ("index", "r"), no_ghost=True) | ||
sg.camera.focus = [0.0, 0.0, 0.0] | ||
rc.run() | ||
|
||
# Cartesian Test (to line 25) | ||
# ds = yt.load_sample("IsolatedGalaxy") | ||
# rc = yt_idv.render_context(height=800, width=800, gui=True) | ||
# sg = rc.add_scene(ds, "density", no_ghost=True) | ||
# rc.run() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import os | ||
|
||
import numpy as np | ||
import pytest | ||
import yt | ||
|
||
import yt_idv | ||
|
||
|
||
@pytest.fixture() | ||
def osmesa_fake_spherical(): | ||
"""Return an OSMesa context that has a "fake" AMR dataset added, with "radius" | ||
as the field. | ||
""" | ||
|
||
sz = (10, 10, 10) | ||
fake_data = {"density": np.random.random(sz)} | ||
|
||
bbox = np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0, np.pi]]) | ||
|
||
ds = yt.load_uniform_grid( | ||
fake_data, | ||
sz, | ||
bbox=bbox, | ||
nprocs=1, | ||
geometry=("spherical", ("r", "phi", "theta")), | ||
length_unit="m", | ||
) | ||
dd = ds.all_data() | ||
rc = yt_idv.render_context("osmesa", width=1024, height=1024) | ||
rc.add_scene(dd, "density", no_ghost=True) | ||
yield rc | ||
rc.osmesa.OSMesaDestroyContext(rc.context) | ||
|
||
|
||
def test_spherical(osmesa_fake_spherical, tmp_path): | ||
# just checking that it runs here | ||
image = osmesa_fake_spherical.run() | ||
|
||
outdir = tmp_path / "snapshots" | ||
outdir.mkdir() | ||
fn = str(outdir / "testout.png") | ||
_ = yt.write_bitmap(image, fn) | ||
assert os.path.exists(fn) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import numpy as np | ||
|
||
|
||
def phi_normal_planes(edge_coordinates, axis_id, cast_type: str = None): | ||
# for spherical geometries, calculates the cartesian normals and constants | ||
# defining the planes normal to a fixed-phi value. The phi normal plane for | ||
# a given spherical coordinate (r, theta, phi) will contain the given | ||
# coordinate and the z-axis. | ||
# | ||
# edge_coordinates: 3D array of shape (N, 3) containing the spherical | ||
# coordinates for which we want the phi-normal. | ||
# axis_id: dictionary that maps the spherical coordinate axis names to the | ||
# index number. | ||
|
||
phi = edge_coordinates[:, axis_id["phi"]] | ||
theta = edge_coordinates[:, axis_id["theta"]] | ||
r = edge_coordinates[:, axis_id["r"]] | ||
|
||
# get the cartesian values of the coordinates | ||
z = r * np.cos(theta) | ||
r_xy = r * np.sin(theta) | ||
x = r_xy * np.cos(phi) | ||
y = r_xy * np.sin(phi) | ||
xyz = np.column_stack([x, y, z]) | ||
|
||
# construct the planes | ||
z_hat = np.array([0, 0, 1]) | ||
# cross product is vectorized, result is shape (N, 3): | ||
normal_vec = np.cross(xyz, z_hat) | ||
# dot product is not vectorized, do it manually via an elemntwise multiplication | ||
# then summation. result will have shape (N,) | ||
d = (normal_vec * xyz).sum(axis=1) # manual dot product | ||
|
||
normals_d = np.column_stack([normal_vec, d]) | ||
if cast_type is not None: | ||
normals_d = normals_d.astype(cast_type) | ||
return normals_d |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
from yt.data_objects.data_containers import YTDataContainer | ||
|
||
from yt_idv.opengl_support import Texture3D, VertexArray, VertexAttribute | ||
from yt_idv.scene_data._geometry_utils import phi_normal_planes | ||
from yt_idv.scene_data.base_data import SceneData | ||
|
||
|
||
|
@@ -41,6 +42,7 @@ def add_data(self, field, no_ghost=False): | |
# Every time we change our data source, we wipe all existing ones. | ||
# We now set up our vertices into our current data source. | ||
vert, dx, le, re = [], [], [], [] | ||
|
||
self.min_val = +np.inf | ||
self.max_val = -np.inf | ||
if self.scale: | ||
|
@@ -77,27 +79,60 @@ def add_data(self, field, no_ghost=False): | |
LE = np.array([b.LeftEdge for i, b in self.blocks.values()]).min(axis=0) | ||
RE = np.array([b.RightEdge for i, b in self.blocks.values()]).max(axis=0) | ||
self.diagonal = np.sqrt(((RE - LE) ** 2).sum()) | ||
|
||
# Now we set up our buffer | ||
vert = np.array(vert, dtype="f4") | ||
units = self.data_source.ds.units | ||
ratio = (units.code_length / units.unitary).base_value | ||
dx = np.array(dx, dtype="f4") * ratio | ||
le = np.array(le, dtype="f4") * ratio | ||
re = np.array(re, dtype="f4") * ratio | ||
dx = np.array(dx, dtype="f4") | ||
le = np.array(le, dtype="f4") | ||
re = np.array(re, dtype="f4") | ||
if self._yt_geom_str == "cartesian": | ||
units = self.data_source.ds.units | ||
ratio = (units.code_length / units.unitary).base_value | ||
dx = dx * ratio | ||
le = le * ratio | ||
re = re * ratio | ||
self._set_geometry_attributes(le, re) | ||
self.vertex_array.attributes.append( | ||
VertexAttribute(name="model_vertex", data=vert) | ||
) | ||
self.vertex_array.attributes.append(VertexAttribute(name="in_dx", data=dx)) | ||
self.vertex_array.attributes.append( | ||
VertexAttribute(name="in_left_edge", data=le) | ||
VertexAttribute(name="in_left_edge", data=le.astype("f4")) | ||
) | ||
self.vertex_array.attributes.append( | ||
VertexAttribute(name="in_right_edge", data=re) | ||
VertexAttribute(name="in_right_edge", data=re.astype("f4")) | ||
) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it occurs to me that we should have the cartesian volume rendering as the basic option, and we can/should have spherical as a subclass. Let's not try cramming it all in; @neutrinoceros 's work on having things be visible in index-space coordinates is encouraging me to think of it in both ways. That's not really specific here though, except that what we may want to do is to have this be a supplemental function that gets called if the component accessing the data is viewing it in spherical coordinates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ya! I agree totally! I just jammed it in here to get things in initially working. Unjamming it and subclassing it relates to your vectorization question I think. If I vectorize that function, it'll be easier to have a sublcass that takes the output from the standard cartesian version to calculate the new spherical-only attributes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. latest push no longer breaks things for cartesian coords (it's now vectorized and only runs if spherical). I still want to keep thinking on how to better capture the different geometries though. I initially thought it may make sense to have a class SphericalBlockCollection(BlockCollection):
def add_data(self, field, no_ghost=False):
super().add_data(field, no_ghost=no_ghost)
<< --- calculate phi-normal planes from left, right edges --- >> would require us also saving the full |
||
# Now we set up our textures | ||
self._load_textures() | ||
|
||
@property | ||
def _yt_geom_str(self): | ||
# note: casting to string for compatibility with new and old geometry | ||
# attributes (now an enum member in latest yt), | ||
# see https://github.com/yt-project/yt/pull/4244 | ||
return str(self.data_source.ds.geometry) | ||
|
||
def _set_geometry_attributes(self, le, re): | ||
# set any vertex_array attributes that depend on the yt geometry type | ||
|
||
if self._yt_geom_str == "cartesian": | ||
return | ||
elif self._yt_geom_str == "spherical": | ||
axis_id = self.data_source.ds.coordinates.axis_id | ||
phi_plane_le = phi_normal_planes(le, axis_id, cast_type="f4") | ||
phi_plane_re = phi_normal_planes(re, axis_id, cast_type="f4") | ||
self.vertex_array.attributes.append( | ||
VertexAttribute(name="phi_plane_le", data=phi_plane_le) | ||
) | ||
self.vertex_array.attributes.append( | ||
VertexAttribute(name="phi_plane_re", data=phi_plane_re) | ||
) | ||
else: | ||
raise NotImplementedError( | ||
f"{self.name} does not implement {self._yt_geom_str} geometries." | ||
) | ||
|
||
def viewpoint_iter(self, camera): | ||
for block in self.data_source.tiles.traverse(viewpoint=camera.position): | ||
vbo_i, _ = self.blocks[id(block)] | ||
|
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.
@matthewturk
So I had to put the new scaling from #79 in this if statement....
I think we should still scale in the spherical case -- but I think we should just apply this ratio to the r coordinate. That sound right? I know we do want to scale theta and phi from 0 to 1 for the texture maps... but we don't want to scale theta and phi when calculating positions in model space.
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.
I think that is right. Just scale r.