Assignment 5#

Due: Friday Oct 9th at 11:59 pm ET

The goal of this assignment is to build a small, reproducible, cloud-native workflow that goes from a point on the map to a measured, correctly projected result, using Sentinel-2 imagery over Fiji. The thread that holds every step together is getting the coordinate reference system (CRS) right. You will write the CRS core by hand, then use an AI coding agent to build the cloud-native half, and your main job on the agent’s work is to catch the places where the CRS is wrong.

You will work at the 180 degree meridian (the antimeridian, or international date line), which is where CRS mistakes stop being subtle and become obvious.

What you are practicing

  • CRS fundamentals: datum, projection, units, and the EPSG, WKT, and PROJ representations.

  • Reprojection with PyProj and Shapely, and choosing an appropriate CRS for a measurement.

  • Cloud-native access: searching a STAC catalog and reading only the part of a COG you need over HTTP.

  • Judgment: recognizing a CRS bug in code that runs without error and returns a plausible-looking number.

Notebooks are for visualization, not logic

As in the previous assignment, your notebook must not contain your analysis functions. Every function you write, and every function the agent writes, lives in src/utils.py. The notebook imports those functions and uses them to explore, measure, and plot.

All work lives in a new directory named assignment-5/ in your geog313-assignments repository. Follow the same git discipline as before: stage files explicitly by name (never git add .), commit in small focused steps with clear messages, and push as you go. Read the whole assignment before you start.


Background: the antimeridian, and why Fiji#

A CRS does two jobs: it fixes a model of the Earth (the datum) and it defines how positions on that round Earth are written down, either as angles (longitude and latitude, as in EPSG:4326) or as projected coordinates in meters on a flat plane (as in a UTM zone). Most of the time you can measure distance or area badly by working in degrees and the answer is simply in the wrong units. At the antimeridian, something worse happens.

Longitude in EPSG:4326 runs from -180 to +180, and those two values are the same physical line. A shape that crosses it, such as the Fiji archipelago, has vertices near +179 on one side and near -179 on the other. Software that treats longitude as an ordinary number sees those vertices as almost 360 degrees apart, so the bounding box and the area of the shape blow up to span most of the planet. The numbers are not just in the wrong units, they are describing the wrong shape.

Fiji makes this concrete. The islands sit on the date line, so UTM zone 60S (which covers longitudes up to 180) meets UTM zone 1S (which begins just across 180). Picking the right CRS here is not automatic, and doing it wrong is easy to miss, which is exactly the point.


Project layout#

Your assignment-5/ directory must follow this layout:

assignment-5/
  pixi.toml            environment, dependencies, and tasks
  pixi.lock            locked versions (managed by pixi)
  .gitignore
  src/
    __init__.py        makes src an importable package
    utils.py           all analysis functions
  notebooks/
    analysis.ipynb     imports from src.utils; explores, measures, and plots only
  tests/
    test_utils.py      tests for the functions in src/utils.py
  results/             exported NDVI raster, figures, and metadata
  AGENTS.md            guidance for the agent
  README.md            what this is and how to reproduce it
  reflection.md        your written reflection (no AI)

Areas of Interest#

You do not need to download anything by hand. Use these two geometries, both in EPSG:4326, defined directly in your notebook.

A date-line-crossing region over the main Fiji islands, used only for the CRS reasoning in Part 1. Build a polygon from these corners, in longitude, latitude order:

(177.2, -16.1), (-178.3, -16.1), (-178.3, -19.2), (177.2, -19.2)

A small analysis area of interest (AOI) over Taveuni, used for the imagery work in Part 2, given as a bounding box (min_lon, min_lat, max_lon, max_lat):

(179.85, -16.95, 179.99, -16.80)

The AOI sits just west of 180, so it is squarely inside UTM zone 60S. Use its centroid as the search point where a single point is needed.


Part 0: Reproducible environment (5 pts)#

  1. From the root of your geog313-assignments repository, create the project assignment-5 and initialize it with pixi.

  2. Set the platforms in pixi.toml to ["osx-arm64", "osx-64", "linux-64"].

  3. Add these packages: pystac-client, rioxarray, rasterio, pyproj, shapely, cartopy, matplotlib, jupyterlab.

  4. Create the src/, notebooks/, tests/, and results/ structure above, add src/__init__.py, and write a README.md that says what the project does and gives the command(s) to reproduce it.

  5. Commit the scaffolding.


Part 1: The CRS core, by hand (40 pts)#

Do not use the agent for this part. You are strictly prohibited from using agentic coding here. Writing the CRS core yourself is what lets you judge the agent’s work in Part 2.

In src/utils.py, write and document these functions, then import and use them in the notebook:

  1. describe_crs(crs): given an EPSG code or a PyProj CRS, return (or print) the same CRS in its EPSG, WKT, and PROJ forms, together with its units and datum.

  2. utm_epsg(lon, lat): return the EPSG code of the UTM zone that contains the given point, with the correct hemisphere. Your function must return 32760 for a point at longitude 179.9 and 32701 for a point at longitude -179.9 (both in the southern hemisphere).

  3. reproject_geometry(geom, src_crs, dst_crs): reproject a Shapely geometry from one CRS to another using a PyProj Transformer. Pay attention to coordinate axis order.

  4. shift_to_360(geom): return the geometry with its longitudes rewritten to the 0 to 360 convention (a longitude of -178.3 becomes 181.7), so that a shape crossing the antimeridian becomes contiguous.

  5. projected_area_km2(geom, target_crs): reproject a geometry to a projected (meter-based) CRS and return its area in square kilometers.

In the notebook, use these functions to:

  • Show describe_crs for EPSG:4326 and for EPSG:32760, and note the difference in units and the shared datum.

  • Build the Fiji region polygon in EPSG:4326 and compute its bounding box and its area in square degrees. You should get a shape that spans almost the whole globe. Explain in one or two sentences what went wrong.

  • Apply shift_to_360 to the same region, recompute the bounding box, and confirm it is now only a few degrees wide.

  • Reproject the contiguous region to an equal-area CRS centered on the date line and report its true area in square kilometers. Use this PROJ definition for the target CRS:

    +proj=laea +lat_0=-17.5 +lon_0=180 +datum=WGS84 +units=m +no_defs
    
  • Report utm_epsg for the AOI centroid, note that it is zone 60S, and state which EPSG code a point just across 180 would get instead.

  • In 3 to 5 sentences, explain why EPSG:4326 fails at the antimeridian, and why this is a problem with the shape and not only with the units.

Commit src/utils.py and the current notebook.


Part 2: The agentic cloud-native pipeline (30 pts)#

Now use GitHub Copilot in Agent mode, guided by an AGENTS.md you write, to build the imagery pipeline. The agent adds functions to src/utils.py; your notebook imports them and visualizes the results. Review every change before accepting it. You own this code and these numbers.

2.1 Write your AGENTS.md#

Before prompting the agent, write AGENTS.md. At a minimum it should tell the agent: this is a pixi-only project; all analysis functions go in src/utils.py and must be pure with no plotting; the notebook is for visualization only; do not create new files or edit reflection.md; the domain rules (Earth Search STAC API, the sentinel-2-l2a collection, the AOI is in EPSG:4326 and must be reprojected to each scene’s own CRS before any pixels are read, read only the AOI window and never the whole scene); run tests with pixi run test; and a definition of done. Commit it.

2.2 Functions for the agent to add#

  • search_best_scene(point, date_range, max_cloud): search the Earth Search API (https://earth-search.aws.element84.com/v1) with pystac-client for sentinel-2-l2a items that intersect the point within the date range and below the cloud-cover limit, and return the least cloudy item.

  • clip_asset(item, asset_key, aoi_4326): reproject the AOI into the scene’s own CRS (its proj:code), open the named COG asset from its href with rioxarray, and read only the AOI window, not the whole scene.

  • ndvi(red, nir): compute the normalized difference vegetation index as a labeled array, handling nodata so the result stays within -1 and 1.

2.3 In the notebook, run and visualize#

  • Search for a recent, low-cloud scene over the AOI and print the chosen scene’s id, date, cloud cover, and CRS.

  • Clip the red and near-infrared assets to the AOI and compute NDVI.

  • Show the cloud-native payoff: time reading the small AOI window against reading the full asset, and report both.

  • Map NDVI with Cartopy, setting the projection’s central_longitude to 180 so the area near the date line renders correctly.

  • Export the NDVI result to results/ndvi.tif and write a short results/scene_metadata.json recording the scene id, date, cloud cover, scene CRS, and the AOI true area in square kilometers from Part 1.

Own the CRS

CRS bugs are the quiet kind: the code runs and prints a number. Watch for the agent clipping before it reprojects the AOI (an empty or wrong result), measuring area or distance in degrees, swapping longitude and latitude, or downloading a whole scene instead of a window. Document at least one such mistake you found and how you corrected it.


Part 3: Tests and quality assurance (15 pts)#

  1. Add pytest to the project and define a test task so the suite runs with pixi run test.

  2. Write tests in tests/test_utils.py on small inputs where you know the answer, including: utm_epsg returns 32760 at longitude 179.9 and 32701 at longitude -179.9 (southern hemisphere); reproject_geometry round-trips a point back to its starting coordinates within a small tolerance; shift_to_360 rewrites a negative longitude correctly; and ndvi stays within -1 and 1 on a small test array.

  3. The agent may help write tests, but verify each test is meaningful: it must fail if the function were wrong. Iterate until pixi run test passes.


Part 4: Reflection (10 pts)#

Write reflection.md yourself, without AI assistance (300 to 500 words):

  • The CRS mistake: describe the incorrect CRS handling the agent produced, how you noticed it, and how writing Part 1 by hand helped you catch it.

  • Context engineering: how did your AGENTS.md shape the agent’s behavior, and what would you tighten next time?

  • Cloud-native access: in one or two sentences, what did the windowed read versus full read timing show you, and why does it matter?


Deliverables#

Committed to your private geog313-assignments repository under assignment-5/, following the project layout above:

  • pixi.toml, pixi.lock, .gitignore.

  • src/utils.py with all analysis functions, and src/__init__.py.

  • notebooks/analysis.ipynb, executed, with all figures rendered on GitHub, importing from src.utils and containing visualization and measurement only.

  • tests/test_utils.py, with pixi run test passing.

  • results/ndvi.tif and results/scene_metadata.json.

  • AGENTS.md, README.md, and reflection.md (written without AI).

  • A commit history showing incremental, reviewed development.