modular / modular/modular

[Enhancement] [Python-Mojo bindings] Loosen `PythonObject` requirement for return type and arguments

Open
#4,986 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Easy to get & work with Mojo enhancement modular-repo mojo Team: Mojo Libraries
Dominant language
Mojo
Stars
29.8k
Forks
3.2k
PR merge metrics
No merged PRs in 30d

Description

Bug description
Actual behavior

The file provided below compiles correctly but the the resulting library doesn't expose any method from the mojo object.

Expected behavior

The object should expose all methods from the mojo compiled library.

Steps to reproduce

mojo file

from python import PythonObject
from python.bindings import PythonModuleBuilder
from os import abort
import math
from python import Python


@export
fn PyInit_odes() -> PythonObject:

    try:
        var m = PythonModuleBuilder("odes")
                # Expose the MFCModel struct as a Python class.
        _ = m.add_type[MFCModel]("MFCModel")

        # Expose the mfc_odes method on the MFCModel class.


        
        return m.finalize()

    except :
        print("Error creating MFCModel:")
        return PythonObject(None)


struct MFCModel( Defaultable, Movable, Representable, Copyable):
    """
    Microbial Fuel Cell (MFC) model parameters and state variables.
    (July 2025 API - Corrected Nested Syntax).
    """
    var F: Float64
    var R: Float64
    var T: Float64
    var k_m: Float64
    var d_m: Float64
    var k_aq: Float64
    var d_cell: Float64
    var C_a: Float64
    var C_c: Float64
    var V_a: Float64
    var V_c: Float64
    var A_m: Float64
    var Y_ac: Float64
    var K_dec: Float64
    var f_x: Float64
    var alpha: Float64
    var beta: Float64
    var k1_0: Float64
    var k2_0: Float64
    var K_AC: Float64
    var K_O2: Float64
    var Q_a: Float64        
    var Q_c: Float64
    var C_AC_in: Float64
    var C_CO2_in: Float64
    var X_in: Float64
    var C_H_in: Float64
    var C_O2_in: Float64
    var C_M_in: Float64
    var C_OH_in: Float64
    var U0: Float64
    var C_AC: Float64
    var C_CO2: Float64
    var C_H: Float64
    var X: Float64
    var C_O2: Float64
    var C_OH: Float64
    var C_M: Float64
    var eta_a: Float64
    var eta_c: Float64

        


    fn __init__(out self):
        """Initializes with default parameters using correct Mojo syntax."""
        self.F = 96485.332
        self.R = 8.314
        self.T = 303
        self.k_m = 17.0
        self.d_m = 1.778e-4
        self.k_aq = 5.0
        self.d_cell = 2.2e-2
        self.C_a = 4e2
        self.C_c = 5e2
        self.V_a = 5.5e-5
        self.V_c = 5.5e-5
        self.A_m = 5.0e-4
        self.Y_ac = 0.05
        self.K_dec = 8.33e-4
        self.f_x = 10.0
        self.alpha = 0.051
        self.beta = 0.063
        self.k1_0 = 0.207
        self.k2_0 = 3.288e-5
        self.K_AC = 0.592
        self.K_O2 = 0.004
        self.Q_a = 2.25e-5
        self.Q_c = 1.11e-3
        self.C_AC_in = 1.56
        self.C_CO2_in = 0.0
        self.X_in = 0.0
        self.C_H_in = 0.0
        self.C_O2_in = 0.3125
        self.C_M_in = 0.0
        self.C_OH_in = 0.0
        self.U0 = 0.77
        self.C_AC = 0.0
        self.C_CO2 = 0.0
        self.C_H = 0.0
        self.X = 0.0
        self.C_O2 = 0.0
        self.C_OH = 0.0
        self.C_M = 0.0
        self.eta_a = 0.0
        self.eta_c = 0.0
        
    fn __moveinit__(out self: Self, owned existing: Self):
        """
        Move initializer for MFCModel.
        This is called when an instance is moved, ensuring proper ownership transfer.
        """
        self.F = existing.F
        self.R = existing.R
        self.T = existing.T
        self.k_m = existing.k_m
        self.d_m = existing.d_m
        self.k_aq = existing.k_aq
        self.d_cell = existing.d_cell
        self.C_a = existing.C_a
        self.C_c = existing.C_c
        self.V_a = existing.V_a
        self.V_c = existing.V_c
        self.A_m = existing.A_m
        self.Y_ac = existing.Y_ac
        self.K_dec = existing.K_dec
        self.f_x = existing.f_x
        self.alpha = existing.alpha
        self.beta = existing.beta
        self.k1_0 = existing.k1_0
        self.k2_0 = existing.k2_0
        self.K_AC = existing.K_AC
        self.K_O2 = existing.K_O2
        self.Q_a = existing.Q_a        
        self.Q_c = existing.Q_c
        self.C_AC_in = existing.C_AC_in
        self.C_CO2_in = existing.C_CO2_in
        self.X_in = existing.X_in
        self.C_H_in = existing.C_H_in
        self.C_O2_in = existing.C_O2_in
        self.C_M_in = existing.C_M_in
        self.C_OH_in = existing.C_OH_in        
        self.U0 = existing.U0        
        self.C_AC = 0.0  # Reset to default value on move init.
        self.C_CO2 = 0.0  # Reset to default value on move init.
        self.C_H = 0.0  # Reset to default value on move init.
        self.X = 0.0  # Reset to default value on move init.
        self.C_O2 = 0.0  # Reset to default value on move init.
        self.C_OH = 0.0  # Reset to default value on move init.
        self.C_M = 0.0  # Reset to default value on move init.
        self.eta_a = 0.0  # Reset to default value on move init.
        self.eta_c = 0.0  # Reset to default value on move init.






    fn __copyinit__(out self: Self, existing: Self):
        """
        Copy initializer for MFCModel.
        This is called when an instance is copied, ensuring proper value transfer.
        """
        self.F = existing.F
        self.R = existing.R
        self.T = existing.T
        self.k_m = existing.k_m
        self.d_m = existing.d_m
        self.k_aq = existing.k_aq
        self.d_cell = existing.d_cell
        self.C_a = existing.C_a
        self.C_c = existing.C_c
        self.V_a = existing.V_a
        self.V_c = existing.V_c
        self.A_m = existing.A_m
        self.Y_ac = existing.Y_ac
        self.K_dec = existing.K_dec
        self.f_x = existing.f_x
        self.alpha = existing.alpha
        self.beta = existing.beta
        self.k1_0 = existing.k1_0
        self.k2_0 = existing.k2_0
        self.K_AC = existing.K_AC
        self.K_O2 = existing.K_O2
        self.Q_a = existing.Q_a        
        self.Q_c = existing.Q_c
        self.C_AC_in = existing.C_AC_in
        self.C_CO2_in = existing.C_CO2_in
        self.X_in = existing.X_in
        self.C_H_in = existing.C_H_in
        self.C_O2_in = existing.C_O2_in
        self.C_M_in = existing.C_M_in
        self.C_OH_in = existing.C_OH_in        
        self.U0 = existing.U0        
        self.C_AC = existing.C_AC
        self.C_CO2 = existing.C_CO2
        self.C_H = existing.C_H
        self.X = existing.X
        self.C_O2 = existing.C_O2
        self.C_OH = existing.C_OH
        self.C_M = existing.C_M
        self.eta_a = existing.eta_a
        self.eta_c = existing.eta_c

    fn __repr__(self: MFCModel) -> String:
        var repr = (
            "MFCModel("
            + "F={self.F}, R={self.R}, T={self.T}, k_m={self.k_m}, d_m={self.d_m}, "
            + "k_aq={self.k_aq}, d_cell={self.d_cell}, C_a={self.C_a}, C_c={self.C_c}, "
            + "V_a={self.V_a}, V_c={self.V_c}, A_m={self.A_m}, Y_ac={self.Y_ac}, "
            + "K_dec={self.K_dec}, f_x={self.f_x}, alpha={self.alpha}, beta={self.beta}, "
            + "k1_0={self.k1_0}, k2_0={self.k2_0}, K_AC={self.K_AC}, K_O2={self.K_O2}, "
            + "Q_a={self.Q_a}, Q_c={self.Q_c}, C_AC_in={self.C_AC_in}, C_CO2_in={self.C_CO2_in}, "
            + "X_in={self.X_in}, C_H_in={self.C_H_in}, C_O2_in={self.C_O2_in}, "
            + "C_M_in={self.C_M_in}, C_OH_in={self.C_OH_in}, U0={self.U0}, "
            + "C_AC={self.C_AC}, C_CO2={self.C_CO2}, C_H={self.C_H}, X={self.X}, "
            + "C_O2={self.C_O2}, C_OH={self.C_OH}, C_M={self.C_M}, "
            + "eta_a={self.eta_a}, eta_c={self.eta_c})"
        )
        return repr

    fn mfc_odes(self: MFCModel, t: Float64, y: List[Float64], i_fc: Float64) -> List[Float64]:
        """
        Calculates the derivatives. This now accepts a NumPy array directly
        and returns a Mojo List, which is automatically converted for Python.
        """
        # Add a check for the input array's length for robustness.
        try:
            if y._len != 9:
                # This error message will be propagated to Python as an exception.
                raise Error("Input list 'y' must have exactly 9 elements.")
        except:
            return List[Float64]()
    
        var C_AC = y[0]
        var C_CO2 = y[1]
        var C_H = y[2]
        var X = y[3]
        var C_O2 = y[4]
        var C_OH = y[5]
        var C_M = y[6]
        var eta_a = y[7]
        var eta_c = y[8]

        var r1 = self.k1_0 * math.exp((self.alpha * self.F) / (self.R * self.T) * eta_a) * (C_AC / (self.K_AC + C_AC)) * X 
        var r2 = -self.k2_0 * (C_O2 / (self.K_O2 + C_O2)) * math.exp((self.beta - 1.0) * self.F / (self.R * self.T) * eta_c)
        var N_M = (3600.0 * i_fc) / self.F

        var dC_AC_dt = (self.Q_a * (self.C_AC_in - C_AC) - self.A_m * r1) / self.V_a
        var dC_CO2_dt = (self.Q_a * (self.C_CO2_in - C_CO2) + 2.0 * self.A_m * r1) / self.V_a
        var dC_H_dt = (self.Q_a * (self.C_H_in - C_H) + 8.0 * self.A_m * r1) / self.V_a
        var dX_dt = (self.Q_a * (self.X_in - X) / self.f_x + self.A_m * self.Y_ac * r1) / self.V_a - self.K_dec * X

        var dC_O2_dt = (self.Q_c * (self.C_O2_in - C_O2) + r2 * self.A_m) / self.V_c
        var dC_OH_dt = (self.Q_c * (self.C_OH_in - C_OH) - 4.0 * r2 * self.A_m) / self.V_c
        var dC_M_dt = (self.Q_c * (self.C_M_in - C_M) + N_M * self.A_m) / self.V_c
        
        var d_eta_a_dt = (3600.0 * i_fc - 8.0 * self.F * r1) / self.C_a
        var d_eta_c_dt = (-3600.0 * i_fc - 4.0 * self.F * r2) / self.C_c

        var derivatives = List[Float64]()
        derivatives.append(dC_AC_dt)
        derivatives.append(dC_CO2_dt)
        derivatives.append(dC_H_dt)            
        derivatives.append(dX_dt)
        derivatives.append(dC_O2_dt)
        derivatives.append(dC_OH_dt)
        derivatives.append(dC_M_dt)
        derivatives.append(d_eta_a_dt)
        derivatives.append(d_eta_c_dt)
        return derivatives

and python file:


# With the July 2025 API, compiling the Mojo file creates a standard
# Python module. The legacy `max.mojo.importer` is no longer needed.


import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
import max.mojo.importer
import sys

sys.path.insert(0, "")
import odes  # Import the Mojo module compiled from odes.mojo
# 1. Initialize the Mojo model struct
mfc = odes.MFCModel()  # Create an instance of the MFCModel

# 2. Define initial conditions for the state variables [y0]
# C_AC, C_CO2, C_H, X, C_O2, C_OH, C_M, eta_a, eta_c
y0 = [
    1.5,      # C_AC: Start with near-influent concentration
    0.1,      # C_CO2: Small initial amount
    1e-4,     # C_H: Corresponds to pH 4
    0.1,      # X: Initial biomass
    0.3,      # C_O2: Start with near-influent concentration
    1e-7,     # C_OH: Corresponds to pH 7
    0.1,      # C_M: Small initial amount
    0.01,     # eta_a: Small initial anodic overpotential
    -0.01     # eta_c: Small initial cathodic overpotential
]

# 3. Define simulation time span
t_span = [0, 100]  # Simulate for 100 hours
t_eval = np.linspace(t_span[0], t_span[1], 500) # Points to evaluate solution

# 4. Set a constant current density for this example
constant_i_fc = 1.0 # A/m²

# 5. Run the ODE solver, passing the Mojo function as the "fun" argument.
#    The core calculations are now executed by the high-performance Mojo code.
solution = solve_ivp(
    fun=lambda t, y: mfc.mfc_odes(t, y, constant_i_fc),
    t_span=t_span,
    y0=y0,
    t_eval=t_eval,
    method='RK45'
)

# 6. Plot the results using Matplotlib
plt.style.use('seaborn-v0_8-whitegrid')
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(solution.t, solution.y[0], label='Acetate Concentration (C_AC)', color='b')
ax.set_xlabel('Time (hours)', fontsize=12)
ax.set_ylabel('Concentration (mol/m³)', fontsize=12)
ax.set_title('Mojo-Powered MFC Simulation: Acetate Concentration', fontsize=14)
ax.legend()
ax.grid(True)
plt.show()

System information

(mojo-practice) uge@uge-X670E-AORUS-XTREME:~/modular/mojo-practice/q-learning-mfcs$ pixi info
System

   Pixi version: 0.49.0
       Platform: linux-64

Virtual packages: __unix=0=0
: __linux=6.11.0=0
: __glibc=2.39=0
: __archspec=1=zen4
Cache dir: /home/uge/.cache/rattler/cache
Auth storage: /home/uge/.rattler/credentials.json
Config locations: No config files found

Global

        Bin dir: /home/uge/.pixi/bin
Environment dir: /home/uge/.pixi/envs
   Manifest dir: /home/uge/.pixi/manifests/pixi-global.toml

Workspace

           Name: mojo-practice
        Version: 0.1.0
  Manifest file: /home/uge/modular/mojo-practice/pixi.toml
   Last updated: 08-07-2025 00:41:49

Environments

    Environment: default
       Features: default
       Channels: conda-forge, https://conda.modular.com/max-nightly, https://repo.prefix.dev/modular, https://repo.prefix.dev/mojo, https://repo.prefix.dev/modular-community

Dependency count: 6
Dependencies: scipy, matplotlib, python, numpy, modular, magic
Target platforms: linux-aarch64, linux-64
Prefix location: /home/uge/modular/mojo-practice/.pixi/envs/default

Package Version Build Size Kind Source
max-core 25.5.0.dev2025070705 release 214 MiB conda https://conda.modular.com/max-nightly/
max-pipelines 25.5.0.dev2025070705 release 9.8 KiB conda https://conda.modular.com/max-nightly/
max-python 25.5.0.dev2025070705 release 30.2 MiB conda https://conda.modular.com/max-nightly/

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the provided Mojo example at @export PyInit_odes, the PythonModuleBuilder setup, and the Python call to mfc.MFCModel.mfc_odes; reproduce the missing-method behavior with the shown Python and Mojo entry points. Done means the compiled module exposes the MFCModel methods and accepts the demonstrated return type and arguments without the current PythonObject restriction.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.