Victor Björklund

Integrating Python with Elixir Using Erlport

Published: Dec 30 2023

Introduction

Elixir is a great language with some amazing libraries but once in a while you encounter a problem that can be solved easier or better in Python (often because the size of the Python community is so much larger). I recently come across such an issue when I needed to solve an optimization problem and couldn’t find any solvers in Elixir that was easy to work with and well-maintained.

The problem just took a few minutes to solve in Python however so I decided that I will just call Python from Elixir instead of trying to bring the solution into Elixir.

There are a few different ways we can call Python from Elixir but one of the most used and trusted ways is to use an Erlang library called Erlport. Erlport provides the capability to call Python functions from Elixir by sending messages from your process inside the BEAM to a process running Python.

This is particularly useful when you have a Python script that you wish to integrate with your Elixir application. This blog post demonstrates how to use Erlport to communicate with a Python program. We’ll use a Python script that solves an optimization problem using the PuLP library.

Our problem

Let’s take a hypothetical problem to solve. We have a factory that produces two types of products, A and B. Product A requires 2 hours of machine time and 3 hours of labor. Product B requires 1 hour of machine time and 2 hours of labor. The profit from each Product A is $50, and from Product B is $40. The goal is to maximize the weekly profit given a certain number of machine hours and labour hours that we can provide.

Python Script Overview

from pulp import *

def maximize_profit(machine_time, labor_time):
    # Define the problem
    prob = LpProblem("Maximize Profit", LpMaximize)

    # Decision variables
    A = LpVariable("Product_A", 0) # Product A units
    B = LpVariable("Product_B", 0) # Product B units

    # Objective function
    prob += 50 * A + 40 * B, "Total Profit"

    # Constraints
    prob += 1 * A + 2 * B <= machine_time, "Machine Time"
    prob += 3 * A + 2 * B <= labor_time, "Labor Time"

    # Solve the problem
    prob.solve()

    # Output the results
    return A.varValue, B.varValue, value(prob.objective)

Let’s take a hypothetical problem to solve. We have a factory that produces two types of products, A and B. Product A requires 1 hour of machine time and 3 hours of labor. Product B requires 2 hours of machine time and 2 hours of labor. The profit from each Product A is $50, and from Product B is $40. The goal is to maximize the weekly profit given a certain number of machine hours and labour hours that we can provide.

The Python script maximize_profit employs the PuLP library for solving a linear programming problem. It aims to maximize weekly profit in a factory that produces two types of products, A and B, considering constraints on machine time and labor hours.

Setting Up Erlport in Elixir

First, add Erlport to your mix.exs file:

defp deps do
  [
    {:erlport, "~> 0.10"}
  ]
end

Then run mix deps.get to fetch the dependency.

Python Environment

Ensure your Python environment is set up and that PuLP is installed. Erlport utilizes the Python interpreter in your Elixir application’s environment.

Communicating with Python

To invoke the Python function, start a Python interpreter and load the script in Elixir. For a script named optimizer.py, here’s how:

defmodule ProfitOptimizer do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def init(:ok) do
    {:ok, python} = :python.start_link(python_path: "python3")
    :python.call(python, :erlport.erlterms, :decode, [File.read!("profit_optimizer.py")])
    {:ok, python}
  end

  def maximize_profit(machine_hours, labor_hours) do
    GenServer.call(__MODULE__, {:maximize_profit, machine_hours, labor_hours})
  end

  def handle_call({:maximize_profit, machine_hours, labor_hours}, _from, python) do
    args = [machine_hours, labor_hours]
    result = :python.call(python, :profit_optimizer, :maximize_profit, args)
    {:reply, result, python}
  end
end

Explanation

  • ProfitOptimizer, a GenServer, is created for Python interaction.
  • init/1 initializes the Python interpreter and loads the script.
  • maximize_profit/2 serves as a public interface to the Python script.
  • handle_call/3 executes the Python function with required arguments.

Conclusion

Integrating Python scripts into Elixir applications with Erlport unlocks Python’s vast library ecosystem. This guide provides a basic framework for such integration, applicable in various scenarios. Experimentation and exploration are key to effectively leveraging Python within your Elixir projects.