A walkthrough of one file

You downloaded this. Here is what it is.

6 min of one satellite pass, holding the ground position of every pixel the instrument sampled — and nothing else.

VNP03IMG.A2026214.2048.002.2026215033340.nc

When20:48 UTC02 Aug 2026
Processing levelLevel 1Calibrated and located
LayoutSwathone pass, as the instrument scanned it
What is inside37 arrays32 of them are data

Nothing below assumes you have worked with satellite data before. Terms with a dotted underline carry their own definition. A panel on a tinted background was read out of your file or its name; a panel with an orange edge is an inference this guide made, flagged so you can check it.

Start
01

First, the unit you are holding

One file is a slice, not a dataset

A dataset is the record. A granule is one file out of it. Almost every confusing thing about Earthdata follows from that distinction.

NASA does not hand you a dataset. It hands you granules — the smallest slice it is willing to distribute. Download a year of this product and you have 87,600 files that are individually meaningless and collectively a scientific record.

This is the first thing that trips people up. You go looking for data and you get a file with a name like the one above, which is one slice out of a record running for years. The collection identifier names the whole record; the granule is one slice of it.

in your file

This granule covers 20:48:00 to 20:54:00 UTC on 02 August 2026 — 6 min.

00:0006:0012:0018:0024:00

240 granules make up one day of this product. Yours is the marked sliver.

Collection
VNP03IMG
Version
C2
The full record
One file every six minutes of orbit, around 240 granules a day per satellite. Suomi-NPP launched in late 2011; the archived record begins in January 2012, with NOAA-20 and NOAA-21 joining later.
not read from the file

The filename gives a start time but no end time. The 6-minute length is a fixed property of this product, so this guide supplied it — it was not read from anywhere. Confirm it against the product user guide, and note the file itself may carry time_coverage_end as a global attribute, which beats anything computed.

02

Second, the container

What kind of file is this, really?

HDF5, netCDF-4, HDF4 and GeoTIFF are containers, not data. Which one you have decides which software will open it.

The file extension is a label someone typed. It is not enforced by anything, and renaming a file does not change what is inside it. So the honest first question is: what container is this actually written in, and what will open it?

HDF5

yours

A filesystem inside a single file.

Holds folders (groups), arrays (datasets), and labelled notes attached to either (attributes). One file can carry a dozen variables, their coordinate axes, and a paragraph describing each, with no separate header to lose.

Opened by h5py, xarray, Panoply, HDFView, MATLAB, IDL

netCDF-4

HDF5 with the rules turned on.

Every netCDF-4 file is valid HDF5. The reverse is not true. netCDF-4 forbids some HDF5 features and requires coordinate axes to be declared a particular way, which is what lets xarray open a file and immediately know which dimension is time.

Opened by xarray, netCDF4-python, NCO, CDO, Panoply, QGIS

netCDF

The classic, flat version.

No groups, no compression, no nesting. Simple and extremely portable. Common for model output and station records.

Opened by netCDF4-python, xarray, NCO, CDO

HDF4 (legacy)

The previous generation. Not compatible with HDF5.

Despite the name, HDF4 and HDF5 are different formats with different libraries. Older MODIS and AIRS products still ship this way. If h5py refuses a .hdf file, this is why.

Opened by pyhdf, GDAL, HDFView

GeoTIFF

An image that knows where it is.

A raster with a coordinate reference system baked into its tags. Excellent in GIS software, awkward for anything with a time dimension.

Opened by GDAL, rasterio, QGIS, ArcGIS

HDF5
netCDF-4

Every netCDF-4 file is a valid HDF5 file. The reverse is not true, which is why a file can be perfectly well formed and still refuse to open in xarray.

in your file

Your file was opened twice: once through the HDF5 library and once through the netCDF-4 library. Both worked, which means xarray, Panoply, QGIS and the HDF5 tools will all read it without special handling.

03

Third, and most important

This is a pass, not a map

Level 1 means the data is still organised the way the instrument collected it. That shapes everything you can and cannot do with it.

Here is the idea that changes how you read every array in this file.

This is not a map. It is a record of one pass, laid out the way the instrument scanned it.

The satellite orbits pole to pole while the Earth turns underneath. A mirror sweeps side to side, and each sweep writes one row into the array. So the rows are consecutive sweeps and the columns are positions along a sweep. Neither axis is latitude and neither is longitude. The strip of ground this traces out is called a swath, and it is curved, tilted, and different on every orbit.

one pass over the ground the array you are handed columns: position along a sweep

The curved strip on the left becomes the rectangle on the right. The rectangle is a convenience of storage, not a geography.

NASA labels how far a product has travelled from the raw signal. The number appears in the filename of almost every Earthdata product, and it is the fastest way to know what you are dealing with.

1724089113362205441782911687raw counts, in the order the instrument sent them

Level 0 — Raw

Instrument counts, straight off the downlink. Nobody outside the mission team works with these.

the same counts, now in physical units and pinned to the groundstill one pass, still curved

Level 1 — Calibrated and located

Counts converted to physical units and tagged with where and when the sensor was looking. Still organised by satellite pass, not by map.

a real one you could downloadVNP02IMG / VNP03IMG This pair. Calibrated radiances, and the ground position of every pixel that produced them.

each sample turned into a quantity — rain rate, temperature, fireeverything off the track is still unknown

Level 2 — Retrieved

A geophysical quantity derived from Level 1, still laid out along the satellite's own ground track.

a real one you could downloadVNP14IMG Active fire detections, derived from those radiances and still laid out along the same swath.

one regular grid, no holessolid cells were observed; pale cells were filled in by the algorithm

Level 3 — Gridded

Level 2 from many passes and many satellites, resampled onto one regular grid over one fixed time window.

a real one you could downloadVNP13A1 Vegetation indices, resampled onto a fixed tiled grid and composited over sixteen days.

a model has taken over; values now come from physics, not observation

Level 4 — Modelled

Output of a model that ingested observations. No longer a measurement of anything.

a real one you could downloadnone in this suite Level 4 is model output. The VIIRS land suite stops at Level 3; VIIRS observations reach Level 4 only by being ingested into models such as reanalyses.

Every panel shows the same stretch of ground. Step through the rungs to watch one satellite pass become a map.

what this means for you

This file contains no measurement. Not a temperature, not a reflectance, nothing the instrument sensed. What it contains is where every pixel was — the latitude, longitude and ground elevation under each detector reading, plus the angles between the pixel, the satellite and the sun.

Geolocation is separated from measurement on purpose. One geolocation file serves every band file from the same six minutes, so the coordinates are stored once instead of sixteen times. The cost is that a measurement file alone is unusable: you need both, matched on the acquisition timestamp in the filename.

The practical consequence is that this file has to carry a latitude and a longitude for every single pixel, as two full-size arrays. A gridded product gets away with two short vectors — one list of latitudes, one of longitudes — because its geometry is regular. Here there is no regularity to exploit and no vector that could describe it.

It also means you cannot crop by index to get a region. Rows near the top of the array might be over Greenland or over the Pacific depending on the orbit. To find your area of interest you have to search the coordinate arrays themselves, or use the archive’s spatial search before you download anything.

04

Fourth, how it was made

Your file shows its working

Every array in here is the output of a step. Knowing the steps is how you know what the numbers mean.

This file contains no measurement. Not a temperature, not a reflectance, nothing the instrument sensed. What it contains is where every pixel was — the latitude, longitude and ground elevation under each detector reading, plus the angles between the pixel, the satellite and the sun.

This is the part worth slowing down for, because the steps below are not background reading — several of them left their output in your file, in the arrays named beside them. Most people take the headline variable and never look. But if you want to know why a value is what it is, the working is right there.

  1. Spacecraft ephemeris and attitude

    Where the satellite was and which way it was pointing, from onboard GPS and star trackers, at every moment of the scan.

  2. Scan geometry

    VIIRS sweeps a mirror across the track. Knowing the mirror angle and the detector layout turns “detector 12 of scan 340” into a direction in space.

  3. Ellipsoid intersection

    That direction is projected down until it meets a smooth mathematical model of the Earth. This gives a first latitude and longitude — correct at sea level, wrong everywhere else.

  4. Terrain correction

    The real ground is not smooth. Looking at a mountain from an angle, the peak appears displaced from where the ellipsoid calculation put it — the same parallax that makes your thumb jump when you close one eye. This step intersects the view direction with an actual elevation model instead. The “terrain-corrected” in the product name is this step, and it is why this file exists rather than the uncorrected one.

    visible in your file asheight

  5. Angle computation

    Finally, the four angles that every downstream correction needs: where the satellite was relative to the pixel, and where the sun was.

    visible in your file assensor_zenithsensor_azimuthsolar_zenithsolar_azimuth

05

Fifth, the label on the outside

The filename is a fingerprint

Every character is doing a job. Nothing in the name is arbitrary and nothing repeats by accident.

An Earthdata filename is not decoration. It is a structured record, and it is the only part of the file you can read without opening anything. Once you can decode it you can sort, filter and sanity-check a directory of ten thousand granules from the shell.

The catch: the key to reading it is not inside the file. It lives in a specification document on a different server, nowhere linked from the download page. That is the gap this whole exercise exists to close.

VNP03IMGProduct
2026214Acquisition date
2048Acquisition time
002Collection
2026215033340Production date-time
ncExtension

Product VNP03IMG

VNP means VIIRS aboard Suomi-NPP; VJ1 and VJ2 are the same instrument on NOAA-20 and NOAA-21. The two digits are the product number inherited from MODIS: 02 is calibrated radiance, 03 is geolocation. IMG selects the 375 m imagery bands; MOD would select the 750 m moderate bands.

Acquisition date 2026214

The A prefix marks this as the acquisition date, and the digits are year plus day-of-year rather than a calendar date. 2026214 is the 214th day of 2026. Day-of-year sorts correctly as plain text and never argues about month lengths, which is why every MODIS-derived product uses it.

Acquisition time 2048

Hours and minutes UTC when the granule starts. Note there is no end time here: the granule length is a fixed property of the product, not something the name tells you.

Collection 002

Which reprocessing of the whole archive this came from. When the science team improves the algorithm they reprocess everything and increment this. Files from different collections are not interchangeable and should never be mixed in one analysis.

Production date-time 2026215033340

When this particular file was generated, in the same year-plus-day-of-year form with seconds appended. This is the second date in the name, and mistaking it for the acquisition date is the classic error — it can be days or years later.

Extension nc

The claimed container format.

Hover or tap a piece of the filename above.

in your file

Day 214 of 2026 is 02 August. In Python that is datetime.strptime("2026214", "%Y%j") — the %j is the whole trick.

Two dates, and they are not the same date. The observation happened 02 August 2026 at 20:48. The file was produced 03 August 2026 at 03:33:40, 6 h 45 min 40 s later. Sort a directory by the wrong one and you get a plausible-looking, completely wrong time series — and no error anywhere to tell you.

06

Sixth, the stamp inside

The extension can lie; the opening bytes cannot

Renaming a file changes its label and nothing else. Formats identify themselves from the inside.

Every file format stamps its identity into its opening bytes. The stamp is written by whatever program created the file, and unlike the extension, it does not survive being renamed. Reading it is how a program knows what it is holding before it commits to a parser.

89‹‹
48H
44D
46F
0DCR
0ALF
1ASUB
0ALF

The opening bytes this format always writes, regardless of what is inside.

Bytes two through four spell HDF in plain ASCII, so a damaged file often still announces what it was trying to be. The bytes either side are deliberate traps: they are chosen to get mangled if the file crosses a connection that “helpfully” converts line endings, which turns a silent corruption into an obvious one.

in your file

The extension claimed netCDF. The opening bytes say HDF5. Software that trusts the name will fail to open this.

07

Seventh, what else you need

One file is rarely the whole answer

Products come in families. The one you downloaded may be a piece of a set, and useless on its own.

This file does not stand alone. NASA splits products along lines that make sense to the production system and are invisible from the download page. Knowing which sibling you actually want saves more time than any amount of clever code.

VNP02IMG

The measurement half. Same six minutes, same pixels, but holding calibrated radiances and reflectances. You need both files to make a map of anything.

VNP03MOD

The same geolocation for the 750 m moderate bands. Different pixel grid, so it is not interchangeable with this one.

VNP03IMGLL

The 'light' version: latitude, longitude and terrain height only, if you do not need the viewing and solar angles.

Distributed by LAADS DAAC rather than GES DISC, which is worth knowing because the two archives have different search interfaces and different download tooling even though both sit behind the same Earthdata login.

08

Opening it up

Inside is a folder tree

Groups, arrays and notes. Once you can see the shape of it, the file stops being opaque.

Open the container and you find something laid out like the folders on your computer. A group is a folder. Inside it are arrays. Attached to each array are attributes — short labelled notes saying what the array is and what to watch out for.

in your file

37 arrays in /geolocation_data and /navigation_data and /scan_line_attributes: 32 data, 5 axes, 0 bounds.

ArrayKind TypeShapeUnitsDescriptionFill
height /geolocation_data data int166464 × 6400 meters Terrain height at pixel locationspacked -32768
land_water_mask /geolocation_data data uint86464 × 6400 Land/Water mask at pixel locations 255
latitude /geolocation_data data float326464 × 6400 degrees_north Latitudes of pixel locations -999.9
longitude /geolocation_data data float326464 × 6400 degrees_east Longitudes of pixel locations -999.9
quality_flag /geolocation_data data uint86464 × 6400 Geolocation pixel quality flags
range /geolocation_data data int166464 × 6400 meters Satellite-to-pixel rangepacked -32768
sensor_azimuth /geolocation_data data int166464 × 6400 degrees Sensor azimuth angle at pixel locationspacked -32768
sensor_zenith /geolocation_data data int166464 × 6400 degrees Sensor zenith angle at pixel locationspacked -32768
solar_azimuth /geolocation_data data int166464 × 6400 degrees Solar azimuth angle at pixel locationspacked -32768
solar_zenith /geolocation_data data int166464 × 6400 degrees Solar zenith angle at pixel locationspacked -32768
att_ang_end /navigation_data data float32202 × 3 degrees Attitude angles (roll, pitch, yaw) at EV end-times -999.9
att_ang_mid /navigation_data data float32202 × 3 degrees Attitude angles (roll, pitch, yaw) at EV mid-times -999.9
att_ang_start /navigation_data data float32202 × 3 degrees Attitude angles (roll, pitch, yaw) at EV start-times -999.9
att_quat_ev /navigation_data data float32202 × 4 Attitude quaternions at EV mid-times (J2000 to spacecraft) -999.9
earth_moon_distance /navigation_data data float32202 meters Earth-Moon distance -999.9
earth_sun_distance /navigation_data data float32202 AU Earth-Sun distance -999.9
lunar_inst /navigation_data data float32202 × 3 Lunar unit vectors in VIIRS frame -999.9
lunar_j2000 /navigation_data data float32202 × 3 Lunar unit vectors in J2000 frame -999.9
orb_pos_ev_end /navigation_data data float32202 × 3 meters Orbit position vectors at EV end-times (ECR) -1e+07
orb_pos_ev_mid /navigation_data data float32202 × 3 meters Orbit position vectors at EV mid-times (ECR) -1e+07
orb_pos_ev_start /navigation_data data float32202 × 3 meters Orbit position vectors at EV start-times (ECR) -1e+07
orb_vel_ev_end /navigation_data data float32202 × 3 meters/second Orbit velocity vectors at EV end-times (ECR) -1e+07
orb_vel_ev_mid /navigation_data data float32202 × 3 meters/second Orbit velocity vectors at EV mid-times (ECR) -1e+07
orb_vel_ev_start /navigation_data data float32202 × 3 meters/second Orbit velocity vectors at EV start-times (ECR) -1e+07
solar_inst /navigation_data data float32202 × 3 Solar unit vectors in VIIRS frame -999.9
solar_j2000 /navigation_data data float32202 × 3 Solar unit vectors in J2000 frame -999.9
number_of_lines / axis >f46464
number_of_pixels / axis >f46400
number_of_scans / axis >f4202
quaternion_elements / axis >f44
HAM_side /scan_line_attributes data uint8202 Half-angle mirror side 255
ev_mid_time /scan_line_attributes data float64202 seconds Earth view mid time (TAI93) -999.9
scan_end_time /scan_line_attributes data float64202 seconds Scan end time (TAI93) -999.9
scan_quality /scan_line_attributes data int16202 Geolocation scan quality flags -999
scan_start_time /scan_line_attributes data float64202 seconds Scan start time (TAI93) -999.9
sensor_mode /scan_line_attributes data uint8202 VIIRS sensor mode 255
vector_elements / axis >f43
09

The notes on the arrays

The sticky notes are the documentation

Units, descriptions, missing-data markers. This is the metadata everyone tells you to read, and here is what is in it.

The notes attached to an array are where a self-describing file earns the name. They are also where the traps are, because a note that is missing does not raise an error — software just carries on without it.

in your file

Every note attached to /geolocation_data/height, the largest data array in this file.

This array is packed. The numbers on disk are small integers, and you must multiply by scale_factor and add add_offset to get real values. xarray and netCDF4-python do this for you; h5py does not. If your values come out as implausible five-digit integers, this is why.

NoteValue What it is for
_Netcdf4Coordinates[1, 2]no standard meaning — product-specific
long_nameTerrain height at pixel locationsA sentence describing the variable, written for a human.
unitsmetersWhat the numbers are measured in. Without it, 3.2 is just a number.
_FillValue-32768The number written into cells with no estimate. Not a measurement; remove it before doing arithmetic.
valid_min-1000Smallest value considered real data.
valid_max10000Largest value considered real data.
scale_factor1.0Multiply the stored integers by this to get real values.
add_offset0.0Add this after multiplying by scale_factor.
DIMENSION_LIST<2 HDF5 internal reference(s)>no standard meaning — product-specific
10

The one that catches everybody

Adjacent cells in the array are not adjacent on the ground

Every product has a way of being silently wrong. This is this product’s.

VIIRS scans by sweeping a mirror side to side. Near the middle of the sweep the footprints line up neatly. Toward the edges, where the instrument is looking out at a shallow angle, each footprint stretches and consecutive scans start to overlap — a pattern called the bow-tie effect. VIIRS suppresses it onboard — aggregating detectors and deleting the most-overlapped edge pixels — but it does not vanish.

The consequence for you: the array is a rectangle, but the ground it covers is not. Two neighbouring cells near the swath edge may cover overlapping patches of ground, and some ground is covered twice. Treating the array as if it were a regular grid — taking a mean over a box, or handing it to something that assumes even spacing — double-counts. This is why the latitude and longitude arrive as full-size arrays rather than as two coordinate vectors: there is no vector that describes this geometry.

What the average becomes if you forget

Drag to change how much of the array has no estimate. Assumes a true mean of 0.12 across the cells that do have one.

Reported by a.mean() −4,000
After masking 0.12

No error is raised. No warning appears. You get a number and it has the right units.

in your file

31 data arrays declare a fill value. Here is what catches people who did everything else right:

What the documentation says -999.9
What is actually stored -999.9000244140625

These are 32-bit floating point numbers, and 32 bits cannot represent -999.9 exactly. Writing a == -999.9 matches nothing, masks nothing, and reports success. Compare with a tolerance instead.

fill = ds[path].attrs['_FillValue']
a = ds[path][:]
a = np.where(np.isclose(a, fill, rtol=0, atol=0.01), np.nan, a)
mean = np.nanmean(a)

No variable declares a valid_min or valid_max either, so you cannot mask by asking for “everything in the sensible range.” The exact fill value is the only handle available.

11

Onto a real map

The map program shows you nothing — correctly

Without a projection, GIS renders the array as-is. That blank, unlocated rectangle is the honest answer.

Sooner or later this file meets a map program, and for a swath product the result is the whole lesson of chapter three made visible: a rectangle in index space, with no projection and no coordinate system, because the array does not know where it is. The knowledge lives in the coordinate arrays — in this product’s case, in this very file.

what opens: the raw arrayindex space — no projection,no coastline anywhere near rightwhere those pixels actually areplaced by the geolocation arrays

The array on the left and the ground on the right are the same pixels.

Drag this granule — or its VNP02 measurement partner — into a map program and you get a rectangle in index space: no projection, no coordinate system, coastlines nowhere near where they belong. That is not a broken file. The coordinates live in this geolocation file as full-size arrays, and mainstream tools can use them: GDAL calls them geolocation arrays (gdalwarp -geoloc), and QGIS can build a georeferenced layer from the pair. One further trap on that route: these files are netCDF-4, and readers that assume rows run bottom-up will hand you an upside-down result — a known stumble with exactly this workflow.

in your file

The geolocation arrays are here: latitude and longitude, each 6464 × 6400 — one position per pixel. These are what a map program resamples against.

The rescue is that mainstream tools know this pattern. Point them at the coordinate arrays and they will resample the pass onto a real map — slower than opening a gridded product, but correct. What never works is pretending the array is already a map.

12

When

The clock does not start in 1970

Time is a count of seconds since a reference date, and the reference is rarely the one your language assumes.

Time in a science data file is almost never a date. It is a count of seconds since some reference moment, and the reference is written in the array’s units string. Read the wrong reference and every timestamp you produce is confidently, consistently wrong. The one everyone assumes is 1 January 1970, the Unix epoch. Earth science products frequently do not use it.

Two more things to internalise while you are here. Everything is UTC, never local — a granule labelled 20:48 covers the evening in Lagos and the afternoon in Denver. And every product picks a convention for its end stamp: the last instant inside the window, or the first instant of the next one. Get it wrong and you double-count or drop a slice everywhere two granules meet. Where this guide knows the product’s convention it has already applied it — the times and durations shown above describe the true window, and the filename chapter explains the raw stamp.

One more, specific to this product: the end of the granule’s window is not in the filename, so this guide computed it. If the file carries time_coverage_end as a global attribute, trust that over anything computed here.

13

House rules

Conventions, and which ones matter to you

Conformance checking sounds like paperwork. Some of it is. Some of it is the difference between a right and a wrong answer.

Now that you know what an attribute is, this will make sense. NASA publishes house rules — the DIWG recommendations — about how a data file should be built, so that software written by someone who has never seen your product can still read it. Alongside those sit the CF conventions, which fix the exact spelling of common attributes.

Files break these rules all the time, including files produced by NASA. That is not a scandal. What matters is which rule, because the consequences run from “a catalogue lists this slightly wrong” to “your average is off by three orders of magnitude.” Sorted that way, not by rule number:

Changes your answer

Reading the file the obvious way gives a wrong number, with no warning.

4.8

Fill value sits outside the valid range

Cannot be judged from this file

None of this file's variables say what their valid data range is, so there's nothing to compare the fill value against. This doesn't mean the file is wrong - it means the file gives you no way to confirm it's right. You still have to mask on the exact fill value, the way section 19 does, rather than on a range.

Breaks your tools

Something will refuse to open, refuse to decode, or run out of range.

2.1

Opens through the netCDF-4 interface

Follows the rule

This opens in both HDF5 and netCDF tools, so xarray, Panoply and most standard software will read it without special handling.

3.8

Standardised file extension

Breaks the rule

This file is named as if it were netCDF, but it is actually HDF5. Software that trusts the name will fail to open it.

Neither

Worth knowing if you catalogue or cite this data. Otherwise ignore.

2.2

CF-compliant attribute names

Bends the rule, allowably

Some variables don't declare a standard_name, so tools that look variables up by their CF standard meaning - rather than this file's own local name - won't find them. The variable still reads fine.

3.11

Date-time information in filenames

Follows the rule

The filename says this granule covers a 6-minute window starting 20:48 UTC on 02 Aug 2026. All times are UTC, not local.

3.9

Filename carries dataset + release identifiers

Follows the rule

The filename identifies which dataset, which version, and when - so files from different releases can't be confused. Whether it's genuinely unique across the whole archive can't be determined from one file.

4.10

Time stored at double precision

Does not apply

Nothing to check - no time variable.

One habit worth taking away: when a tool says a file is non-conformant, the useful question is never is this bad. It is what would I have got wrong if nobody had told me. Most of the time the answer is nothing. Occasionally it is everything.