Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test-expressions:
name: Python expression tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Run expression tests
run: python tests/test_expressions.py

test-solve:
name: End-to-end solve (juliacall + HiGHS)
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
julia-version: ["1.11", "1.12"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.julia-version }}

- uses: julia-actions/cache@v2

- name: Install juliacall
run: pip install juliacall

- name: Preinstall Julia packages
run: |
julia -e '
using Pkg
Pkg.add(["MathOptInterface", "HiGHS"])
using MathOptInterface
using HiGHS
'

- name: Run solve tests
run: python tests/test_solve.py
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__/
*.pyc
*.egg-info/
dist/
build/
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ version = "0.1.0"
description = "A Python interface to JuMP's MathOptInterface via GeneratorOptInterface"
requires-python = ">=3.10"
license = "MIT"
dependencies = [
"numpy",
]
dependencies = []

[project.optional-dependencies]
juliacall = ["juliacall>=0.9.23"]

[tool.hatch.build.targets.wheel]
packages = ["src/jumpy"]
Binary file removed src/jumpy/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file removed src/jumpy/__pycache__/expressions.cpython-311.pyc
Binary file not shown.
Binary file removed src/jumpy/__pycache__/iterators.cpython-311.pyc
Binary file not shown.
Binary file removed src/jumpy/__pycache__/model.cpython-311.pyc
Binary file not shown.
Binary file removed src/jumpy/__pycache__/serialize.cpython-311.pyc
Binary file not shown.
140 changes: 140 additions & 0 deletions src/jumpy/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""
Solver backend abstraction.

Two backends:
- "juliac" (default): calls a precompiled shared library via ctypes.
No Julia installation required.
- "juliacall": calls MOI + GenOpt + HiGHS through juliacall.
Requires Julia (installed lazily by juliacall on first use).
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from jumpy.model import Model


class Backend(ABC):
"""Abstract solver backend."""

@abstractmethod
def optimize(self, model: Model) -> list[float]:
"""Solve the model and return the solution vector."""
...


class JuliacBackend(Backend):
"""
Default backend: calls a precompiled Julia shared library via ctypes.

The library is built with juliac from:
MOI + GenOpt + Bridges + HiGHS

No Julia installation required.
"""

def __init__(self):
self._lib = None

def _load_lib(self):
if self._lib is not None:
return
import ctypes
import importlib.resources
# TODO: resolve platform-specific library path
# For now, search standard locations
import os
lib_names = [
"libjumpy_backend.so",
"libjumpy_backend.dylib",
"jumpy_backend.dll",
]
for name in lib_names:
for search_dir in [os.path.dirname(__file__), os.getcwd(), "/usr/local/lib"]:
path = os.path.join(search_dir, name)
if os.path.exists(path):
self._lib = ctypes.CDLL(path)
return
raise FileNotFoundError(
"Could not find the compiled JuMPy backend library.\n"
"The juliac-compiled shared library (libjumpy_backend.so) is not installed.\n"
"Either:\n"
" 1. Install the pre-built wheel: pip install jumpy\n"
" 2. Use the juliacall backend: jp.Model(backend='juliacall')\n"
)

def optimize(self, model: Model) -> list[float]:
self._load_lib()
data = model._serialize()
# TODO: implement ctypes calls to the compiled library
raise NotImplementedError(
"juliac backend not yet compiled. "
"Use jp.Model(backend='juliacall') for now."
)


class JuliaCallBackend(Backend):
"""
Optional backend: calls Julia directly through juliacall.

Requires `pip install jumpy[juliacall]`. Julia is installed lazily
by juliacall on first use if not already present.

This backend has full flexibility — it can use any solver or MOI
feature, not just what's compiled into the juliac library.
"""

def __init__(self):
self._jl = None

def _init_julia(self):
if self._jl is not None:
return
try:
from juliacall import Main as jl
except ImportError:
raise ImportError(
"juliacall is not installed.\n"
"Install it with: pip install jumpy[juliacall]\n"
"This will also install Julia automatically if needed."
) from None
# Install and load Julia packages on first use
jl.seval("using Pkg")
for pkg in ["MathOptInterface", "HiGHS", "GenOpt"]:
jl.seval(f"""
if !haskey(Pkg.project().dependencies, "{pkg}")
Pkg.add("{pkg}")
end
""")
jl.seval("import MathOptInterface as MOI")
jl.seval("import GenOpt")
jl.seval("import HiGHS")
# TODO: load GenOpt once it's registered / available
self._jl = jl

def optimize(self, model: Model) -> list[float]:
self._init_julia()
jl = self._jl
return self._build_and_solve(jl, model)

def _build_and_solve(self, jl, model: Model) -> list[float]:
from jumpy.bridge_juliacall import build_moi_model
return build_moi_model(jl, model)


_BACKENDS = {
"juliac": JuliacBackend,
"juliacall": JuliaCallBackend,
}


def get_backend(name: str) -> Backend:
cls = _BACKENDS.get(name)
if cls is None:
raise ValueError(
f"Unknown backend '{name}'. Choose from: {list(_BACKENDS.keys())}"
)
return cls()
Loading
Loading