Integrating Octave with Python: A Comprehensive Guide to MATLAB-Style Computing in Python
Discover how to effortlessly execute MATLAB-like code within Python by leveraging Octave through the oct2py interface. This tutorial walks you through setting up the environment on Google Colab, exchanging data between NumPy and Octave, creating and invoking .m scripts, rendering Octave plots inside Python, and handling advanced features such as toolboxes, structs, and .mat files. This approach combines Python’s versatility with the familiar syntax and numerical capabilities of MATLAB/Octave, enabling a unified and efficient computational workflow.
Environment Setup: Installing Octave and Python Dependencies on Google Colab
!apt-get -qq update
!apt-get -qq install -y octave gnuplot octave-signal octave-control > /dev/null
!python -m pip -q install oct2py scipy matplotlib pillow
After installing Octave and essential Octave-Forge packages alongside Python libraries, we initiate an Oct2Py session. To facilitate visualization, we define a helper function that displays PNG images generated by Octave plots directly within the Python notebook.
from oct2py import Oct2Py, Oct2PyError
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import savemat, loadmat
from PIL import Image
oc = Oct2Py()
print("Octave version:", oc.eval("version"))
def display_image(path, title=None):
img = Image.open(path)
plt.figure(figsize=(6,5))
plt.imshow(img)
plt.axis('off')
if title:
plt.title(title)
plt.show()
Executing Basic Octave Commands and Data Exchange with NumPy
We validate the Python-Octave bridge by performing fundamental matrix operations, eigenvalue computations, and trigonometric calculations directly in Octave. Next, we transfer NumPy arrays to Octave to apply a convolution filter, demonstrating seamless data interchange.
print("--- Basic Octave Evaluations ---")
print(oc.eval("A = magic(4); A"))
print("Eigenvalues diagonal:", oc.eval("[V,D] = eig(A); diag(D)'"))
print("sin(pi/4):", oc.eval("sin(pi/4)"))
print("n--- NumPy to Octave Data Transfer ---")
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x) + 0.1 * np.random.randn(x.size)
y_filtered = oc.feval("conv", y, np.ones(5)/5, "same")
print("Filtered signal shape:", np.asarray(y_filtered).shape)
Working with Octave Cells and Structs from Python
We demonstrate how Python lists can be pushed into Octave as cell arrays, then used to construct structs. These structs are subsequently retrieved back into Python, enabling complex data structures to be shared effortlessly between the two environments.
print("n--- Handling Cells and Structs ---")
cells = ["world", 123, [4, 5, 6]]
oc.push("C", cells)
oc.eval("s = struct('name', 'Grace', 'score', 88, 'tags', {C});")
struct_data = oc.pull("s")
print("Struct retrieved from Octave:", struct_data)
Creating and Invoking Custom Octave Functions from Python
We write a custom gradient_descent.m function in Octave to perform linear regression with gradient descent. This function is called from Python, returning estimated weights and loss history, confirming the integration’s effectiveness.
import textwrap
gd_function = r"""
function [weights, loss_history] = gradient_descent(X, y, alpha, iterations)
% Performs gradient descent for linear regression.
if size(X,2) == 0
error('Input X must be 2D');
end
n = rows(X);
X_bias = [ones(n,1), X];
m = columns(X_bias);
weights = zeros(m,1);
loss_history = zeros(iterations,1);
for i = 1:iterations
predictions = X_bias * weights;
gradient = (X_bias' * (predictions - y)) / n;
weights = weights - alpha * gradient;
loss_history(i) = sum((predictions - y).^2) / (2*n);
end
end
"""
with open("gradient_descent.m", "w") as file:
file.write(textwrap.dedent(gd_function))
np.random.seed(42)
X = np.random.randn(200, 3)
true_weights = np.array([1.5, -2.0, 0.7, 4.0])
y = true_weights[0] + X @ true_weights[1:] + 0.25 * np.random.randn(200)
weights_est, loss_hist = oc.gradient_descent(X, y.reshape(-1,1), 0.05, 150, nout=2)
print("Estimated weights:", np.ravel(weights_est))
print("Final loss value:", float(np.ravel(loss_hist)[-1]))
Visualizing Octave Plots within Python
We generate a damped sine wave plot in Octave using an off-screen figure, save it as a PNG, and display it inline in Python. This technique maintains a smooth visualization workflow without leaving the Python environment.
oc.eval("x = linspace(0, 4*pi, 500); y = sin(3*x) .* exp(-0.3*x);")
oc.eval("figure('visible', 'off'); plot(x, y, 'r-', 'LineWidth', 2); grid on; title('Damped Sine Wave (Octave)');")
plot_file = "/content/octave_plot.png"
oc.eval(f"print('{plot_file}', '-dpng'); close all;")
display_image(plot_file, title="Octave Plot Rendered in Python")
Utilizing Octave Packages: Signal Processing and Control Systems
By loading Octave’s signal and control packages, we design a Butterworth filter and apply it to a composite signal. The filtered output is then visualized in Python. Additionally, we explore function handles by evaluating an anonymous quadratic function and a named function defined in a separate .m file.
print("n--- Loading Signal and Control Packages ---")
try:
oc.eval("pkg load signal; pkg load control;")
print("Signal and control packages loaded successfully.")
except Oct2PyError as e:
print("Package loading failed:", str(e).splitlines()[0])
if 'signal' in oc.eval("pkg list"):
oc.push("t", np.linspace(0, 1, 1000))
oc.eval("x = sin(2*pi*10*t) + 0.3*sin(2*pi*60*t);")
oc.eval("[b,a] = butter(5, 15/(1000/2)); filtered_signal = filtfilt(b,a,x);")
filtered = oc.pull("filtered_signal")
plt.plot(filtered)
plt.title("Filtered Signal via Octave Signal Package")
plt.show()
print("n--- Function Handles and Named Functions ---")
oc.eval("""
quad_handle = @(z) z.^2 + 4*z + 1;
results = feval(quad_handle, [0, 1, 2, 3]);
""")
results = oc.pull("results")
print("Anonymous function results:", np.ravel(results))
quad_function_code = r"""
function output = quad_function(z)
output = z.^2 + 4*z + 1;
end
"""
with open("quad_function.m", "w") as f:
f.write(textwrap.dedent(quad_function_code))
results_named = oc.quad_function(np.array([0, 1, 2, 3], dtype=float))
print("Named function results:", np.ravel(results_named))
Seamless .mat File Interchange and Robust Error Handling
We demonstrate bidirectional data exchange using MATLAB-compatible .mat files, saving data in Python and loading it in Octave, then saving modified data back for Python to read. We also showcase error handling by capturing Octave exceptions as Python errors.
print("n--- .mat File Exchange ---")
data_dict = {"matrix": np.arange(16).reshape(4,4), "description": "sample data"}
savemat("sample.mat", data_dict)
oc.eval("load('sample.mat'); matrix_plus_one = matrix + 1;")
oc.eval("save('-mat', 'modified_sample.mat', 'matrix_plus_one', 'description');")
loaded_back = loadmat("modified_sample.mat")
print("Keys in loaded .mat file:", list(loaded_back.keys()))
print("n--- Handling Octave Errors in Python ---")
try:
oc.eval("undefined_function(10);")
except Oct2PyError as e:
print("Caught Octave error:", str(e).splitlines()[0])
Performance Comparison: Vectorized vs. Loop-Based Summation in Octave
To highlight Octave’s efficiency with vectorized operations, we benchmark summing a large random vector using both vectorized and loop-based methods, illustrating the significant speed advantage of vectorization.
oc.eval("N = 3e6; data = rand(N,1);")
oc.eval("tic; sum_vec = sum(data); time_vec = toc;")
time_vectorized = float(oc.pull("time_vec"))
oc.eval("tic; sum_loop = 0; for i=1:length(data), sum_loop += data(i); end; time_loop = toc;")
time_loop = float(oc.pull("time_loop"))
print(f"Vectorized sum time: {time_vectorized:.4f} seconds")
print(f"Loop sum time: {time_loop:.4f} seconds")
Building a Modular Octave Pipeline for Signal Processing
We create a multi-step Octave function that applies filtering and envelope detection to a signal, returning key metrics such as RMS and peak values. This modular approach demonstrates how to organize Octave code into reusable components callable from Python.
pipeline_code = r"""
function result = signal_pipeline(signal, fs)
try
pkg load signal;
catch
end
[b,a] = butter(6, 0.25);
filtered = filtfilt(b,a,signal);
envelope = abs(hilbert(filtered));
result = struct('rms', sqrt(mean(filtered.^2)), 'peak', max(abs(filtered)), 'env_sample', envelope(1:10));
end
"""
with open("signal_pipeline.m", "w") as f:
f.write(textwrap.dedent(pipeline_code))
fs = 250.0
time_vec = np.linspace(0, 4, int(4*fs))
signal = np.sin(2*np.pi*5*time_vec) + 0.15 * np.random.randn(len(time_vec))
output = oc.signal_pipeline(signal, fs, nout=1)
print("Pipeline output keys:", list(output.keys()))
print(f"RMS: {float(output['rms']):.3f}, Peak: {float(output['peak']):.3f}, Envelope sample: {np.ravel(output['env_sample'])[:5]}")
Summary
This guide illustrates how to integrate Octave’s MATLAB-compatible environment directly into Python, particularly within Google Colab. We covered environment setup, data exchange, custom function creation, plotting, package utilization, error management, and performance benchmarking. By combining Octave’s numerical strengths with Python’s extensive ecosystem, you can develop flexible, efficient workflows that harness the best of both worlds without switching contexts.
