Which of these worlds could have liquid water — and why does your test reject Earth?¶
EPS 88 · PyEarth. Open your own copy on DataHub: click here.
Astronomers have found six thousand planets around other stars. For most of them we know three numbers and nothing else: how hot the star is, how big it is, and how far out the planet orbits. Today you turn those three numbers into a temperature, and a temperature into a verdict.
Every place you write something opens with a pencil icon and the words Your turn, and is
followed by a cell that already has the shape of the answer written in, with ... wherever your
code goes. Replace every ..., run every cell so your answers and figures are
saved in the file, then download this notebook itself — the .ipynb file — and upload it
to Gradescope. Not a PDF: the marking reads your notebook, and a PDF cannot be read.
In JupyterLab: File ▸ Download, or right-click the file in the left-hand panel and choose
Download.
Two habits from the first minute. A cell runs when you press Shift+Enter, and the notebook remembers everything it has already run — so when something breaks and you cannot see why, Kernel → Restart Kernel and Run All Cells throws the memory away and rebuilds it from the top. That is never the wrong thing to do.
The question¶
Liquid water is the one thing every living system we know of needs, so the first question asked of any newly found planet is whether it could hold some. For most of the six thousand planets found around other stars we know three numbers: the star's temperature, the star's size, and the width of the planet's orbit.
That is enough to compute the temperature a planet would sit at if starlight were the only thing heating it. Today you write that calculation as a function, run it over three thousand worlds with a loop, and let an if statement sort them. Then you run the same test on Earth, whose answer we already know. Which of these worlds could have liquid water — and why does your test reject Earth?
What you'll be able to do¶
The science. Compute a planet's equilibrium temperature from three archive numbers, and measure how far it misses the real temperature on the three worlds where we know both.
The code. Loops that build a count or a list up as they go · if / elif / else ·
None, and why you cannot compare it with a number · writing your own function with def.
Eight places where you write something: five in class, three at home. Each is headed
Your turn, and the cell under it already has the shape of the answer written in, with ...
wherever your code goes. Replace every ...; a comment beside each one says what it is for. A
... left in place is not code, and the cell will not do what you expect until it is replaced.
- What temperature would starlight alone hold a world at?
- Does that test say Venus could have liquid water?
- How much of a world's warmth comes from its air, not its star?
- Which of three thousand real planets pass — and what does the archive not know about them?
Setup¶
Run the next cell once; you are not expected to follow it. It fetches the NASA Exoplanet Archive's list of confirmed planets and hands it back as six ordinary lists, one per column and all in the same order, so position 40 of each list is the same planet. (It uses pandas, the tables library we meet properly in the tables week.)
Every count printed in this notebook came from the copy stored with the course. The live archive grows and gets revised, so a slightly different number is the archive having moved, not you having made a mistake.
import pandas as pd
import matplotlib.pyplot as plt
# house style, set once, so every plot cell below holds only what matters
plt.rcParams.update({"figure.dpi": 110,
"axes.grid": True, "grid.alpha": 0.3, "axes.axisbelow": True})
CACHE = "https://raw.githubusercontent.com/AI4EPS/EPS88_PyEarth/main/data"
def load():
"""the NASA Exoplanet Archive's confirmed planets: live if the network is up, the copy stored with the course if not"""
# Ask the live archive first. If it is down, or you are offline, read the copy stored with
# the course instead, so the notebook still runs.
try:
return pd.read_csv("https://exoplanetarchive.ipac.caltech.edu/TAP/sync?query="
"select+pl_name,st_teff,st_rad,pl_orbsmax,pl_rade,pl_eqt"
"+from+ps+where+default_flag=1&format=csv")
except Exception as e:
print("live source unreachable, using the cached copy:", type(e).__name__)
return pd.read_csv(CACHE + "/" + "week02_exoplanets.csv")
archive = load()
archive = archive.astype(object).where(archive.notna(), None)
def column(name):
"""one column of the archive as an ordinary list, with None wherever it has no value"""
return list(archive[name])
planet_names = column("pl_name") # what the planet is called
star_temps = column("st_teff") # its star's surface temperature, in K
star_radii = column("st_rad") # its star's radius, in radii of the Sun
distances = column("pl_orbsmax") # the width of the planet's orbit, in AU
planet_radii = column("pl_rade") # the planet's radius, in radii of the Earth
archive_temps = column("pl_eqt") # the archive's own equilibrium temperature, where it has one
print("planets in the archive:", len(planet_names))
live source unreachable, using the cached copy: URLError
planets in the archive: 6354
1. What temperature would starlight alone hold a world at?¶
Before three thousand planets, three. Venus, Earth and Mars are the only planets whose surface temperature we know from having been there. Their numbers are below, from NASA's planetary fact sheets: five lists in the same order — Venus, Earth, Mars, going outwards from the Sun.
A for loop does the same thing once for each item in a list.
# from NASA's planetary fact sheets
worlds = ["Venus", "Earth", "Mars"]
sun_distances = [0.723, 1.000, 1.524] # width of the orbit, in AU
bond_albedos = [0.770, 0.306, 0.250] # fraction of sunlight reflected
surface_temps = [737.0, 288.0, 214.0] # measured surface temperature, in K
air_pressures = [92.0, 1.014, 0.006] # atmospheric pressure at the surface, in bar
for world in worlds:
print(world)
Venus Earth Mars
world is a name the loop invents: on the first pass it holds "Venus", then "Earth", then
"Mars", and the indented lines run once per item.
With five lists you need the position instead, so that the same position can be read out of
all of them — last week's trick, where the position of the largest magnitude also found the place
name. range(len(worlds)) is every position in the list: range(3) is 0, 1, 2.
for i in range(len(worlds)):
print(worlds[i], "orbits at", sun_distances[i], "AU and its surface sits at",
surface_temps[i], "K under", air_pressures[i], "bar of air")
Venus orbits at 0.723 AU and its surface sits at 737.0 K under 92.0 bar of air Earth orbits at 1.0 AU and its surface sits at 288.0 K under 1.014 bar of air Mars orbits at 1.524 AU and its surface sits at 214.0 K under 0.006 bar of air
Venus's air is 92 bar against Earth's 1.014 and Mars's 0.006 — hold on to that.
A loop can also build something up as it goes. Set a counter to 0 before the loop starts and add to it inside; same idea for a list — start
it empty and append inside. results.append(x) adds one item to the end of the list results.
✏️ Your turn 1¶
Sunlight thins out with distance: a planet twice as far from its star catches a quarter as much.
So the sunlight reaching a world, compared with Earth, is 1 / distance ** 2 — ** means "to the
power of", so distance ** 2 is the distance squared.
The cell below loops over the three worlds and appends each one's share of sunlight to
sunlight, rounded to two decimal places. Fill in the blank: the share itself, from that world's
sun_distances[i].
Use these names: sunlight.
sunlight = []
for i in range(len(worlds)):
sunlight.append(round(1 / sun_distances[i] ** 2, 2))
print("sunlight compared with Earth, for", worlds, "-", sunlight)
sunlight compared with Earth, for ['Venus', 'Earth', 'Mars'] - [1.91, 1.0, 0.43]
A planet catches starlight and warms up until it glows away, in the infrared, exactly as much heat as it catches. The temperature where the two balance is its equilibrium temperature.
Equilibrium temperature: the temperature a planet would sit at if starlight were the only thing heating it and it had no air.
The standard formula needs three numbers about the star and the orbit, plus the fraction of light that bounces off:
temperature = star_temp * (star_radius / (2 * distance)) ** 0.5 * (1 - albedo) ** 0.25
star_temp is the star's surface temperature in kelvin; star_radius / distance is how big the
star looks from the planet, both lengths in the same units, and the 2 is there because the
planet catches light on the disc facing its star but glows from its whole surface, four times
that area; ** 0.5 is a square root and ** 0.25 a fourth root. For the Sun: 5,772 K and 0.00465047 AU. Start with Earth, with albedo
left out entirely, as if the planet reflected nothing.
sun_temp = 5772 # K, the Sun's surface temperature
sun_radius_au = 0.00465047 # the Sun's radius, in AU
earth_temp = sun_temp * (sun_radius_au / (2 * 1.000)) ** 0.5
print("Earth, reflecting nothing and with no air:", round(earth_temp, 1), "K")
Earth, reflecting nothing and with no air: 278.3 K
✏️ Your turn 2¶
Do the same for all three worlds. The cell below is the range loop from section 1 with the
arithmetic from the cell above dropped inside it; fill in the two blanks — this world's distance
in place of the 1.000, and the temperature to print, rounded to one decimal place.
for i in range(len(worlds)):
temperature = sun_temp * (sun_radius_au / (2 * sun_distances[i])) ** 0.5
print(worlds[i], round(temperature, 1), "K")
Venus 327.3 K Earth 278.3 K Mars 225.5 K
2. Does that test say Venus could have liquid water?¶
Now turn a temperature into a verdict. Water is liquid between 273 K and 373 K at Earth's sea-level pressure, and that is the window this notebook uses throughout; it is a convention.
An if statement runs its indented block only when a comparison comes out true; elif
("else if") tries another comparison if the first failed, and else catches everything left. The
comparisons are <, >, <=, >=, == for "is equal to" and != for "is not". Two can be
chained: 273 <= temperature <= 373 is true inside the window.
Here is the whole test, over the three worlds.
for i in range(len(worlds)):
temperature = sun_temp * (sun_radius_au / (2 * sun_distances[i])) ** 0.5
if temperature < 273:
verdict = "too cold"
elif temperature > 373:
verdict = "too hot"
else:
verdict = "liquid water possible"
print(worlds[i], round(temperature, 1), "K -", verdict)
Venus 327.3 K - liquid water possible Earth 278.3 K - liquid water possible Mars 225.5 K - too cold
3. How much of a world's warmth comes from its air, not its star?¶
Two of the three pass, and one of them is Venus — whose surface is really 737 K, 364 degrees above boiling. Something is missing, and section 1 already put it on screen: 92 bar of air.
The formula also has a term we have not used yet.
Albedo: the fraction of the starlight falling on a world that it reflects straight back into space; only the rest is absorbed and turned into heat.
A world that reflects more keeps less and runs cooler: Mars reflects a quarter of what reaches
it, Earth 0.306, cloud-covered Venus 0.770 — the bond_albedos from section 1. In the formula
that is a factor of (1 - bond_albedos[i]) ** 0.25.
With each world's real albedo in, how many of the three end up inside the 273–373 K window: 0, 1, 2 or 3?
albedo_temps = []
for i in range(len(worlds)):
starlight = sun_temp * (sun_radius_au / (2 * sun_distances[i])) ** 0.5
albedo_temps.append(starlight * (1 - bond_albedos[i]) ** 0.25)
print(worlds[i], round(albedo_temps[i], 1), "K")
Venus 226.7 K Earth 254.0 K Mars 209.8 K
None of them. Venus falls to 226.7 K and Earth to 254.0 K — nineteen degrees below freezing — and Mars was never in. Run the test one way and it accepts Earth and Venus together; run it the other way and it rejects all three. It has not been right once.
So how wrong is it? We know what these three worlds are actually like.
✏️ Your turn 3¶
For each world, print its measured surface temperature, the temperature the test just gave it,
the difference between the two, and its air pressure. The cell below prints those four numbers on
one line per world; fill in gap, the measured temperature minus the test's.
Use these names: gap, for the difference.
for i in range(len(worlds)):
gap = surface_temps[i] - albedo_temps[i]
print(worlds[i], "really", surface_temps[i], "K, test says", round(albedo_temps[i], 1),
"K, gap", round(gap, 1), "K, under", air_pressures[i], "bar of air")
Venus really 737.0 K, test says 226.7 K, gap 510.3 K, under 92.0 bar of air Earth really 288.0 K, test says 254.0 K, gap 34.0 K, under 1.014 bar of air Mars really 214.0 K, test says 209.8 K, gap 4.2 K, under 0.006 bar of air
Mars, 0.006 bar of air: out by 4.2 K. Earth, 1.014 bar: out by 34.0 K. Venus, 92 bar: out by 510.3 K. Same formula, three thicknesses of air, and the error tracks the air. That difference has a name.
The greenhouse effect: the difference between the temperature a world would have with no air and the temperature it actually has.
Nobody defined it and then went looking for it: we computed what starlight alone would do, compared that with three thermometers, and the leftover came out in the order of how much air each world has. The figure below plots it — the diagonal is where a world with no air would sit, and each planet stands above it by exactly what its own air adds.
plt.figure(figsize=(7, 4.5))
plt.plot([195, 390], [195, 390], color="0.6", lw=1) # where a world with no air would sit
plt.scatter(albedo_temps, surface_temps, s=45)
plt.axvline(273, color="0.8")
plt.axvline(373, color="0.8")
for i in range(len(worlds)):
plt.text(albedo_temps[i] + 4, surface_temps[i], worlds[i])
plt.text(285, 720, "liquid water window")
plt.text(330, 305, "no air at all")
plt.xlabel("equilibrium temperature, at the world's own albedo (K)")
plt.ylabel("measured surface temperature (K)")
plt.title(f"Starlight alone against reality (n = {len(worlds)})")
plt.show()
4. Which of three thousand real planets pass — and what does the archive not know about them?¶
Three thousand planets are sitting in the setup cell, and running the test over all of them is a loop you can already write. First, one piece of housekeeping: you have typed that formula four times, and a fifth copy inside the loop would mean any correction had to be made in five places.
A function is the fix: write the recipe once, give it a name, and ask for it by name from
then on. def starts the definition, the names in brackets are what the function needs to be
given, and return hands one value back. The triple-quoted line just inside is a docstring —
what the function is for.
def equilibrium_temperature(star_temp, star_radius, distance, albedo):
"""the temperature starlight alone would hold a planet at, in kelvin"""
starlight = star_temp * (star_radius * sun_radius_au / (2 * distance)) ** 0.5
return starlight * (1 - albedo) ** 0.25
print("Earth at its own albedo:", round(equilibrium_temperature(5772, 1.0, 1.000, 0.306), 1), "K")
help(equilibrium_temperature)
Earth at its own albedo: 254.0 K
Help on function equilibrium_temperature in module __main__:
equilibrium_temperature(star_temp, star_radius, distance, albedo)
the temperature starlight alone would hold a planet at, in kelvin
Two things changed on the way in. star_radius is now in radii of the Sun, the unit the archive
uses — the function multiplies by sun_radius_au itself, so the Sun goes in as 1.0. And
albedo is an argument, so the same function answers the albedo-0 question and the albedo-0.306
question. help(equilibrium_temperature) printed the docstring back: that is what writing one
buys you.
Now the archive. Most of its rows are incomplete — different discovery methods measure different
things — and where the archive has no value, the setup cell put None in the list, Python's
word for "nothing here". None is not zero: it is the absence of a number, and Python refuses to
compare it with one — None < 1.6 is an error, not False. The test for it is is None (or
is not None), and and joins two conditions so that both have to hold.
The cell below keeps the planets that carry all three numbers the formula needs, calls the
function once on each, and checks our number against the archive's own pl_eqt where it has
one — abs(x) is the size of a difference, whichever way round it went. Four lists come out, in
step: name, temperature, radius, and the star's own temperature, which the homework asks about.
usable_names = []
usable_temps = []
usable_radii = []
usable_star_temps = []
compared = 0
agree = 0
hotter = 0
for i in range(len(planet_names)):
if star_temps[i] is not None and star_radii[i] is not None and distances[i] is not None:
temperature = equilibrium_temperature(star_temps[i], star_radii[i], distances[i], 0.0)
usable_names.append(planet_names[i])
usable_temps.append(temperature)
usable_radii.append(planet_radii[i])
usable_star_temps.append(star_temps[i])
if archive_temps[i] is not None:
compared = compared + 1
if abs(temperature - archive_temps[i]) < 10:
agree = agree + 1
elif temperature > archive_temps[i]:
hotter = hotter + 1
print("planets with all three numbers:", len(usable_names), "out of", len(planet_names))
print("the archive publishes its own temperature for", compared, "of them:", agree,
"agree with ours within 10 K, and", hotter, "of the rest are ours running hotter")
planets with all three numbers: 3101 out of 6354 the archive publishes its own temperature for 1525 of them: 952 agree with ours within 10 K, and 456 of the rest are ours running hotter
3,101 of 6,354 planets have all three numbers; the rest were never measured that way. Of the 1,525 published temperatures, 952 agree with ours inside 10 K, and most of the misses are ours running hotter — as an albedo of 0 would do; the published values come from many papers, each with its own assumptions. Our arithmetic is the field's arithmetic; section 3 already showed what that arithmetic leaves out.
One more number: the radius. At about 1.6 Earth radii, planets stop being rock and become small
versions of Neptune, with no ground under the air. So a planet inside the
temperature window is one of three things — rocky, too big to be rock, or the archive never
measured its radius — and None makes the third case unavoidable.
That is a second verdict from a different number, and a function can hand back two values at
once: return verdict, kind gives both, and v, k = check_planet(...) catches them in that
order.
✏️ Your turn 4¶
Write check_planet: it takes a temperature and a radius and returns two strings. The first is
the verdict on the temperature — "too cold" below 273 K, "too hot" above 373 K,
"liquid water possible" in between. The second is what the radius says the planet is —
"radius unknown" when the radius is None, "rocky" below 1.6, "too big to be rock"
otherwise.
The cell below has the shape: two if / elif / else chains and one return. Fill in the
four comparisons, the return, and the docstring; the two calls at the bottom — Earth at its own
albedo, and the first planet in the archive — are given.
Use these names: check_planet.
def check_planet(temperature, radius):
"""the verdict on a world this warm, and whether it is small enough to be rock"""
# 1. The first verdict, from the temperature alone: the window is water's own, freezing
# to boiling at Earth's sea-level pressure.
if temperature < 273:
verdict = "too cold"
elif temperature > 373:
verdict = "too hot"
else:
verdict = "liquid water possible"
# 2. The second verdict, from a different number entirely. `is None` has to be asked
# first: the archive never measured some of these radii, and comparing None with 1.6
# would stop the notebook with an error rather than give a wrong answer.
if radius is None:
kind = "radius unknown"
elif radius < 1.6:
kind = "rocky"
else:
kind = "too big to be rock"
return verdict, kind
print("Earth at its own albedo:", check_planet(albedo_temps[1], 1.0))
print(f"{usable_names[0]}:", check_planet(usable_temps[0], usable_radii[0]))
Earth at its own albedo: ('too cold', 'rocky')
TOI-4914 b: ('too hot', 'too big to be rock')
✏️ Your turn 5¶
Now the survey: call check_planet on every usable planet, count the ones in the window in
n_window, and sort those by what the radius said — the name into rocky_names for "rocky",
one more too_big for "too big to be rock", one more unknown_radius otherwise.
The cell below has the loop and the printout; fill in the blanks. You will need the printed list of candidates for the homework.
Use these names, because the self-check looks for them: n_window, unknown_radius,
rocky_names and too_big.
n_window = 0
unknown_radius = 0
too_big = 0
rocky_names = []
for i in range(len(usable_names)):
verdict, kind = check_planet(usable_temps[i], usable_radii[i])
if verdict == "liquid water possible":
n_window = n_window + 1
if kind == "rocky":
rocky_names.append(usable_names[i])
elif kind == "too big to be rock":
too_big = too_big + 1
else:
unknown_radius = unknown_radius + 1
print("inside the window:", n_window)
print(" of those, rocky:", len(rocky_names), " too big:", too_big,
" radius never measured:", unknown_radius)
for i in range(len(usable_names)):
if usable_names[i] in rocky_names:
print(f" {usable_names[i]}: {round(usable_temps[i], 1)} K, {usable_radii[i]} Earth radii")
inside the window: 189 of those, rocky: 12 too big: 75 radius never measured: 102 Kepler-438 b: 319.9 K, 1.12 Earth radii Kepler-1126 c: 333.9 K, 1.45 Earth radii K2-3 d: 305.2 K, 1.458 Earth radii TRAPPIST-1 c: 339.9 K, 1.097 Earth radii TRAPPIST-1 d: 286.3 K, 0.788 Earth radii TOI-700 e: 295.6 K, 0.953 Earth radii K2-72 e: 286.3 K, 1.29 Earth radii Kepler-296 e: 303.9 K, 1.53 Earth radii Kepler-395 c: 364.2 K, 1.32 Earth radii K2-72 c: 333.8 K, 1.16 Earth radii K2-3 c: 371.8 K, 1.582 Earth radii TOI-2095 c: 324.8 K, 1.33 Earth radii
assert unknown_radius > 0, "unknown_radius never moved — nothing reached the `radius unknown` case"
assert too_big > 0, "too_big never moved — check the string the elif compares kind with"
assert n_window == len(rocky_names) + too_big + unknown_radius, "every planet in the window is rocky, too big, or unmeasured — the three should add up to n_window"
assert rocky_names[0] in usable_names, "rocky_names should hold planet NAMES — append usable_names[i]"
assert 150 < n_window < 250, "a couple of hundred planets sit in the window — thousands means the temperature test is not being asked, a handful means the window is wrong"
assert len(rocky_names) < too_big < unknown_radius, "in the window, the unmeasured outnumber the too-big, which outnumber the rocky — check which count goes where"
print(f"✓ the survey — {n_window} planets in the window: {len(rocky_names)} rocky, "
f"{too_big} too big, {unknown_radius} with no measured radius")
✓ the survey — 189 planets in the window: 12 rocky, 75 too big, 102 with no measured radius
102 of the 189 have no published radius at all — more than half the answer. A test that silently counted the unknowns as "not rocky" would have reported the same 12 candidates and looked far more confident than the data allows.
The figure below is every one of the three thousand temperatures, with the window marked.
plt.figure(figsize=(7, 4.5))
plt.hist(usable_temps, bins=250)
plt.axvline(273, color="0.4")
plt.axvline(373, color="0.4")
plt.text(323, 105, "liquid water window", ha="center") # ha= centres both on the band
plt.text(323, 94, "↓", ha="center")
plt.xlim(0, 2500)
plt.ylim(0, 115) # room above the bars for the label
plt.xlabel("equilibrium temperature at albedo 0 (K)")
plt.ylabel("number of planets")
plt.title(f"Every planet with the three numbers (n = {len(usable_temps)}; a few hotter than 2500 K are off the right edge)")
plt.show()
Most known planets are far hotter than the window, because a planet close to its star is the easiest kind to find — a bias in the catalogue, not in the galaxy.
Astronomers keep an informal list of the planets thought most likely to be habitable: the outer TRAPPIST-1 planets, Proxima Centauri b, TOI-700 d, Kepler-186 f, Kepler-442 b. The cell below asks our test about those, and about two more.
famous = ["TRAPPIST-1 c", "TRAPPIST-1 e", "TRAPPIST-1 f", "TRAPPIST-1 g",
"Proxima Cen b", "TOI-700 d", "Kepler-186 f", "Kepler-442 b", "K2-18 b"]
for i in range(len(usable_names)):
if usable_names[i] in famous:
verdict, kind = check_planet(usable_temps[i], usable_radii[i])
print(f"{usable_names[i]}: {round(usable_temps[i], 1)} K - {verdict}, {kind}")
K2-18 b: 282.7 K - liquid water possible, too big to be rock TRAPPIST-1 c: 339.9 K - liquid water possible, rocky TRAPPIST-1 e: 249.8 K - too cold, rocky TRAPPIST-1 f: 217.7 K - too cold, rocky TRAPPIST-1 g: 197.4 K - too cold, rocky TOI-700 d: 267.8 K - too cold, rocky Kepler-186 f: 199.2 K - too cold, rocky Kepler-442 b: 256.7 K - too cold, rocky Proxima Cen b: 238.5 K - too cold, radius unknown
It rejects almost all of them as too cold — planets whose whole claim to interest is that they
might have air, judged by a formula that knows nothing about air. That is the failure it made on
Earth in section 3. (Proxima Cen b comes back radius unknown: it was found by watching its star
wobble, and that method never measures a size.)
It accepts two. TRAPPIST-1 c, at 339.9 K, is the hottest planet it accepts in that system — a
Venus in waiting. K2-18 b's line reads liquid water possible, too big to be rock: warm enough,
but with no surface for water to sit on. Within a system the test sorts by temperature alone —
TRAPPIST-1 b too hot, c and d in, e, f and g too cold — and temperature is exactly the sorting
that put Earth on the wrong side of the line.
The question, answered¶
Reflecting nothing, the test accepts 189 planets — 12 of them small enough to be rock — and accepts Earth at 278.3 K and Venus at 327.3 K together. At each world's measured albedo it rejects Earth at 254.0 K, Venus at 226.7 K and Mars at 209.8 K alike. It rejects Earth because equilibrium temperature is the temperature of a bare rock in sunlight, and Earth is not one: 34.0 K of Earth's warmth comes from its air, 510.3 K of Venus's from its own. The test is not broken; it answers a narrower question than the one we asked, and the gap between the two is the greenhouse effect.
Week 2 summary¶
The science¶
Which of these worlds could have liquid water — and why does your test reject Earth?
Equilibrium temperature ignores atmospheres: run the test one way and it accepts Venus alongside Earth, run it the other and it rejects both. It never gets the answer right. The greenhouse effect arrives as a measured discrepancy, not a definition. A function is a question you can ask many times without retyping it.
- Equilibrium temperature. The temperature a planet would sit at if starlight were the only thing heating it and it had no air.
- Albedo. The fraction of the starlight falling on a world that it reflects straight back into space; only the rest is absorbed and turned into heat.
- The greenhouse effect. The difference between the temperature a world would have with no air and the temperature it actually has.
The code¶
| Function | What it does |
|---|---|
| Python | |
for x in things: |
do the same thing once for each item |
range(n) |
the numbers 0 to n-1, to count with |
list.append(x) |
add one item to the end of a list |
if / elif / else |
choose what to do, based on a comparison |
and / or / not |
combine two conditions into one |
abs(x) |
the size of a number, ignoring its sign |
x in things |
true when that value is somewhere in the list |
None |
Python's word for "nothing here" — you cannot compare it with a number |
def name(a, b): |
write the recipe once and give it a name; the names in brackets are what it needs |
return value |
hand one value back to whoever called the function |
"""docstring""" |
triple-quoted text as the first thing inside a function, saying what it does |
return a, b |
hand back two values at once; catch them with a, b = f(...) |
help(f) |
print a function's docstring, which is why writing one pays |
| Matplotlib | |
plt.axvline(x) |
a vertical line at x, to mark a boundary on a plot |
plt.text(x, y, "Venus") |
write a word at a point on the axes |
Homework¶
Three parts. Each cell below has the same shape as the survey you wrote in Your turn 5 — a loop
over the usable planets, a call to check_planet, a count — with blanks to fill in. If you have
restarted the kernel, run the setup cell at the top, the checkpoint cell below, and then your own
Your turn 4 and Your turn 5: check_planet and rocky_names are your answers, so the checkpoint
cannot rebuild them for you.
# ── Checkpoint ── run this if you restarted the kernel or fell behind ──
sun_temp = 5772
sun_radius_au = 0.00465047
def equilibrium_temperature(star_temp, star_radius, distance, albedo):
"""the temperature starlight alone would hold a planet at, in kelvin"""
starlight = star_temp * (star_radius * sun_radius_au / (2 * distance)) ** 0.5
return starlight * (1 - albedo) ** 0.25
worlds = ["Venus", "Earth", "Mars"]
sun_distances = [0.723, 1.000, 1.524]
bond_albedos = [0.770, 0.306, 0.250]
albedo_temps = []
for i in range(len(worlds)):
starlight = sun_temp * (sun_radius_au / (2 * sun_distances[i])) ** 0.5
albedo_temps.append(starlight * (1 - bond_albedos[i]) ** 0.25)
usable_names = []
usable_temps = []
usable_radii = []
usable_star_temps = []
for i in range(len(planet_names)):
if star_temps[i] is not None and star_radii[i] is not None and distances[i] is not None:
temperature = equilibrium_temperature(star_temps[i], star_radii[i], distances[i], 0.0)
usable_names.append(planet_names[i])
usable_temps.append(temperature)
usable_radii.append(planet_radii[i])
usable_star_temps.append(star_temps[i])
# Re-run your own Your turn 4 (check_planet) and Your turn 5 (rocky_names) as well: every part
# below uses them, and they are not repeated here because they are your answers.
✏️ Your turn 6¶
What kind of stars do the candidates orbit?
Class found twelve rocky candidates, rocky_names, and never asked what they orbit. A star's
temperature is the first number the formula takes, and each planet's is in usable_star_temps.
The cell below loops over the usable planets and, for each one that is a candidate, keeps its
star's temperature in candidate_star_temps and counts it in red_dwarfs if that star is a red
dwarf — roughly, cooler than 4000 K; the Sun is 5772 K. Fill in the blanks; the printout is given.
So: do the candidates orbit Sun-like stars, or red dwarfs?
Use these names, because the self-check looks for them: candidate_star_temps and
red_dwarfs.
candidate_star_temps = []
red_dwarfs = 0
for i in range(len(usable_names)):
if usable_names[i] in rocky_names:
candidate_star_temps.append(usable_star_temps[i])
if usable_star_temps[i] < 4000:
red_dwarfs = red_dwarfs + 1
print("candidates:", len(candidate_star_temps))
print("orbiting a red dwarf, below 4000 K:", red_dwarfs)
print("the coolest star of the set:", min(candidate_star_temps), "K")
candidates: 12 orbiting a red dwarf, below 4000 K: 10 the coolest star of the set: 2566.0 K
assert len(candidate_star_temps) == len(rocky_names), \
"one star temperature per candidate — append inside the same if that tests the name"
assert 0 < red_dwarfs < len(candidate_star_temps), \
"some but not all of the candidates orbit a star below 4000 K — check the comparison and which list it reads"
assert min(candidate_star_temps) > 1000, \
"these should be STAR temperatures, thousands of kelvin — usable_star_temps[i], not usable_temps[i]"
print(f"✓ Homework 1 — {red_dwarfs} of {len(candidate_star_temps)} candidates orbit a red dwarf "
f"(below 4000 K); the coolest star is {min(candidate_star_temps)} K")
✓ Homework 1 — 10 of 12 candidates orbit a red dwarf (below 4000 K); the coolest star is 2566.0 K
What the numbers mean. Ten of the twelve orbit a star below 4000 K, and the coolest, TRAPPIST-1 at 2566 K, is under half
the Sun's temperature. Part of that is probably selection: a temperate planet orbits close in only
around a cool star, and there a small planet transits often and blocks a large share of a small
star's light, so it is the easiest kind to find — in this table 14% of all usable planets orbit a
star below 4000 K, 37% of those in the window, and 83% of the rocky ones in the window. Part may be
real: small planets do seem to be common around red dwarfs, which are most stars. Either way it
bears on the question, because a cool star is also faint, so a planet warm enough for liquid water
has to orbit close in — likely held with one face to its star, while the formula's 2 assumes the
heat is spread over the whole surface — and exposed to its flares. The formula asks for
the star's temperature and nothing about what that temperature implies, so "liquid water possible"
around a red dwarf rests on more assumptions than the same words around a Sun-like star. What would
settle it is not in this table.
✏️ Your turn 7¶
Give every planet an albedo.
Class gave every planet an albedo of 0 — reflecting nothing — which no real world does. Pick one:
- Option A — Venus's.
albedo = 0.770 - Option B — Earth's.
albedo = 0.306
A different albedo only rescales the albedo-0 temperature, usable_temps[i] * (1 - albedo) ** 0.25,
and the cell below already does that. Fill in the blanks: your albedo, the check_planet call,
the two verdicts that make a planet a candidate, and the count of class's twelve still on the
list (stayed).
So: are class's twelve candidates a property of those planets, or of the albedo class happened to pick?
Use these names, because the self-check looks for them: albedo, new_candidates and
stayed.
albedo = 0.770 # option A — Venus's. Option B, albedo = 0.306, gives 27 candidates, 9 still on the list.
new_candidates = []
for i in range(len(usable_names)):
temperature = usable_temps[i] * (1 - albedo) ** 0.25
verdict, kind = check_planet(temperature, usable_radii[i])
if verdict == "liquid water possible" and kind == "rocky":
new_candidates.append(usable_names[i])
stayed = 0
for i in range(len(rocky_names)):
if rocky_names[i] in new_candidates:
stayed = stayed + 1
print("rocky candidates with albedo", albedo, ":", len(new_candidates))
print("class had:", len(rocky_names), " of which still on the list:", stayed)
rocky candidates with albedo 0.77 : 59 class had: 12 of which still on the list: 0
assert albedo == 0.770 or albedo == 0.306, "set albedo to Venus's 0.770 (option A) or Earth's 0.306 (option B)"
assert len(rocky_names) < len(new_candidates) < 70, \
"an albedo above 0 cools every planet, so hot ones drop INTO the window: more than class's twelve, but a few dozen at most — is the rocky test there, and is it usable_radii?"
assert new_candidates[0] in usable_names, "new_candidates should hold planet NAMES — append usable_names[i]"
assert stayed < len(rocky_names), \
"some of class's candidates cool out of the window — stayed should be fewer than all of them"
assert albedo != 0.306 or stayed > 0, \
"with Earth's albedo some of class's candidates stay — stayed counts the names in rocky_names that are also in new_candidates"
print(f"✓ Homework 2 — albedo {albedo}: {len(new_candidates)} rocky candidates against class's "
f"{len(rocky_names)}, {stayed} of those still on the list")
✓ Homework 2 — albedo 0.77: 59 rocky candidates against class's 12, 0 of those still on the list
What the numbers mean. With Venus's albedo the list grows from 12 to 59 and none of class's twelve remain; with Earth's it
grows to 27 and nine remain. Nothing measured about any planet changed — one number this table does
not hold for any of them did. That is enough to say the twelve depend on the albedo class picked,
and not enough to say which albedo is right: a real planet's could sit anywhere from about 0.1 for
bare rock, like the Moon, to Venus's 0.77. Both options assume clouds, and a distant rock is not
obviously more like Venus than like Earth — or like the Moon. The mechanism is easy to see —
(1 - 0.770) ** 0.25 is about 0.69, so every temperature is scaled down past a window that stayed
put. A candidate list is best read with its assumptions attached.
✏️ Your turn 8¶
What if every planet had air?
The test treats every planet as a bare rock. Section 3 measured how much warmer a real surface is: 34.0 K on Earth, 510.3 K on Venus. Suppose every planet got Earth's greenhouse gap — and then Venus's. (A rough rule: the gap depends on the planet, and a colder world's air would add less; but these are the sizes we measured.)
The cell below calls check_planet twice for every usable planet — once with Earth's gap added
to its temperature, once with Venus's — and counts the rocky candidates each time. Fill in the two
gaps, the two tests and the two counting lines.
So: how many rocky candidates are there if every planet has Earth-like air, and how many with Venus-like air?
Use these names, because the self-check looks for them: earth_air and venus_air.
earth_air = 0
venus_air = 0
for i in range(len(usable_names)):
verdict, kind = check_planet(usable_temps[i] + 34.0, usable_radii[i])
if verdict == "liquid water possible" and kind == "rocky":
earth_air = earth_air + 1
verdict, kind = check_planet(usable_temps[i] + 510.3, usable_radii[i])
if verdict == "liquid water possible" and kind == "rocky":
venus_air = venus_air + 1
print("rocky candidates if every planet had Earth's air:", earth_air)
print("rocky candidates if every planet had Venus's air:", venus_air)
print("class had, with no air at all:", len(rocky_names))
rocky candidates if every planet had Earth's air: 14 rocky candidates if every planet had Venus's air: 0 class had, with no air at all: 12
assert venus_air < earth_air < 100, \
"Venus's 510 K puts every planet past boiling and Earth's 34 K leaves a few dozen at most — check which gap went where, and that the rocky test is still there"
assert venus_air == 0, \
"with Venus's 510 K added, every planet is past boiling — venus_air can only be 0; anything else means the gap was not added"
assert earth_air > len(rocky_names), \
"in this archive more cold rocky worlds warm INTO the window with Earth's 34 K than warm ones leave it, so earth_air should beat class's count — is it being counted up each time?"
print(f"✓ Homework 3 — {earth_air} rocky candidates with Earth's air, {venus_air} with Venus's, "
f"against class's {len(rocky_names)} with none")
✓ Homework 3 — 14 rocky candidates with Earth's air, 0 with Venus's, against class's 12 with none
What the numbers mean. With Earth's 34 K added there are 14 rocky candidates instead of 12: three of class's twelve, the ones already above 339 K, move out of the top of the window, and five colder worlds move in. With Venus's 510 K there are none. This is a rough rule twice over. The gap a real atmosphere adds depends on the planet, and these are two of the only three we measured. And each gap was measured against a world's temperature at its own albedo but is added here to albedo-0 temperatures, so even Earth comes out at 312 K rather than 288; Earth's albedo and gap together give 288 K exactly, and 13 candidates rather than 14. So the counts are illustrations, not predictions. What they do show is that an atmosphere can move a planet in or out of the window by more than the window is wide, and the archive measures nothing about atmospheres. The test can rule some worlds in as possible; it cannot say which of them has water.