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.
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
daysandreadingsdeclarations explicitly. -
Chained comparisons evaluate as pairwise comparisons joined by
and. This expression:10 <= Float64(hour_string) < 14is 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 usetry/exceptto handle the error.numpyisn’t part of Mojo. Add it to your project withpixi add numpy. Python standard library modules such asreandmathneed no separate install.np.array()doesn’t accept a MojoListdirectly. Build aPython.list()and append the values, crossing the Python boundary once instead of once per value.slopeis aPythonObject. Compare it in Python space (slope > 0) instead of converting it to a MojoFloat64.- 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 sametry/exceptprobe as for NumPy.get_function()specifies the C signature.thin abi("C")declares a plain C function, whilec_intandc_doublematch C’s integer and double types.c_doubleis an alias forFloat64, 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_dasumsums 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.soorlibblas.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.