Skip to content
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

BUG: calculate default depth for off axis projections #5081

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

chrishavlin
Copy link
Contributor

@chrishavlin chrishavlin commented Dec 12, 2024

Close #5080

Pretty sure this won't break the SPH rendering.... we'll see how the tests go. but would be good to have confirmation from @nastasha-w

@@ -101,15 +105,7 @@ def get_oblique_window_parameters(

w = tuple(el.in_units("code_length") for el in width)
bounds = tuple(((2 * (i % 2)) - 1) * w[i // 2] / 2 for i in range(len(w) * 2))
if get3bounds and depth is None:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we initially calculate a depth equal to the diagonal + a bit, we don't need this extra logic here as the off-axis projection calls to this function will always have a depth.

@@ -639,7 +639,6 @@ def _generate_image_and_mask(self, item) -> None:
self.bounds[5] - self.bounds[4],
)
)
depth = dd.depth[0] if dd.depth is not None else None
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing this because the depth sanitization before dd creation takes care of this now.

if depth is None:
# off-axis projection, depth not specified
# -> set 'large enough' depth using half the box diagonal + margin
depth = np.linalg.norm(ds.domain_width.in_units("code_length")) * 1.02
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 1.02? Does it need to be an epsilon that large?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 1.02?

Good question -- I was just preserving the 2% epsilon in this line

bounds = bounds + (-0.51 * diag, 0.51 * diag)

Does it need to be an epsilon that large?

I suspect it could be smaller, and for periodic datasets in particular a 2% epsilon might not be ideal... @nastasha-w was there reasoning behind the magnitude of margin you used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for SPH data, periodicities are applied before rotation, and are never taken to be periodic in the rotated coordinates. Therefore, the large margin shouldn't cause trouble, but a smaller value ought to be fine as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I also figured that the half box diagonal would be a very large margin for most line of sight directions anyway

@nastasha-w
Copy link
Contributor

This seems to make sense overall; setting a default depth somewhere should be enough, although TBH it's probably not a great idea in most situations to project a whole box off-axis; in practice that means the depth will vary across the image. It's been a while since I looked into the code and checked what's called when here, but this does seem like something that would break the tests for SPH if done wrong. (I do have a few in there that test if at least roughly the right depth is used.)

@nastasha-w
Copy link
Contributor

Thanks for looking into this! Should we maybe add an SPH and a grid code off-axis image test to look out for this sort of thing going forward?

@chrishavlin
Copy link
Contributor Author

although TBH it's probably not a great idea in most situations to project a whole box off-axis; in practice that means the depth will vary across the image.

i don't disagree necessarily, but that's what was happening implicitly here:

d2 = ds.domain_width[0].in_units("code_length") ** 2
d2 += ds.domain_width[1].in_units("code_length") ** 2
d2 += ds.domain_width[2].in_units("code_length") ** 2
diag = np.sqrt(d2)
bounds = bounds + (-0.51 * diag, 0.51 * diag)

So IMO it's clearer to set that default depth explicitly on initialization of the plot window (whether the default depth is the diagonal of the whole domain or something smaller).

At least for SPH data, periodicities are applied before rotation, and are never taken to be periodic in the rotated coordinates. Therefore, the large margin shouldn't cause trouble, but a smaller value ought to be fine as well.

I'm not positive on if that's the case for gridded datasets, I'll take a closer look to be sure.

Should we maybe add an SPH and a grid code off-axis image test to look out for this sort of thing going forward?

ya, definitely -- I'm very surprised there wasn't already an off-axis image test for gridded data to catch this initially...

@nastasha-w
Copy link
Contributor

So IMO it's clearer to set that default depth explicitly on initialization of the plot window (whether the default depth is the diagonal of the whole domain or something smaller).

Makes sense! I mostly used some of those more complicated things because I wanted to avoid breaking stuff for e.g., grid code projections. I very much appreciate a more logical/simplified approach.

ya, definitely -- I'm very surprised there wasn't already an off-axis image test for gridded data to catch this initially...

There aren't for SPH either -- I have some off-axis tests for those, but those are for fake datasets with a small number of particles. I did do a quick visual test though; I'm pasting the code below. It's for off-axis projections and slices through an SPH dataset. I'm guessing something similar would work for other hydro methods.
(I think the standard image tests were meant more as tests of the different frontends than of the different visualizations.)

# looks good!
def visual_check_offaxis_projection():
    '''
    Plot projections along the x, z axes using the axis-aligned
    methods, then check against just-off x, z and intermediate
    lines of sight to see if we get a nice, continuous rotation.
    Note that paths here are system-specific.
    '''
    simpath = '/Users/nastasha/code/ytdev_misc/sample_data/gizmo_mhd_mwdisk/'
    simfile = 'gizmo_mhd_mwdisk.hdf5'
    outdir = '/Users/nastasha/code/ytdev_misc/testimgs/'
    outfilen_temp = 'sph_projection_test_lineofsight_{projdir}.pdf'

    nsteps = 9
    projdirs = ['x', 'z'] + [f'step{i}' for i in range(nsteps)]
    epsilon = 1e-4
    north_vector = [0., 1., 0.] #y-axis
    
    ds = yt.load(simpath + simfile)
    source = ds.all_data()
    center = [0., 0., 0.]

    for projdir in projdirs:
        if projdir in ['x', 'y', 'z']:
            outfilen = outdir + outfilen_temp.format(projdir=projdir)
            projaxis = projdir
            title = f'line of sight along the {projdir}-axis'
            #dle = ds.domain_left_edge.to("kpc")
            center = ds.arr(center, 'kpc')
            le = center - ds.quan(30., 'kpc')
            re = center + ds.quan(30, 'kpc')
            reg = YTRegion(center.to('code_length'), le.to('code_length'),
                           re.to('code_length'), ds=ds)
            # need to specify width *and* region because some giant SPH
            # particle intersects the central region
            prj = yt.ProjectionPlot(ds, projaxis, ("gas", "density"),
                                    weight_field=None,
                                    buff_size=(400, 400),
                                    center=center,
                                    width=(60, "kpc"),
                                    data_source=reg)
        else:
            _projdir = projdir + f'of{nsteps}'
            outfilen = outdir + outfilen_temp.format(projdir=_projdir)
            stepnum = int(projdir[4:])
            angle = stepnum / (nsteps - 1.) * 0.5 * np.pi
            projaxis = [np.cos(angle + epsilon), 0., np.sin(angle + epsilon)]
            title = f'line of sight x- to z-axis: step {stepnum} / {nsteps}'
            prj = yt.ProjectionPlot(ds, projaxis, ("gas", "density"),
                                    width=(60, "kpc"),
                                    weight_field=None,
                                    buff_size=(400, 400),
                                    center=center,
                                    data_source=source,
                                    north_vector=north_vector,
                                    depth=(60, 'kpc'))
        
        prj.annotate_title(title)
        prj.save(outfilen)

# looks ok, seems consisitent with projections
def visual_check_offaxis_slice():
    '''
    Plot projections along the x, z axes using the axis-aligned
    methods, then check against just-off x, z and intermediate
    lines of sight to see if we get a nice, continuous rotation.
    Note that paths here are system-specific.
    '''
    simpath = '/Users/nastasha/code/ytdev_misc/sample_data/gizmo_mhd_mwdisk/'
    simfile = 'gizmo_mhd_mwdisk.hdf5'
    outdir = '/Users/nastasha/code/ytdev_misc/testimgs/'
    outfilen_temp = 'sph_slice_test_lineofsight_{projdir}.pdf'
    # bounding box recommended at loading
    simbounds = [[-335.41967773, -305.92471313, -304.52108765],
                 [283.54318237, 300.88858032, 379.63751221]]

    nsteps = 9
    projdirs = ['x', 'z'] + [f'step{i}' for i in range(nsteps)]
    epsilon = 1e-4
    north_vector = [0., 1., 0.] #y-axis
    
    ds = yt.load(simpath + simfile, bounding_box=simbounds)
    center = ([0., 0., 0.], 'kpc')
    width = (60., "kpc")

    for projdir in projdirs:
        if projdir in ['x', 'y', 'z']:
            outfilen = outdir + outfilen_temp.format(projdir=projdir)
            projaxis = projdir
            title = f'line of sight along the {projdir}-axis'
            slc = yt.SlicePlot(ds, projaxis, ("gas", "density"),
                               center=center, width=width,
                               buff_size=(400, 400),)
        else:
            _projdir = projdir + f'of{nsteps}'
            outfilen = outdir + outfilen_temp.format(projdir=_projdir)
            stepnum = int(projdir[4:])
            angle = stepnum / (nsteps - 1.) * 0.5 * np.pi
            projaxis = [np.cos(angle + epsilon), 0., np.sin(angle + epsilon)]
            title = f'line of sight x- to z-axis: step {stepnum} / {nsteps}'
            slc = yt.SlicePlot(ds, projaxis, ("gas", "density"),
                               center=center, width=width,
                               buff_size=(400, 400), north_vector=north_vector)
        slc.annotate_title(title)
        slc.save(outfilen)


if __name__ == '__main__':
    visual_check_offaxis_projection()
    visual_check_offaxis_slice()

@chrishavlin
Copy link
Contributor Author

Thanks for the example tests! I think I'll open an issue for adding off axis projections tests so we can get this bug fix in a bit faster. I think I'd also like to address the magnitude of the margin in a follow up as well, @matthewturk how do you feel about that?

@matthewturk
Copy link
Member

sounds good to me

@chrishavlin
Copy link
Contributor Author

Ok, so I think that this one is good to go then, but needs an official approval from someone (@matthewturk @nastasha-w or anyone)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BUG: Broken sample outputs of off-axis projection plots in docs
3 participants