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

Octree ghost zones — reloaded #2425

Merged
merged 65 commits into from
Jul 19, 2020
Merged

Conversation

cphyc
Copy link
Member

@cphyc cphyc commented Jan 23, 2020

PR Summary

This PR adds support for ghost zones (or a similar interface). This is another attempt than #2166 based on the OctVisitors machinery.

The good

import numpy as np
import yt
ds = yt.load('output_00080/info_00080.txt')

ad = ds.all_data()
center = ds.arr(ad.argmax('density'))
sp = ds.sphere(center, (100, 'kpc'))
ds.add_gradient_fields(('gas', 'density'))

center = np.array([.1, .5, .5])
w = np.array([.05, 1, 1])
reg = ds.box(center-w/2, center+w/2)
p = yt.ProjectionPlot(ds, 'x', ['density'] + [f'density_gradient_{k}' for k in 'xyz'], center=center, data_source=reg, weight_field='density')
p.set_axes_unit('Mpc')

for k in 'xyz':
    p.set_log(f'density_gradient_{k}', True, linthresh=1e-55)
    p.set_zlim(f'density_gradient_{k}', -1e-53, 1e-53)
    p.set_cmap(f'density_gradient_{k}', 'bwr')
p.save('frames/the_good/')

info_00080_Projection_x_density_density
info_00080_Projection_x_density_gradient_x_density
info_00080_Projection_x_density_gradient_y_density
info_00080_Projection_x_density_gradient_z_density

The bad

ad = ds.all_data()
center = ds.arr(ad.argmax('density'))
sp = ds.sphere(center, (100, 'kpc'))
ds.add_gradient_fields(('gas', 'density'))

p = yt.SlicePlot(ds, 'x', ['density'] + [f'density_gradient_{k}' for k in 'xyz'], width=sp.radius, center=center, data_source=sp)
p.set_axes_unit('kpc')

dens = p.frb['density']


p.annotate_clear()

for k in 'xyz':
    p.set_log(f'density_gradient_{k}', True, linthresh=1e-48)
    p.set_zlim(f'density_gradient_{k}', -1e-45, 1e-45)
    p.set_cmap(f'density_gradient_{k}', 'bwr')

p.annotate_domain_edges()
p.save('frames/the_bad/')

info_00080_Slice_x_density
info_00080_Slice_x_density_gradient_x
info_00080_Slice_x_density_gradient_y
info_00080_Slice_x_density_gradient_z

@matthewturk
Copy link
Member

I dig the fstrings in your example.

@cphyc
Copy link
Member Author

cphyc commented Jan 30, 2020

Current version:

The good

import numpy as np
import yt
ds = yt.load('output_00080/info_00080.txt')

ad = ds.all_data()
center = ds.arr(ad.argmax('density'))
sp = ds.sphere(center, (100, 'kpc'))
ds.add_gradient_fields(('gas', 'density'))

center = np.array([.1, .5, .5])
w = np.array([.05, 1, 1])
reg = ds.box(center-w/2, center+w/2)
p = yt.ProjectionPlot(ds, 'x', ['density'] + [f'density_gradient_{k}' for k in 'xyz'], center=center, data_source=reg, weight_field='density')
p.set_axes_unit('Mpc')

for k in 'xyz':
    p.set_log(f'density_gradient_{k}', True, linthresh=1e-55)
    p.set_zlim(f'density_gradient_{k}', -1e-53, 1e-53)
    p.set_cmap(f'density_gradient_{k}', 'bwr')
p.save('frames/the_good/')

info_00080_Projection_x_density_density
info_00080_Projection_x_density_gradient_y_density
info_00080_Projection_x_density_gradient_x_density
info_00080_Projection_x_density_gradient_z_density

The bad

ad = ds.all_data()
center = ds.arr(ad.argmax('density'))
sp = ds.sphere(center, (100, 'kpc'))
ds.add_gradient_fields(('gas', 'density'))

p = yt.SlicePlot(ds, 'x', ['density'] + [f'density_gradient_{k}' for k in 'xyz'], width=sp.radius, center=center, data_source=sp)
p.set_axes_unit('kpc')

dens = p.frb['density']


p.annotate_clear()

for k in 'xyz':
    p.set_log(f'density_gradient_{k}', True, linthresh=1e-48)
    p.set_zlim(f'density_gradient_{k}', -1e-45, 1e-45)
    p.set_cmap(f'density_gradient_{k}', 'bwr')

p.annotate_domain_edges()
p.save('frames/the_bad/')

info_00080_Slice_x_density_gradient_y
info_00080_Slice_x_density
info_00080_Slice_x_density_gradient_z
info_00080_Slice_x_density_gradient_x

yt/fields/fluid_fields.py Outdated Show resolved Hide resolved
yt/data_objects/octree_subset.py Outdated Show resolved Hide resolved
yt/fields/fluid_fields.py Outdated Show resolved Hide resolved
Copy link
Member

@matthewturk matthewturk left a comment

Choose a reason for hiding this comment

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

This looks pretty good, but I'm not sure that I've hit all the corner cases. Nice!

setup.py Outdated Show resolved Hide resolved
yt/fields/fluid_fields.py Outdated Show resolved Hide resolved
yt/geometry/oct_container.pyx Show resolved Hide resolved
@cphyc
Copy link
Member Author

cphyc commented Feb 5, 2020

@yt-fido test this please

@cphyc cphyc changed the title [WIP] Octree ghost zones — reloaded Octree ghost zones — reloaded Feb 5, 2020
@cphyc
Copy link
Member Author

cphyc commented Feb 5, 2020

This should be ready to go!

Copy link
Member

@brittonsmith brittonsmith left a comment

Choose a reason for hiding this comment

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

This all looks good to me, but I think it would good to see the answer tests run on this before it's merged since the gradient fields are changed. We could either include this in the PR #2437 or wait until that's in. @matthewturk, what do you think?

@matthewturk
Copy link
Member

I suspect it will be fine to include it now. I will work on getting answer tests in #2437 working for the RAMSES datasets before merging this into that PR.

@brittonsmith
Copy link
Member

I think as long as answer tests are run at some point, it's ok with me. If that happens as part of the yt-4.0 PR, that's fine, too.

yt/fields/fluid_fields.py Show resolved Hide resolved
yt/frontends/ramses/data_structures.py Outdated Show resolved Hide resolved
@cphyc cphyc mentioned this pull request Feb 10, 2020
5 tasks
@cphyc
Copy link
Member Author

cphyc commented Feb 12, 2020

Since @brittonsmith approved the PR, I refactored parts of it to reduce code repetition and make it more readable. Sorry for updating a PR post-review!

Copy link
Member

@brittonsmith brittonsmith left a comment

Choose a reason for hiding this comment

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

New changes look ok to me!

@cphyc
Copy link
Member Author

cphyc commented May 5, 2020

For information, I just rebased the branch on yt-4.0. @matthewturk, you mentioned #2437 (in #2425 (comment)), is it still a blocker to merge this PR?

@cphyc cphyc mentioned this pull request May 27, 2020
8 tasks
@munkm
Copy link
Member

munkm commented May 28, 2020

We're temporarily blocking all merges to the yt-4.0 branch until #2437 gets merged. Is that what you're asking?

@cphyc
Copy link
Member Author

cphyc commented May 29, 2020

Yes, thanks for the reply @munkm ;) In the meantime is there anything I can do to prepare this PR for a future merge into yt-4?

@munkm
Copy link
Member

munkm commented May 29, 2020

I'll do my best to review it today! I can't think of anything at the moment. 🙂

Comment on lines +434 to +441
for i in range(-1, 3):
ishift[0] = i
for j in range(-1, 3):
ishift[1] = j
for k in range(-1, 3):
ishift[2] = k
self.set_neighbour_info(o, ishift)

Copy link
Member Author

Choose a reason for hiding this comment

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

This piece of code is very much optimizable. Here are possible routes to achieve much better performance:

  1. Work oct by oct, which would reduce the number of neighbor lookup from 4³=64 to 3³=27,
  2. Use faster neighbor lookup method(s). For now, all searches are started from the root mesh down to leaf nodes, but we could instead go up the tree from the central oct then down to find all neighbors (see e.g. https://geidav.wordpress.com/2017/12/02/advanced-octrees-4-finding-neighbor-nodes/).
  3. Pre-compute the face-neighbors of all octs.

Note that for the last point, algorithms exist that generate the neighbors of all octs in O(1) time (https://link.springer.com/article/10.1007/s13319-015-0060-9) during the octree construction.
Another possible solution would be to keep a unordered hash map of all the octs indexed by their (3-integers) position. With such structure, finding a neighbor takes O(1) time. This could even come as a replacement of the current pointer-based octree structure.

Copy link
Member

Choose a reason for hiding this comment

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

I think this should be in a comment. It obviously doesn't need to be implemented right now.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

@cphyc cphyc added the yt-4.0 feature targeted for the yt-4.0 release label Jun 11, 2020
@cphyc
Copy link
Member Author

cphyc commented Jun 30, 2020

@yt-fido test this please

@cphyc
Copy link
Member Author

cphyc commented Jul 1, 2020

So the new tests answer tests are hidden here https://tests.yt-project.org/job/yt_py38_git/423/testReport/yt.frontends.ramses.tests (they pass).

@Xarthisius
Copy link
Member

Xarthisius commented Jul 1, 2020

So the new tests answer tests are hidden here https://tests.yt-project.org/job/yt_py38_git/423/testReport/yt.frontends.ramses.tests (they pass).

The test that you've added won't work as intended. There's a few things that need to be fulfilled for a method to work as an answer test:

  1. It has to be decorated with requires_ds (not requires_file)
  2. It has to yield test classes based on AnswerTestingTest e.g. FieldValuesTest
  3. It has to be explicitly enumerated in tests/tests.yaml as yt/frontends/ramses/tests/test_outputs.py:test_field_accession
  4. In case of name collision (which would happen here with test_output_00800 should you yield some additional FieldValuesTest) a unique suffix needs to be added to the test name.

Ultimately it would look roughly like this:

@requires_ds(output_00080)
def test_field_accession():
    ds = data_dir_load(output_00080)
    fields = [ 
        ('gas', 'density'),  # basic ones
        ('gas' ,'pressure'),
        ('gas', 'pressure_gradient_magnitude'), # requires ghost zones
    )   
    dso = ( 
        None,
        ("sphere", ([.1]*3, (0.01, 'unitary'))),
        ("sphere", ([.5]*3, (0.05, 'unitary'))),
        ("box", ([.1]*3, [.2]*3)),
    )   
    for dobj_name in dso:
        for field in fields:
            test = FieldValuesTest(ds, field, dobj_name)
            test.suffix = "accession"
            test_field_accession.__name__ = test.description + test.suffix
            yield test

Having said all that, I think most of the things from this tests are already done in the test_output_00800. It tests density and pressure gradient magnitude, it tests entire domain and one sphere. If you would add ('gas' ,'pressure') to L20, and additional objects in L28 it would do the same (and more) for you. For the sake of efficiency though I'd evaluate if test_output_00800 is sufficient to test your work even without any additions.

Copy link
Member

@neutrinoceros neutrinoceros left a comment

Choose a reason for hiding this comment

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

I'm not competent enough to approve this, but I left a few comments and questions, I hope it helps making this even better. In any case there's a lot going on here and you're doing great ! Keep up the awesome PRs

yt/data_objects/data_containers.py Outdated Show resolved Hide resolved
yt/fields/fluid_fields.py Show resolved Hide resolved
yt/fields/fluid_fields.py Outdated Show resolved Hide resolved
yt/fields/fluid_fields.py Outdated Show resolved Hide resolved
yt/fields/geometric_fields.py Outdated Show resolved Hide resolved
yt/geometry/oct_container.pyx Outdated Show resolved Hide resolved
yt/geometry/oct_container.pyx Outdated Show resolved Hide resolved
yt/geometry/oct_container.pyx Outdated Show resolved Hide resolved
yt/geometry/oct_container.pyx Outdated Show resolved Hide resolved
yt/frontends/ramses/tests/test_outputs.py Outdated Show resolved Hide resolved
@cphyc
Copy link
Member Author

cphyc commented Jul 1, 2020

So the new tests answer tests are hidden here https://tests.yt-project.org/job/yt_py38_git/423/testReport/yt.frontends.ramses.tests (they pass).

The test that you've added won't work as intended. There's a few things that need to be fulfilled for a method to work as an answer test:

1. It has to be decorated with `requires_ds` (not `requires_file`)

2. It has to yield test classes based on `AnswerTestingTest` e.g. `FieldValuesTest`

3. It has to be explicitly enumerated in `tests/tests.yaml` as `yt/frontends/ramses/tests/test_outputs.py:test_field_accession`

4. In case of name collision (which would happen here with `test_output_00800` should you yield some additional FieldValuesTest) a unique suffix needs to be added to the test name.

Ultimately it would look roughly like this:

@requires_ds(output_00080)
def test_field_accession():
    ds = data_dir_load(output_00080)
    fields = [ 
        ('gas', 'density'),  # basic ones
        ('gas' ,'pressure'),
        ('gas', 'pressure_gradient_magnitude'), # requires ghost zones
    )   
    dso = ( 
        None,
        ("sphere", ([.1]*3, (0.01, 'unitary'))),
        ("sphere", ([.5]*3, (0.05, 'unitary'))),
        ("box", ([.1]*3, [.2]*3)),
    )   
    for dobj_name in dso:
        for field in fields:
            test = FieldValuesTest(ds, field, dobj_name)
            test.suffix = "accession"
            test_field_accession.__name__ = test.description + test.suffix
            yield test

Having said all that, I think most of the things from this tests are already done in the test_output_00800. It tests density and pressure gradient magnitude, it tests entire domain and one sphere. If you would add ('gas' ,'pressure') to L20, and additional objects in L28 it would do the same (and more) for you. For the sake of efficiency though I'd evaluate if test_output_00800 is sufficient to test your work even without any additions.

Thanks for the detailed answer. The answer tests should now be included in the already-existing test_output_00080 answer tests. I also added some other unit tests, tell me if you're ok with them.

Copy link
Member

@neutrinoceros neutrinoceros left a comment

Choose a reason for hiding this comment

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

I changed my mind, here's my approval 🥇

@cphyc
Copy link
Member Author

cphyc commented Jul 13, 2020

@yt-fido test this please.

Copy link
Member

@matthewturk matthewturk left a comment

Choose a reason for hiding this comment

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

I think this is really good. Only a few comments.

self.selector,
gz[field][ngz:-ngz, ngz:-ngz, ngz:-ngz],
rv, ind)
data = gz[field][ngz:-ngz, ngz:-ngz, ngz:-ngz]
Copy link
Member

Choose a reason for hiding this comment

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

I like getting rid of the conditional! Nice.

yt/frontends/ramses/data_structures.py Show resolved Hide resolved
yt/frontends/ramses/data_structures.py Show resolved Hide resolved
yt/geometry/oct_container.pyx Show resolved Hide resolved
cdef class BaseNeighbourVisitor(OctVisitor):
cdef int idim # 0,1,2 for x,y,z
cdef int direction # +1 for +x, -1 for -x
cdef np.int8_t[:] neigh_ind
Copy link
Member

Choose a reason for hiding this comment

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

two quick things -- should neigh_ind be a 3d view? and, it's a bit faster if we declare ::1 in them so that it knows they have stride 1.

Copy link
Member Author

Choose a reason for hiding this comment

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

neigh_ind has size 3. I actually just changed it to use a C array instead of a memory view.

cdef np.int64_t[:] domain_inds

cdef class NeighbourCellVisitor(BaseNeighbourVisitor):
cdef np.uint8_t[:] levels
Copy link
Member

Choose a reason for hiding this comment

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

same here about stride

# Assuming periodicity
if fcoords[i] < 0:
fcoords[i] += 1
elif fcoords[i] > 1:
Copy link
Member

Choose a reason for hiding this comment

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

can you make this explicitly a float? and, shouldn't this be checking if it's greater than (and above, less than) the domain width?

Copy link
Member

Choose a reason for hiding this comment

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

actually i may be wrong on that, but want confirmation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thing is, we are computing here the center of each cell, which cannot be at the edge. For example, the cell positions at level 2 in 1D would be [0.125, 0.375, 0.625, 0.875] to which we add ±0.25, so that should cause no problems.

However if you prefer, I can do

Suggested change
elif fcoords[i] > 1:
elif fcoords[i] >= 1:

Comment on lines +434 to +441
for i in range(-1, 3):
ishift[0] = i
for j in range(-1, 3):
ishift[1] = j
for k in range(-1, 3):
ishift[2] = k
self.set_neighbour_info(o, ishift)

Copy link
Member

Choose a reason for hiding this comment

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

I think this should be in a comment. It obviously doesn't need to be implemented right now.

Copy link
Member Author

@cphyc cphyc left a comment

Choose a reason for hiding this comment

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

Comments addressed! Thanks for them

yt/frontends/ramses/data_structures.py Show resolved Hide resolved

if num_ghost_zones > 0:
if not all(ds.periodicity):
warnings.warn('Ghost zones will wrongly assume the domain to be periodic.')
Copy link
Member Author

Choose a reason for hiding this comment

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

No, I changed it to raise a RuntimeError unless there's a better one to raise here?

fill_hydro(fd, file_handler.offset,
file_handler.level_count, levels, cell_inds,
file_handler.level_count,
[self.domain_id-1],
Copy link
Member Author

Choose a reason for hiding this comment

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

I made the name of the variable explicit here and below (the next fill_hydro occurrence).

yt/frontends/ramses/data_structures.py Show resolved Hide resolved
Comment on lines 274 to 275
oinds = oinds.reshape(-1, 64)
cinds = cinds.reshape(-1, 64)
Copy link
Member Author

Choose a reason for hiding this comment

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

Correct, I changed the names.

Comment on lines 735 to 736
for i in range(levels.shape[0]):
if levels[i] != level: continue
Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

-------
oct_inds : int64 ndarray (nocts*8, )
The on-domain index of the octs containing each cell
cell_inds : uint8 array (nocts*8, )
Copy link
Member Author

Choose a reason for hiding this comment

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

done

cdef class BaseNeighbourVisitor(OctVisitor):
cdef int idim # 0,1,2 for x,y,z
cdef int direction # +1 for +x, -1 for -x
cdef np.int8_t[:] neigh_ind
Copy link
Member Author

Choose a reason for hiding this comment

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

neigh_ind has size 3. I actually just changed it to use a C array instead of a memory view.

# Assuming periodicity
if fcoords[i] < 0:
fcoords[i] += 1
elif fcoords[i] > 1:
Copy link
Member Author

Choose a reason for hiding this comment

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

Thing is, we are computing here the center of each cell, which cannot be at the edge. For example, the cell positions at level 2 in 1D would be [0.125, 0.375, 0.625, 0.875] to which we add ±0.25, so that should cause no problems.

However if you prefer, I can do

Suggested change
elif fcoords[i] > 1:
elif fcoords[i] >= 1:

Comment on lines +434 to +441
for i in range(-1, 3):
ishift[0] = i
for j in range(-1, 3):
ishift[1] = j
for k in range(-1, 3):
ishift[2] = k
self.set_neighbour_info(o, ishift)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@munkm munkm merged commit 6daf9cd into yt-project:master Jul 19, 2020
@cphyc cphyc deleted the neighbour-search branch July 20, 2020 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
index: octree new feature Something fun and new! yt-4.0 feature targeted for the yt-4.0 release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants