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

Improvements to reading / writing FITS images #2246

Merged
merged 74 commits into from
May 29, 2019
Merged

Conversation

jzuhone
Copy link
Contributor

@jzuhone jzuhone commented Apr 17, 2019

This PR introduces improvements to FITS file writing and reading. Specifically:

  1. Specifies a new standard for writing metadata to FITS files using the FITSImageData class and its subclasses. This standard uses the header to write unit information and other metadata to the FITS file so that when it is read back into yt as a dataset it is more self-describing.
  2. Defines a new class, YTFITSDataset, which is a subclass of FITSDataset which looks specifically for this header metadata.
  3. Simplifies the logic in FITSHierarchy for determining units.
  4. Makes the "beam" unit part of SkyDataFITSDataset.
  5. Adds a convolve method to FITSImageData instances to convolve them with any kernel created using AstroPy or a simple Gaussian kernel.
  6. Specifies that the default image resolution for FITSImageData subclasses should be 512. The previous behavior was inconsistent across the various subclasses.

The new yt-based standard for writing FITS datasets with self-describing information and reading them back in as datasets is set out in a YTEP:

yt-project/ytep#5

Things left to do:

  • add built-in method to YTCoveringGrid to write these grids to FITS files
  • change docs
  • fix / add more tests

@jzuhone jzuhone force-pushed the fits branch 2 times, most recently from a0d51dd to a475b1c Compare April 22, 2019 19:01
@jzuhone jzuhone changed the title [WIP] Improvements to reading / writing FITS images Improvements to reading / writing FITS images Apr 23, 2019
@jzuhone
Copy link
Contributor Author

jzuhone commented Apr 23, 2019

Assuming the tests pass, this is now finally ready to go. I know it's huge and that it's not a very familiar part of the code, for which I apologize.

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.

lgtm!

* ``BUNIT``: The units of the image

FITS header keywords can easily be updated using AstroPy. For example,
to set the ``BTYPE`` and ``BUNIT`` keywords:

.. code-block:: python

import astropy.io.fits as pyfits
f = pyfits.open("xray_flux_image.fits", mode="update")
from astropy.io import fits
Copy link
Member

Choose a reason for hiding this comment

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

how modern! 😁

@@ -69,11 +67,13 @@
sky_prefixes = list(sky_prefixes)
spec_prefixes = list(spec_names.keys())

field_from_unit = {"Jy":"intensity",
"K":"temperature"}
field_from_unit = {"Jy": "intensity",
Copy link
Member

Choose a reason for hiding this comment

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

Do we ever expect these to be imported outside of this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, I'll fix that.

@@ -92,7 +93,7 @@ def __init__(self,ds,dataset_type='fits'):
self.dataset_type = dataset_type
self.field_indexes = {}
self.dataset = weakref.proxy(ds)
# for now, the index file is the dataset!
# for now, the index file is the dataset
Copy link
Member

Choose a reason for hiding this comment

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

:inception_noise:

if field_units[0] == "/":
field_units = "1%s" % field_units
return field_units
try:
Copy link
Member

Choose a reason for hiding this comment

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

Is there any way we could flatten this?

Copy link
Member

Choose a reason for hiding this comment

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

If possible, flattening this would make it a bit more readable!

@@ -410,21 +402,22 @@ def _set_code_unit_attributes(self):
length_factor = self.wcs.wcs.cdelt[0]
length_unit = str(file_units[0])
mylog.info("Found length units of %s." % (length_unit))
else:
elif "length_unit" not in self.units_override:
Copy link
Member

Choose a reason for hiding this comment

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

Any chance we'll need a final else block?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add one. Thanks for catching this.

Copy link
Member

@munkm munkm left a comment

Choose a reason for hiding this comment

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

Overall, this looks great! I noticed the YTEP still has the old PR (not the one you rebased with 4.0) linked to it for management. Maybe include this PR in that too?

]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A 3D FITS cube can also be created from a covering grid:"
"A 3D FITS cube can also be created from regularly gridded 3D data. In yt, there are covering grids and \"arbitrary grids\". The easiest way to make a grid is using `ds.r`:"
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 that the transition from mentioning yt's covering grids and arbitrary grids, and then saying that a user should make a grid with ds.r() is a little bit confusing. Maybe a transition sentence that the ds.r() is creating an ArbitraryGrid object based on the boundaries you're passing it would help?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good

@@ -992,7 +989,7 @@ zero, and the left and right edges of the domain along this axis are
Examples of Using FITS Data
^^^^^^^^^^^^^^^^^^^^^^^^^^^

The following IPython notebooks show examples of working with FITS data in yt,
The following Jupyter notebooks show examples of working with FITS data in yt,
which we recommend you look at in the following order:

* :ref:`radio_cubes`
Copy link
Member

Choose a reason for hiding this comment

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

Do you also want to add a link to the FITSImageData notebook in /visualizing/ here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good

import astropy.io.fits as pyfits
f = pyfits.open("xray_flux_image.fits", mode="update")
from astropy.io import fits
f = fits.open("xray_flux_image.fits", mode="update")
f[0].header["BUNIT"] = "cts/s/pixel"
Copy link
Member

Choose a reason for hiding this comment

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

In the notebook you showed updating header values with the update_header() method. Can that be used to do the same thing as the code block here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is actually a different situation where we show how to make FITS files more self-descripting so yt can read them properly.

Copy link
Member

Choose a reason for hiding this comment

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

Ah ok! That makes sense! Is there any threshold of requirements that yt needs to read FITS files properly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really a threshold, it's more that the more self-descripting your data is, the better it is supported. Even data that's not very self-descripting can still be loaded.

@@ -833,7 +847,7 @@ The third way is if auxiliary files are included along with the main file, like

.. code-block:: python

ds = load("flux.fits", auxiliary_files=["temp.fits","metal.fits"])
ds = yt.load("flux.fits", auxiliary_files=["temp.fits","metal.fits"])
Copy link
Member

Choose a reason for hiding this comment

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

Just checking: this is an example of a case where there might be auxiliary files. We don't have any datasets in the FITS frontend that have these, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@munkm we don't, no. I don't think I have any on hand at the moment, actually.

Copy link
Member

Choose a reason for hiding this comment

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

That's ok! I just wanted to check that I shouldn't be able to run it with any files that I currently have locally.

@@ -87,10 +91,12 @@ def test_fits_image():
cut = ds.cutting([0.1, 0.2, -0.9], [0.5, 0.42, 0.6])
cut_frb = cut.to_frb((0.5, "unitary"), 128)

fid3 = FITSImageData(cut_frb, fields=[("gas","density"), ds.fields.gas.temperature], units="cm")
fid3 = cut_frb.to_fits_data(fields=[("gas","density"),
Copy link
Member

Choose a reason for hiding this comment

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

Maybe change fields=[("gas","density"), to add a space between the fields, like fields=[("gas", "density"), here and to the fields in the fits_cut three lines down?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep!

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.

One totally minor comment, respond or ignore as you like. I'm happy for this to be merged.


# We need to have a bunch of species fields here, too
("metal_density", ("code_mass/code_length**3", ["metal_density"], None)),
("hi_density", ("code_mass/code_length**3", ["hi_density"], None)),
Copy link
Member

Choose a reason for hiding this comment

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

Do you want to also add the yt standard alias names (as per YTEP 3) for these species fields, so for example, 'H_p0_density`?

Copy link
Member

Choose a reason for hiding this comment

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

I talked to @jzuhone about this and he's going to address it in a different PR.

@munkm munkm merged commit 099ac4a into yt-project:yt-4.0 May 29, 2019
@jzuhone jzuhone deleted the fits branch May 29, 2019 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants