Run MATLAB-Style Code Inside Python by Connecting Octave with the oct2py Library

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.

More from this stream

Recomended