ATOC 4815/5815

Midterm Post-Test

Will Chapman

CU Boulder ATOC

2026-01-01

Midterm Post-Test

The Three Things That Got You

Let’s talk about what tripped people up.

  • Two topics: nested loops, why integration needs a loop

1. The Date

Problem Most Missed on the Entire Exam

“What is today’s date?”

2. Nested For Loops

The Question

Using two nested for loops over row and column indices, print each entry of temp_matrix in the format temp_matrix[ii, jj] = value for all valid ii and jj, in row-major order.

(Do not use flatten(), ravel(), np.nditer, or vectorized printing.)

Setup:

import numpy as np
temp_matrix = np.array([[15.2, 18.7, 22.1],
                        [14.8, 17.3, 21.5]])
print(f"Shape: {temp_matrix.shape}")  # (2, 3) — 2 rows, 3 columns
Shape: (2, 3)

The Common Mistake

Many of you wrote something like:

# WRONG — iterates over rows, not indices
for row in temp_matrix:
    for val in row:
        print(val)

What this does:

  • Prints the values: 15.2, 18.7, 22.1, …
  • But you don’t have ii and jj to format the output!
  • You’re iterating over elements, not indices

The question asked for: temp_matrix[ii, jj] = value

You need the indices, not just the values.

The Correct Answer

n_rows, n_cols = temp_matrix.shape

for ii in range(n_rows):
    for jj in range(n_cols):
        print(f"temp_matrix[{ii}, {jj}] = {temp_matrix[ii, jj]}")
temp_matrix[0, 0] = 15.2
temp_matrix[0, 1] = 18.7
temp_matrix[0, 2] = 22.1
temp_matrix[1, 0] = 14.8
temp_matrix[1, 1] = 17.3
temp_matrix[1, 2] = 21.5

Why This Works

  1. temp_matrix.shape gives (2, 3) — unpack into n_rows and n_cols
  2. Outer loop for ii in range(n_rows) walks through rows: 0, 1
  3. Inner loop for jj in range(n_cols) walks through columns: 0, 1, 2
  4. temp_matrix[ii, jj] grabs the element at row ii, column jj
  5. Inner loop completes all columns before outer loop moves to next row — row-major order

The Pattern to Memorize

Looping over indices (when you need ii, jj):

for ii in range(n_rows):        # outer = rows
    for jj in range(n_cols):    # inner = columns
        do_something(array[ii, jj])

Looping over values (when you just need the numbers):

for row in array:
    for val in row:
        do_something(val)

Know the difference. The question tells you which one to use.

  • “loop over indices” → range(shape) + array[ii, jj]
  • “loop over elements/values” → for val in array

Practice: Try It Now

Given:

wind_data = np.array([[5.2, 3.1, 8.7, 2.4],
                       [6.1, 4.5, 7.3, 3.8],
                       [4.9, 2.8, 9.1, 5.0]])

Write nested loops to print every element where wind speed exceeds 5.0:

wind_data[0, 0] = 5.2 --> WINDY
wind_data[0, 2] = 8.7 --> WINDY
...

Solution:

n_rows, n_cols = wind_data.shape

for ii in range(n_rows):
    for jj in range(n_cols):
        if wind_data[ii, jj] > 5.0:
            print(f"wind_data[{ii}, {jj}] = {wind_data[ii, jj]} --> WINDY")
wind_data[0, 0] = 5.2 --> WINDY
wind_data[0, 2] = 8.7 --> WINDY
wind_data[1, 0] = 6.1 --> WINDY
wind_data[1, 2] = 7.3 --> WINDY
wind_data[2, 2] = 9.1 --> WINDY

3. Why Integration Needs a Loop

The Question

In the Lorenz63 class, the integrate() method uses a loop to advance the model forward in time. Explain in 1-2 sentences why we need a loop for time integration.

The common (wrong) answer:

“Because we have many time steps and need to repeat the calculation.”

This is too vague. You could say the same thing about converting 1000 temperatures from Celsius to Fahrenheit — but that doesn’t need a loop (use NumPy!).

What makes time integration different?

The Correct Answer

Each step depends on the previous step. To compute \(y_{n+1}\), we need \(y_n\), which itself came from \(y_{n-1}\). This is a sequential dependency — we cannot compute step 100 without first computing steps 1 through 99.

Independent — no loop needed:

# Every element can be computed on its own
temps_f = temps_c * 9/5 + 32

Sequential — loop required:

# Each step needs the RESULT of the previous step
for i in range(n_steps):
    state = state + tendency(state) * dt

The state on the right side is the output of the previous iteration. You can’t skip ahead.

Sequential vs. Independent

Operation Independent? Loop needed?
Convert 1000 temps C → F Yes No — vectorize!
Compute mean of each row Yes No — axis=1
Euler time stepping No\(y_{n+1}\) needs \(y_n\) Yes
Running sum / cumulative No — each sum needs the previous Yes

The rule: If step \(n+1\) depends on the result of step \(n\), you must use a loop.

Visualizing the Dependency

Independent (vectorize!):

    x₀ → f(x₀)       Each computation stands alone.
    x₁ → f(x₁)       You can do them all at once.
    x₂ → f(x₂)       NumPy handles this in C.
    x₃ → f(x₃)


Sequential (loop required):

    y₀ ──▶ y₁ ──▶ y₂ ──▶ y₃ ──▶ ...
      Euler   Euler   Euler   Euler

    Each arrow REQUIRES the previous value to exist.
    You can't skip ahead. This is a sequential chain.

This is the defining feature of time integration, and it’s why we can’t just vectorize the whole thing in one NumPy call.

Summary

What to Remember

Concept What went wrong What to remember
The date You forgot it existed the date
Nested loops Iterated over values instead of indices for ii in range(n_rows): for jj in range(n_cols): array[ii, jj]
Why loops for integration Answers were too vague \(y_{n+1}\) depends on \(y_n\), sequential dependencies require a loop