Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Call Python and C libraries 🔥

Your puzzle is solved, but one question hangs over the season: is the Pole actually warming, or is that just the midday sun on station 3? Here’s what you need: hand your readings to Python’s numpy to fit a trend, and to a C library to crunch them at native speed. Mojo links to both.

Mojo solves the whole season on its own. This page is about reach: when a library you already trust does exactly what you want, you call it instead of reimplementing it.

Mojo connects to C and Python libraries. Mojo connects to C and Python libraries.

The question

Station 3 is the one with the sun problem: readings taken between 10:00 and 14:00 run warm because the sensor bakes in direct light. Pull station 3’s entries back out of station_reports.txt and correct them the same way you did on the toolkit page, so the trend you fit measures climate, not sunshine on a roof.

Create warming.mojo:

    var log = Path("src/downloads/station_reports.txt")
    var lines = log.read_text().splitlines()

    var days: List[Int64] = []
    var readings: List[Float64] = []

    for line in lines:
        # ['Station 3', 'Day 0', '06:00', '-22.3C', 'Clear']
        var fields = line.split(", ")
        if String(fields[0]) != "Station 3":
            continue

        try:
            var dayString = String(fields[1].split(" ")[1])
            var day = Int64(atol(dayString))
            days.append(day)

            var hourString = String(fields[2].split(":")[0])
            var temperatureString = String(fields[3].removesuffix("C"))
            var temperature = Float64(atof(temperatureString))

            # Station 3 bakes in solar loading between 10:00 and 14:00
            if Float64(10) <= Float64(hourString) < Float64(14):
                temperature -= 1.5

            readings.append(temperature)

        except e:
            print(t"Unable to convert text to data: {e}")

    print("Warming report")
    print(t"Readings:    {len(readings)} (Station 3 only)")  # Four readings

Day 1’s only station 3 reading lands at noon, right in the sun-bias window, so it comes in at a suspiciously warm -18.5C. The correction knocks it back down to -20.0, right in line with the rest of the season. Skip the correction and the trend still looks like it’s warming, just by a slightly inflated amount.

Takeaways

  • Type the days and readings declarations explicitly.

  • Chained comparisons evaluate as pairwise comparisons joined by and. This expression:

    10 <= Float64(hour_string) < 14
    

    is equivalent to:

    10 <= Float64(hour_string) and Float64(hour_string) < 14
    

Call Python: fit a trend with numpy

numpy fits a line in one call. Its polyfit function returns the slope and intercept. The sign of the slope answers the question: “Is it warming?”

Add imports at the top:

from std.python import Python
from std.python.numpy import copy_to_numpy_array, from_numpy_array

Then, in main():

    # Converts True/False to "Yes"/"No"
    var truth_string = lambda (b: Bool) -> String: ("Yes" if b else "No")

    try:
        var np = Python.import_module("numpy")
        var xs = copy_to_numpy_array(days)
        var ys = copy_to_numpy_array(readings)

        var fit = np.polyfit(xs, ys, 1)  # [slope, intercept]
        var slope = Float64(py=fit[0])  # degrees per day

        print(t"Trend:       {round(slope, 2)} C/day")  # ~1.91, rising
        print(t"Warming?:    {truth_string(slope > 0)}")  # Yes
    except:
        print("numpy not found. Add it with `pixi add numpy`.")

Takeaways

  • Python.import_module() loads any module the active Python can see. It raises if the module is missing, so use try/except to handle the error.
  • numpy isn’t part of Mojo. Add it to your project with pixi add numpy. Python standard library modules such as re and math need no separate install.
  • np.array() doesn’t accept a Mojo List directly. Build a Python.list() and append the values, crossing the Python boundary once instead of once per value.
  • slope is a PythonObject. Compare it in Python space (slope > 0) instead of converting it to a Mojo Float64.
  • The exact slope includes floating-point noise. Its sign is all you need to answer whether the readings are warming.

Call C: crunch with BLAS

BLAS is a widely used C numeric library. Load it and call a function by name to reduce the array by summing absolute values.

Add the FFI imports at the top:

from std.ffi import OwnedDLHandle, c_int, c_double

Then, in main():

    try:
        var blas = OwnedDLHandle(
            "/System/Library/Frameworks/Accelerate.framework/Accelerate"
        )

        # Keep readings alive
        comptime origin = origin_of(readings)

        # cblas_dasum(count, X, stride) sums |X| in one C call
        var dasum = blas.borrow().get_function[
            def(
                c_int, Pointer[c_double, origin], c_int
            ) thin abi("C") -> c_double
        ]("cblas_dasum")

        var total = dasum(c_int(len(readings)), readings.unsafe_ptr(), c_int(1))
        var mean = total / Float64(len(readings))

        # Every Station 3 reading is below zero, so the magnitude sum
        # negates back to the real mean
        print(t"season mean: {round(-mean, 2)}")  # -20.55
    except:
        print("BLAS not found.")

Takeaways

  • OwnedDLHandle(path) opens a shared library. It raises if the library is missing, so use the same try/except probe as for NumPy.
  • get_function() specifies the C signature. thin abi("C") declares a plain C function, while c_int and c_double match C’s integer and double types. c_double is an alias for Float64, so the readings need no conversion.
  • readings.unsafe_ptr() gives C a pointer to the list’s storage. Get the pointer immediately before the call. Growing the list can move its storage and invalidate the pointer.
  • cblas_dasum sums absolute values. Because every station 3 reading is negative, negate the result before calculating the mean.
  • macOS provides BLAS through the Accelerate framework used here. On Linux, use libopenblas.so or libblas.so.3, which usually requires a separate installation.

Final code

Your complete warming.mojo:

from std.pathlib import Path

from std.python import Python
from std.python.numpy import copy_to_numpy_array, from_numpy_array


from std.ffi import OwnedDLHandle, c_int, c_double



def main() raises:
    var log = Path("src/downloads/station_reports.txt")
    var lines = log.read_text().splitlines()

    var days: List[Int64] = []
    var readings: List[Float64] = []

    for line in lines:
        # ['Station 3', 'Day 0', '06:00', '-22.3C', 'Clear']
        var fields = line.split(", ")
        if String(fields[0]) != "Station 3":
            continue

        try:
            var dayString = String(fields[1].split(" ")[1])
            var day = Int64(atol(dayString))
            days.append(day)

            var hourString = String(fields[2].split(":")[0])
            var temperatureString = String(fields[3].removesuffix("C"))
            var temperature = Float64(atof(temperatureString))

            # Station 3 bakes in solar loading between 10:00 and 14:00
            if Float64(10) <= Float64(hourString) < Float64(14):
                temperature -= 1.5

            readings.append(temperature)

        except e:
            print(t"Unable to convert text to data: {e}")

    print("Warming report")
    print(t"Readings:    {len(readings)} (Station 3 only)")  # Four readings

    # Converts True/False to "Yes"/"No"
    var truth_string = lambda (b: Bool) -> String: ("Yes" if b else "No")

    try:
        var np = Python.import_module("numpy")
        var xs = copy_to_numpy_array(days)
        var ys = copy_to_numpy_array(readings)

        var fit = np.polyfit(xs, ys, 1)  # [slope, intercept]
        var slope = Float64(py=fit[0])  # degrees per day

        print(t"Trend:       {round(slope, 2)} C/day")  # ~1.91, rising
        print(t"Warming?:    {truth_string(slope > 0)}")  # Yes
    except:
        print("numpy not found. Add it with `pixi add numpy`.")

    try:
        var blas = OwnedDLHandle(
            "/System/Library/Frameworks/Accelerate.framework/Accelerate"
        )

        # Keep readings alive
        comptime origin = origin_of(readings)

        # cblas_dasum(count, X, stride) sums |X| in one C call
        var dasum = blas.borrow().get_function[
            def(
                c_int, Pointer[c_double, origin], c_int
            ) thin abi("C") -> c_double
        ]("cblas_dasum")

        var total = dasum(c_int(len(readings)), readings.unsafe_ptr(), c_int(1))
        var mean = total / Float64(len(readings))

        # Every Station 3 reading is below zero, so the magnitude sum
        # negates back to the real mean
        print(t"season mean: {round(-mean, 2)}")  # -20.55
    except:
        print("BLAS not found.")

Topics covered

Importing a Python module, gating an import with try/except, building a Python.list to cross the boundary, working with a PythonObject, loading a C library with OwnedDLHandle, naming a C signature for get_function, the c_int and c_double aliases, and passing an array to C with unsafe_ptr.

You started the season printing one line and finished it linking Mojo to Python and C to answer a real question. The whole toolkit, from a first print to the outside world, is yours now. Go solve some puzzles.