From 9742200a9ec5d15b39139101b14d4c4035b86c08 Mon Sep 17 00:00:00 2001 From: jackdfranklin Date: Thu, 29 Oct 2026 17:12:51 +0000 Subject: [PATCH 1/5] Add energy balance climate model plot When thinking about exercises for testing I felt that having a less trivial plot/data analysis would be good. --- exercises/problem/climate_model.py | 15 +++++++++++++++ exercises/problem/plot4.py | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 exercises/problem/climate_model.py create mode 100644 exercises/problem/plot4.py diff --git a/exercises/problem/climate_model.py b/exercises/problem/climate_model.py new file mode 100644 index 0000000..d791bdc --- /dev/null +++ b/exercises/problem/climate_model.py @@ -0,0 +1,15 @@ +solar_constant = 1370 +sigma = 5.67e-8 # Stefan-Boltzmann constant + +def absorbed_solar_radiation(albedo): + return solar_constant*(1-albedo)/4 + +def emitted_radiation(emissivity, temperature): + return emissivity * sigma * temperature**4 + +def energy_balanced_temperature(emissivity, albedo): + # dT/dt = 0 => in = out + return (solar_constant*(1-albedo)/(4*emissivity * sigma))**0.25 + +def emissivity_given_temp(albedo, temperature): + return solar_constant*(1-albedo)/(4*sigma*temperature**4) diff --git a/exercises/problem/plot4.py b/exercises/problem/plot4.py new file mode 100644 index 0000000..afa599a --- /dev/null +++ b/exercises/problem/plot4.py @@ -0,0 +1,25 @@ +import climate_model + +import xarray +import matplotlib.pyplot as plt + +global_ds = xarray.open_dataset("../data/HadCRUT.5.0.0.0_analysis_summary-series_global_annual.nc") + +fig, axs = plt.subplots(1, 1, figsize=(10,6), layout='constrained') + +# Absolute mean temperature between 1961 and 1990 +# https://crudata.uea.ac.uk/cru/data/temperature/abs_glnhsh.txt +absolute_value = 13.974 + 273.5 +time = global_ds['time'].data +temperature = global_ds['tas_mean'].data + absolute_value +albedo = 0.3 + +epsilon = climate_model.emissivity_given_temp(albedo, temperature) + +axs.plot(time, epsilon) + +axs.set_xlabel("Year") +axs.set_ylabel("Effective emissivity of Earth") + +plt.show() + From d8ea2df416b76dafcbf1f24459c1b807cf03a11d Mon Sep 17 00:00:00 2001 From: jackdfranklin Date: Wed, 4 Nov 2026 17:31:33 +0000 Subject: [PATCH 2/5] Add pytest slides --- src/_pytest.qmd | 162 +++++++++++++++++++++++++ src/custom.scss | 312 ++++++++++++++++++++++++++++++++++++++++++++++++ src/slides.qmd | 1 + 3 files changed, 475 insertions(+) create mode 100644 src/_pytest.qmd diff --git a/src/_pytest.qmd b/src/_pytest.qmd new file mode 100644 index 0000000..5bed2b1 --- /dev/null +++ b/src/_pytest.qmd @@ -0,0 +1,162 @@ +# Testing with pytest + +## What is pytest? + +:::{.columns} +::::{.column width=50%} +- A testing *framework* for python +- Makes it a lot easier to write tests +- Is used extensively by python packages +- Automatically discovers and runs tests +:::: +::::{.column width=50%} +![](img/pytest.png){style="width:50%;"} +:::: +::: + +## pytest basics + +\ + +- Tests are .py files and live in a `tests/` directory +- Each test group is a function with prefix `test_` +- Use the `assert` keyword to validate an expression is true + +\ + +:::{.columns} +::::{.column width=30%} +```{.python filename="src/add.py"} +def add(a, b): + return a+b +``` +```{.python filename="tests/test.py"} +def test_add(): + assert add(0, 0) == 0 + assert add(1, 0) == 1 + assert add(0, 1) == 1 + assert add(1, 1) == 2 + ... +``` +:::: +::::{.column width=70%} + +```{=html} +
terminal
+
+===================================== test session starts ===================================== +platform linux -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /tmp/example +collected 1 item +  +tests/test.py . [100%] +  +====================================== 1 passed in 0.00s ====================================== +
+``` +:::: +::: + +## pytest basics + +What if we implemented `add` wrong? + +```{.python filename="src/add,py"} +def add(a, b): + return a-b # Incorrect +``` + +\ + +```{=html} +
+[jack@nixos:~]$ pytest tests/test.py +platform linux -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /tmp/example +collected 1 item +  +tests/test.py F [100%] +  +========================================== FAILURES =========================================== +__________________________________________ test_add ___________________________________________ +  + def test_add(): + assert add(0, 0) == 0 + assert add(1, 0) == 1 +> assert add(0, 1) == 1 +E assert -1 == 1 +E + where -1 = add(0, 1) +  +tests/test.py:7: AssertionError +=================================== short test summary info =================================== +FAILED tests/test.py::test_add - assert -1 == 1 +====================================== 1 failed in 0.02s ====================================== +
+``` + +## Parametrized tests + +- pytest implements a lot of useful tools to make writing tests easier +- One of these is test parametrization, which allows us to reuse tests for different + inputs +- We only need to give the `pytest.mark.parametrize` decorator: + - The names of the input variables + - A list of tuples of inputs + +```{.python filename="tests/test.py"} +@pytest.mark.parametrize("x,y,expected", [(0, 0, 0), (1, 0, 1), (0, 1, 1), (1, 1, 2)]) +def test_add(x, y, expected): + assert add(x, y) == expected +``` + +## What to test + +- Beyond using fixed points, we can think about how a function should behave +- For example, our add function should have the following *properties* + - `add(x,0) == x` + - `add(x, y) == add(y, x)` + - `add(x, add(y, z)) == add(add(x, y), z)` + +## Writing code for unit testing + +\ + +- Unit testing forces us to write code that is minimal +- Decompose functions into their smallest logical unit +- Smaller functions are easier to test (and review!) + - Fewer/simpler inputs + - Can help to think of edge cases + +### Example + +:::{.columns} +::::{.column width=50%} +Easier to test this: +```{.python} +def complicated_computation(data): + z = computation_1(data.x, data.y) + return computation_2(data.t, data.w, z) + +def computation_1(x, y): + result = atan2(x, y) + # Maybe do something else here + return result + +def computation_2(t, w, z): + result = exp((w+z)/t) + # Maybe do something else here + return result +``` +:::: +::::{.column width=50%} +Than this: +```{.python} +def complicated_computation(data): + z = atan2(data.x, data.y) + z = ... + result = exp((data.w+z)/data.t) + result = ... + return result +``` +:::: +::: diff --git a/src/custom.scss b/src/custom.scss index b082f18..b986bc7 100644 --- a/src/custom.scss +++ b/src/custom.scss @@ -83,6 +83,17 @@ $accent: #0c4e8a; font-weight: 500; } +$presentation-header-margin: 5%; + +$presentation-font-size-root: 1.5em; +$presentation-line-height: 1.5em; + +/*-- scss:rules --*/ + +.reveal .slide .li { + margin-top: 1em; +} + /*-- vertically center columns (or any container) --*/ .v-center-container { display: flex; @@ -104,3 +115,304 @@ $accent: #0c4e8a; } } } + +.term-container { + background: #171717; + border-radius: 5px; + border-color: white; + color: white; + word-break: break-word; + overflow-wrap: break-word; + font-family: "SFMono-Regular", Monaco, Menlo, Consolas, "Liberation Mono", Courier, monospace; + font-size: 12px; + line-height: 20px; + white-space: pre-wrap; +} + +.term-container img { max-width: 100%; } + +.term-container time { padding-right: 1ex; } + +.term a { color: inherit; text-decoration: underline; text-decoration-style: dashed; } +.term a:hover { color: #2882F9 } + +@keyframes blink-animation { + to { + visibility: hidden; + } +} + +.term-fg1 { } /* don't bold beccause it looks weird */ +.term-fg2 { color: #838887; } /* faint (decreased intensity) - same as gray really */ +.term-fg3 { font-style: italic; } /* italic */ +.term-fg4 { text-decoration: underline; } /* underline */ +.term-fg5 { animation: blink-animation 1s steps(3, start) infinite; } /* blink */ +.term-fg9 { text-decoration: line-through; } /* crossed-out */ + +.term-fg30 { color: #666666; } /* black (but we can't use black, so a diff color) */ +.term-fg31 { color: #ff7070; } /* red */ +.term-fg32 { color: #b0f986; } /* green */ +.term-fg33 { color: #c6c502; } /* yellow */ +.term-fg34 { color: #8db7e0; } /* blue */ +.term-fg35 { color: #f271fb; } /* magenta */ +.term-fg36 { color: #6bf7ff; } /* cyan */ + +/* high intense colors */ +.term-fgi1 { color: #5ef765; } +.term-fgi90 { color: #838887; } /* grey */ +.term-fgi91 { color: #ff3333; } /* red */ +.term-fgi92 { color: #00FF00; } /* green */ +.term-fgi93 { color: #fffc67; } /* yellow */ +.term-fgi94 { color: #6871ff; } /* blue */ +.term-fgi95 { color: #ff76ff; } /* magenta */ +.term-fgi96 { color: #60fcff; } /* cyan */ + +/* background colors */ +.term-bg40 { background: #676767; } /* grey */ +.term-bg41 { background: #ff4343; } /* red */ +.term-bg42 { background: #99ff5f; } /* green */ + +/* custom foreground/background combos for readability */ +.term-fg31.term-bg40 { color: #F8A39F; } + +/* xterm colors */ +.term-fgx16 { color: #000000; } +.term-fgx17 { color: #00005f; } +.term-fgx18 { color: #000087; } +.term-fgx19 { color: #0000af; } +.term-fgx20 { color: #0000d7; } +.term-fgx21 { color: #0000ff; } +.term-fgx22 { color: #005f00; } +.term-fgx23 { color: #005f5f; } +.term-fgx24 { color: #005f87; } +.term-fgx25 { color: #005faf; } +.term-fgx26 { color: #005fd7; } +.term-fgx27 { color: #005fff; } +.term-fgx28 { color: #008700; } +.term-fgx29 { color: #00875f; } +.term-fgx30 { color: #008787; } +.term-fgx31 { color: #0087af; } +.term-fgx32 { color: #0087d7; } +.term-fgx33 { color: #0087ff; } +.term-fgx34 { color: #00af00; } +.term-fgx35 { color: #00af5f; } +.term-fgx36 { color: #00af87; } +.term-fgx37 { color: #00afaf; } +.term-fgx38 { color: #00afd7; } +.term-fgx39 { color: #00afff; } +.term-fgx40 { color: #00d700; } +.term-fgx41 { color: #00d75f; } +.term-fgx42 { color: #00d787; } +.term-fgx43 { color: #00d7af; } +.term-fgx44 { color: #00d7d7; } +.term-fgx45 { color: #00d7ff; } +.term-fgx46 { color: #00ff00; } +.term-fgx47 { color: #00ff5f; } +.term-fgx48 { color: #00ff87; } +.term-fgx49 { color: #00ffaf; } +.term-fgx50 { color: #00ffd7; } +.term-fgx51 { color: #00ffff; } +.term-fgx52 { color: #5f0000; } +.term-fgx53 { color: #5f005f; } +.term-fgx54 { color: #5f0087; } +.term-fgx55 { color: #5f00af; } +.term-fgx56 { color: #5f00d7; } +.term-fgx57 { color: #5f00ff; } +.term-fgx58 { color: #5f5f00; } +.term-fgx59 { color: #5f5f5f; } +.term-fgx60 { color: #5f5f87; } +.term-fgx61 { color: #5f5faf; } +.term-fgx62 { color: #5f5fd7; } +.term-fgx63 { color: #5f5fff; } +.term-fgx64 { color: #5f8700; } +.term-fgx65 { color: #5f875f; } +.term-fgx66 { color: #5f8787; } +.term-fgx67 { color: #5f87af; } +.term-fgx68 { color: #5f87d7; } +.term-fgx69 { color: #5f87ff; } +.term-fgx70 { color: #5faf00; } +.term-fgx71 { color: #5faf5f; } +.term-fgx72 { color: #5faf87; } +.term-fgx73 { color: #5fafaf; } +.term-fgx74 { color: #5fafd7; } +.term-fgx75 { color: #5fafff; } +.term-fgx76 { color: #5fd700; } +.term-fgx77 { color: #5fd75f; } +.term-fgx78 { color: #5fd787; } +.term-fgx79 { color: #5fd7af; } +.term-fgx80 { color: #5fd7d7; } +.term-fgx81 { color: #5fd7ff; } +.term-fgx82 { color: #5fff00; } +.term-fgx83 { color: #5fff5f; } +.term-fgx84 { color: #5fff87; } +.term-fgx85 { color: #5fffaf; } +.term-fgx86 { color: #5fffd7; } +.term-fgx87 { color: #5fffff; } +.term-fgx88 { color: #870000; } +.term-fgx89 { color: #87005f; } +.term-fgx90 { color: #870087; } +.term-fgx91 { color: #8700af; } +.term-fgx92 { color: #8700d7; } +.term-fgx93 { color: #8700ff; } +.term-fgx94 { color: #875f00; } +.term-fgx95 { color: #875f5f; } +.term-fgx96 { color: #875f87; } +.term-fgx97 { color: #875faf; } +.term-fgx98 { color: #875fd7; } +.term-fgx99 { color: #875fff; } +.term-fgx100 { color: #878700; } +.term-fgx101 { color: #87875f; } +.term-fgx102 { color: #878787; } +.term-fgx103 { color: #8787af; } +.term-fgx104 { color: #8787d7; } +.term-fgx105 { color: #8787ff; } +.term-fgx106 { color: #87af00; } +.term-fgx107 { color: #87af5f; } +.term-fgx108 { color: #87af87; } +.term-fgx109 { color: #87afaf; } +.term-fgx110 { color: #87afd7; } +.term-fgx111 { color: #87afff; } +.term-fgx112 { color: #87d700; } +.term-fgx113 { color: #87d75f; } +.term-fgx114 { color: #87d787; } +.term-fgx115 { color: #87d7af; } +.term-fgx116 { color: #87d7d7; } +.term-fgx117 { color: #87d7ff; } +.term-fgx118 { color: #87ff00; } +.term-fgx119 { color: #87ff5f; } +.term-fgx120 { color: #87ff87; } +.term-fgx121 { color: #87ffaf; } +.term-fgx122 { color: #87ffd7; } +.term-fgx123 { color: #87ffff; } +.term-fgx124 { color: #af0000; } +.term-fgx125 { color: #af005f; } +.term-fgx126 { color: #af0087; } +.term-fgx127 { color: #af00af; } +.term-fgx128 { color: #af00d7; } +.term-fgx129 { color: #af00ff; } +.term-fgx130 { color: #af5f00; } +.term-fgx131 { color: #af5f5f; } +.term-fgx132 { color: #af5f87; } +.term-fgx133 { color: #af5faf; } +.term-fgx134 { color: #af5fd7; } +.term-fgx135 { color: #af5fff; } +.term-fgx136 { color: #af8700; } +.term-fgx137 { color: #af875f; } +.term-fgx138 { color: #af8787; } +.term-fgx139 { color: #af87af; } +.term-fgx140 { color: #af87d7; } +.term-fgx141 { color: #af87ff; } +.term-fgx142 { color: #afaf00; } +.term-fgx143 { color: #afaf5f; } +.term-fgx144 { color: #afaf87; } +.term-fgx145 { color: #afafaf; } +.term-fgx146 { color: #afafd7; } +.term-fgx147 { color: #afafff; } +.term-fgx148 { color: #afd700; } +.term-fgx149 { color: #afd75f; } +.term-fgx150 { color: #afd787; } +.term-fgx151 { color: #afd7af; } +.term-fgx152 { color: #afd7d7; } +.term-fgx153 { color: #afd7ff; } +.term-fgx154 { color: #afff00; } +.term-fgx155 { color: #afff5f; } +.term-fgx156 { color: #afff87; } +.term-fgx157 { color: #afffaf; } +.term-fgx158 { color: #afffd7; } +.term-fgx159 { color: #afffff; } +.term-fgx160 { color: #d70000; } +.term-fgx161 { color: #d7005f; } +.term-fgx162 { color: #d70087; } +.term-fgx163 { color: #d700af; } +.term-fgx164 { color: #d700d7; } +.term-fgx165 { color: #d700ff; } +.term-fgx166 { color: #d75f00; } +.term-fgx167 { color: #d75f5f; } +.term-fgx168 { color: #d75f87; } +.term-fgx169 { color: #d75faf; } +.term-fgx170 { color: #d75fd7; } +.term-fgx171 { color: #d75fff; } +.term-fgx172 { color: #d78700; } +.term-fgx173 { color: #d7875f; } +.term-fgx174 { color: #d78787; } +.term-fgx175 { color: #d787af; } +.term-fgx176 { color: #d787d7; } +.term-fgx177 { color: #d787ff; } +.term-fgx178 { color: #d7af00; } +.term-fgx179 { color: #d7af5f; } +.term-fgx180 { color: #d7af87; } +.term-fgx181 { color: #d7afaf; } +.term-fgx182 { color: #d7afd7; } +.term-fgx183 { color: #d7afff; } +.term-fgx184 { color: #d7d700; } +.term-fgx185 { color: #d7d75f; } +.term-fgx186 { color: #d7d787; } +.term-fgx187 { color: #d7d7af; } +.term-fgx188 { color: #d7d7d7; } +.term-fgx189 { color: #d7d7ff; } +.term-fgx190 { color: #d7ff00; } +.term-fgx191 { color: #d7ff5f; } +.term-fgx192 { color: #d7ff87; } +.term-fgx193 { color: #d7ffaf; } +.term-fgx194 { color: #d7ffd7; } +.term-fgx195 { color: #d7ffff; } +.term-fgx196 { color: #ff0000; } +.term-fgx197 { color: #ff005f; } +.term-fgx198 { color: #ff0087; } +.term-fgx199 { color: #ff00af; } +.term-fgx200 { color: #ff00d7; } +.term-fgx201 { color: #ff00ff; } +.term-fgx202 { color: #ff5f00; } +.term-fgx203 { color: #ff5f5f; } +.term-fgx204 { color: #ff5f87; } +.term-fgx205 { color: #ff5faf; } +.term-fgx206 { color: #ff5fd7; } +.term-fgx207 { color: #ff5fff; } +.term-fgx208 { color: #ff8700; } +.term-fgx209 { color: #ff875f; } +.term-fgx210 { color: #ff8787; } +.term-fgx211 { color: #ff87af; } +.term-fgx212 { color: #ff87d7; } +.term-fgx213 { color: #ff87ff; } +.term-fgx214 { color: #ffaf00; } +.term-fgx215 { color: #ffaf5f; } +.term-fgx216 { color: #ffaf87; } +.term-fgx217 { color: #ffafaf; } +.term-fgx218 { color: #ffafd7; } +.term-fgx219 { color: #ffafff; } +.term-fgx220 { color: #ffd700; } +.term-fgx221 { color: #ffd75f; } +.term-fgx222 { color: #ffd787; } +.term-fgx223 { color: #ffd7af; } +.term-fgx224 { color: #ffd7d7; } +.term-fgx225 { color: #ffd7ff; } +.term-fgx226 { color: #ffff00; } +.term-fgx227 { color: #ffff5f; } +.term-fgx228 { color: #ffff87; } +.term-fgx229 { color: #ffffaf; } +.term-fgx230 { color: #ffffd7; } +.term-fgx231 { color: #ffffff; } +.term-fgx232 { color: #080808; } +.term-fgx233 { color: #121212; } +.term-fgx234 { color: #1c1c1c; } +.term-fgx235 { color: #262626; } +.term-fgx236 { color: #303030; } +.term-fgx237 { color: #3a3a3a; } +.term-fgx238 { color: #444444; } +.term-fgx239 { color: #4e4e4e; } +.term-fgx240 { color: #585858; } +.term-fgx241 { color: #626262; } +.term-fgx242 { color: #6c6c6c; } +.term-fgx243 { color: #767676; } +.term-fgx244 { color: #808080; } +.term-fgx245 { color: #8a8a8a; } +.term-fgx246 { color: #949494; } +.term-fgx247 { color: #9e9e9e; } +.term-fgx248 { color: #a8a8a8; } +.term-fgx249 { color: #b2b2b2; } +.term-fgx250 { color: #bcbcbc; } +.term-fgx251 { color: #c6c6c6; } +.term-fgx252 { color: #d0d0d0; } +.term-fgx253 { color: #dadada; } +.term-fgx254 { color: #e4e4e4; } +.term-fgx255 { color: #eeeeee; } diff --git a/src/slides.qmd b/src/slides.qmd index 8753614..139d8bd 100644 --- a/src/slides.qmd +++ b/src/slides.qmd @@ -44,6 +44,7 @@ execute: {{< include ../exercises/automation_exercises.qmd >}} {{< include testing.qmd >}} +{{< include _pytest.qmd >}} {{< include documentation.qmd >}} {{< include ../exercises/documentation_exercises.qmd >}} From 471a20aa5d487cc1982f3de75fb61d8ca21df625 Mon Sep 17 00:00:00 2001 From: jackdfranklin Date: Wed, 4 Nov 2026 17:53:12 +0000 Subject: [PATCH 3/5] Add testing exercises --- exercises/testing_exercises.qmd | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 exercises/testing_exercises.qmd diff --git a/exercises/testing_exercises.qmd b/exercises/testing_exercises.qmd new file mode 100644 index 0000000..eba8a7b --- /dev/null +++ b/exercises/testing_exercises.qmd @@ -0,0 +1,13 @@ +## Exercises + +In the example repository, there is a file called 'climate_model.py'. +This file contains some functions that are used in 'plot4.py' + +Task: + +1. Write some basic tests for these functions (think about inputs that give known + outputs) +2. Think about what properties/behaviours these functions should have, and write + tests for them +3. Look at the other python files, and see if any functions can be extracted and + tested From fc69a4e1a1056da35c9a1b74da87e42168bb95ad Mon Sep 17 00:00:00 2001 From: jackdfranklin Date: Fri, 6 Nov 2026 10:04:02 +0000 Subject: [PATCH 4/5] Improve layout of testing slides --- src/testing.qmd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/testing.qmd b/src/testing.qmd index 0221e71..d3f1c43 100644 --- a/src/testing.qmd +++ b/src/testing.qmd @@ -24,6 +24,9 @@ ## A very short intro to testing +:::{.columns} +::::{.column width=50%} + ### Unit tests - Test the smallest logical unit of the code @@ -31,6 +34,9 @@ - Test functions for known results - Compare to previously produced results +:::: +::::{.column width=50%} + ### Integration tests - Test that components work together @@ -38,6 +44,9 @@ - Can use previous results to validate model - Ensure no regression of results +:::: +::: + --- ## Adding tests to a project From dbce4fedeb6a9d817276d0ab6e1b576ee7a5e539 Mon Sep 17 00:00:00 2001 From: jackdfranklin Date: Fri, 6 Nov 2026 10:04:15 +0000 Subject: [PATCH 5/5] Add testing exercises to slides --- src/slides.qmd | 1 + 1 file changed, 1 insertion(+) diff --git a/src/slides.qmd b/src/slides.qmd index 139d8bd..dda7f20 100644 --- a/src/slides.qmd +++ b/src/slides.qmd @@ -45,6 +45,7 @@ execute: {{< include testing.qmd >}} {{< include _pytest.qmd >}} +{{< include ../exercises/testing_exercises.qmd >}} {{< include documentation.qmd >}} {{< include ../exercises/documentation_exercises.qmd >}}