Run PRIK in a Notebook¶
This tutorial compiles Fortran and C in notebook cells, calls them from Python, and then reshapes the generated API by editing its semantic contract — all in one session.
▶ Run it in Colab ⬇ Download the notebook
The notebook runs top to bottom and builds real extension modules, so it needs a compiler. In Colab the first cell installs one, along with PRIK:
pip install "prik[jupyter]"
1. Load the extension¶
%load_ext prik.jupyter
2. Compile a Fortran cell¶
%%fortran compiles the cell and publishes what it declares. A Fortran module
becomes a notebook name:
%%fortran
module geometry
contains
real(8) function circle_area(radius)
real(8), intent(in) :: radius
circle_area = 3.141592653589793d0 * radius**2
end function
end module
area = geometry.circle_area(np.float64(2.0))
assert np.isclose(area, np.pi * 4)
print(f"✅ circle_area(2.0) = {area} (expected {np.pi * 4})")
✅ circle_area(2.0) = 12.566370614359172 (expected 12.566370614359172)
Every result the notebook claims is asserted, so a ✅ means the cell really did that rather than the page saying so.
3. Compile a C cell¶
%%c publishes C functions directly. This one doubles an array in place and
takes the element count the way C usually does:
%%c
#include <stddef.h>
void scale(size_t count, double *values) {
for (size_t index = 0; index < count; ++index) {
values[index] *= 2.0;
}
}
double *values becomes runtime-rank storage, so it accepts a NumPy array of
any rank and writes through it. The count still has to be passed by hand,
though NumPy already knows it:
values = np.array([1.0, 2.0, 3.0])
scale(np.uintp(values.size), values)
assert np.allclose(values, [2.0, 4.0, 6.0])
print(f"✅ scale(count, values) doubled in place: {values} (expected [2. 4. 6.])")
✅ scale(count, values) doubled in place: [2. 4. 6.] (expected [2. 4. 6.])
4. Reshape the API with a contract¶
--pyi compiles nothing. It keeps the source and hands back the semantic
contract it derived, as an editable cell:
%%c --pyi
#include <stddef.h>
void scale(size_t count, double *values) {
for (size_t index = 0; index < count; ++index) {
values[index] *= 2.0;
}
}
Jupyter and Colab insert the contract below the cell you just ran:
%%pyi
# prik: source-sha256=<generated digest>
from prik.contracts import Float64, UInt64
def scale(
count: UInt64,
values: Float64[...]
) -> None: ...
Edit it so the count comes from the array. Arg(0).size supplies it, and
Float64[:] pins the rank to one. Keep the # prik: line, then run the cell:
%%pyi
# prik: source-sha256=<generated digest>
from prik.contracts import Arg, Float64, native_call
@native_call([Arg(0).size, Arg(0)])
def scale(values: Float64[:]) -> None: ...
Same C code, same compiler; only the Python API changed — count is gone:
values = np.array([1.0, 2.0, 3.0])
scale(values)
assert np.allclose(values, [2.0, 4.0, 6.0])
print(f"✅ scale(values) doubled in place: {values} (expected [2. 4. 6.])")
✅ scale(values) doubled in place: [2. 4. 6.] (expected [2. 4. 6.])
Where to go next¶
- IPython and Jupyter Notebooks covers every magic, its options, and the cell cache.
- C Pointers, Arrays, and Strings
explains what
Float64[...]accepts and how to narrow it. - Design a Pythonic BLAS API applies the same contract editing to a real library.