Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch from pyjulia to juliacall #12

Merged
merged 12 commits into from
Aug 4, 2024
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
2 changes: 2 additions & 0 deletions .conda/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
juliaup update
python -c "from pyrms import rms"
42 changes: 42 additions & 0 deletions .conda/meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#for conda build
package:
name: pyrms
version: {{ environ.get('GIT_DESCRIBE_TAG', '') }}

source:
path: ../

build:
number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }}
script_env:
- JULIAUP_DEPOT_PATH=$PREFIX/share/juliaup
- JULIA_DEPOT_PATH=$PREFIX/share/julia
- JULIA_CONDAPKG_ENV=$PREFIX

requirements:
host:
- pyjuliacall
- rmgmolecule >=0.3
- rdkit >=2015.09.2
- setuptools
- juliaup
- numpy
run:
- pydot >=2.0
- yaml
- pyjuliacall
- rmgmolecule >=0.3
- rdkit >=2015.09.2
- juliaup
- jupyter
- matplotlib
- nose
test:
imports:
- pyrms
commands:
- nosetests pyrms/rmsTest.py # [unix]
about:
home: http://github.com/ReactionMechanismGenerator/pyrms
license: MIT
summary: "A program for simulating and analyzing kinetic models of chemical reaction mechanisms."
53 changes: 53 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI

on:
push:
branches:
- main
tags: '*'
pull_request:
workflow_dispatch:

jobs:
test:
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash -l {0}
strategy:
matrix:
os:
- ubuntu-latest
architecture: [x64]
python-version: ['3.9']
fail-fast: false
name: Test ${{ matrix.os }} ${{ matrix.architecture }}
Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v1
- name: Setup python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
- name: Setup Conda Environment
uses: conda-incubator/setup-miniconda@v3
with:
activate-environment: rms_env
environment-file: environment.yml
miniforge-variant: Mambaforge
miniforge-version: "latest"
python-version: ${{ matrix.python-version }}
use-mamba: true
- name: Install packages
run: |
python -m pip install pytest nbmake
python -m pip install -e .
- name: Conda Info
run: |
mamba info
mamba list
- name: Run test
run: |
nosetests pyrms/rmsTest.py
pytest --nbmake Ipython/pyrms_example.ipynb
22 changes: 11 additions & 11 deletions Ipython/pyrms example.ipynb → Ipython/pyrms_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"metadata": {},
"outputs": [],
"source": [
"from diffeqpy import de\n",
"from pyrms import rms\n",
"from juliacall import Main as jl\n",
"from matplotlib import pyplot as plt\n",
"%matplotlib inline"
]
Expand Down Expand Up @@ -55,7 +55,7 @@
"metadata": {},
"outputs": [],
"source": [
"domain,y0 = rms.ConstantTPDomain(phase=ig,initialconds=initialconds)"
"domain,y0,p = rms.ConstantTPDomain(phase=ig,initialconds=jl.convert(jl.Dict,initialconds))"
]
},
{
Expand All @@ -64,7 +64,7 @@
"metadata": {},
"outputs": [],
"source": [
"react = rms.Reactor(domain,y0,(0.0,10.001))"
"react = rms.Reactor(domain,y0,(0.0,10.001),p=p)"
]
},
{
Expand All @@ -73,7 +73,7 @@
"metadata": {},
"outputs": [],
"source": [
"sol = de.solve(react.ode,de.CVODE_BDF(),abstol=1e-20,reltol=1e-8)"
"sol = rms.solve(react.ode,rms.CVODE_BDF(),abstol=1e-20,reltol=1e-8)"
]
},
{
Expand All @@ -100,7 +100,7 @@
"metadata": {},
"outputs": [],
"source": [
"rms.plotrops(sim,\"OH(D)\",1.0,N=10)"
"rms.plotrops(sim,\"OH\",1.0,N=10)"
]
},
{
Expand All @@ -122,23 +122,23 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python2"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.15"
"pygments_lexer": "ipython3",
"version": "3.9.19"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ In theory this wraps all functionality within RMS with two caveats:

1) In jupyter notebooks julia objects don't display the same way in the python kernel as they would in the julia kernel. For example flux diagram generation had to be hard coded into this wrapper to display properly. If this happens please make an issue.

2) There are ways to define julia functions that makes them impossible to call from python using the pyjulia. In most cases this is easy to fix. If you find a case where this happens please make an issue.
2) There are ways to define julia functions that makes them impossible to call from python. In most cases this is easy to fix. If you find a case where this happens please make an issue.

## Installation Instructions from Anaconda:

Note: We recommend installing pyrms in its own conda environment when convenient because getting pyjulia to work with conda python efficiently currently requires relinking
the python executable to the python-jl executable, this usually isn't problematic, but it can be an issue in more complicated conda environments.
We recommend installing pyrms in its own conda environment

To Install Binaries with Conda Run:
To Install Binaries with Conda Run:
```
conda install -c rmg pyrms
python -c "import pyrms; pyrms.install()"
ln -sfn $(which python-jl) $(which python)
```

Note the install process occurs automatically on your first import.
15 changes: 15 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: pyrms_env
channels:
- defaults
- mjohnson541
- conda-forge
dependencies:
- juliaup
- nose
- pyjuliacall
- rmgmolecule >= 0.3
- rdkit
- pydot >= 2.0
- matplotlib
- numpy
- jupyter
55 changes: 42 additions & 13 deletions pyrms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
import os
import subprocess
import shutil
from subprocess import run
# juliacall must be loaded after `_ensure_julia_installed()` is run,
# so this import is in `load_julia_packages()`
# from juliacall import Main

def install():
def _find_julia():
return shutil.which("julia")

def _ensure_julia_installed():
if not _find_julia():
print("No Julia version found. Installing Julia.")
run("juliaup update")
if not _find_julia():
raise RuntimeError(
"Julia installed with jill but `julia` binary cannot be found in the path"
)

# TODO: upstream this function or an alternative into juliacall
def load_julia_packages(*names):
"""
Install Julia packages required for diffeqpy.
Load Julia packages and return references to them, automatically installing julia and
the packages as necessary.
"""
import julia
import diffeqpy
julia.install()
diffeqpy.install()
from julia.api import Julia
jl = Julia(compiled_modules=False)
from julia import Pkg
Pkg.add("ReactionMechanismSimulator")
from julia import ReactionMechanismSimulator
# This is terrifying to many people. However, it seems SciML takes pragmatic approach.
_ensure_julia_installed()

script = """import Pkg
Pkg.activate(\"pyrms\", shared=true)
try
import {0}
catch e
e isa ArgumentError || throw(e)
Pkg.add([{1}])
import {0}
end
{0}""".format(", ".join(names), ", ".join(f'"{name}"' for name in names))

# Unfortunately, `seval` doesn't support multi-line strings
# https://github.com/JuliaPy/PythonCall.jl/issues/433
script = script.replace("\n", ";")

# Must be loaded after `_ensure_julia_installed()`
from juliacall import Main
return Main.seval(script)
58 changes: 4 additions & 54 deletions pyrms/rms.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,9 @@
import os
import sys
from IPython.display import Image
import IPython.display
from . import load_julia_packages
rms, _, = load_julia_packages("ReactionMechanismSimulator", "PythonCall")
from juliacall import Main

from julia import Main
from julia import ReactionMechanismSimulator
sys.modules[__name__] = rms

from julia.ReactionMechanismSimulator import makefluxdiagrams

def pygetfluxdiagram(bsol,t,centralspecieslist=[],superimpose=False,
maximumnodecount=50, maximumedgecount=50, concentrationtol=1e-6, speciesratetolerance=1e-6,
maximumnodepenwidth=10.0,maximumedgepenwidth=10.0,radius=1,centralreactioncount=-1,outputdirectory="fluxdiagrams",
colorscheme="viridis"):

fd = makefluxdiagrams(bsol,[t], centralspecieslist=centralspecieslist,superimpose=superimpose,
maximumnodecount=maximumnodecount, maximumedgecount=maximumedgecount, concentrationtol=concentrationtol,
speciesratetolerance=speciesratetolerance,maximumnodepenwidth=maximumnodepenwidth,
maximumedgepenwidth=maximumedgepenwidth,radius=radius,centralreactioncount=centralreactioncount,
outputdirectory=outputdirectory,colorscheme=colorscheme)

IPython.display.display(IPython.display.Image(os.path.join(fd.outputdirectory,"flux_diagram_1.png")))

ReactionMechanismSimulator.getfluxdiagram = pygetfluxdiagram

# These functions force the returned object to be a jlwrap object,
# to avoid the solution object gets converted into list of lists
_threadedsensitivities_inds = Main.pyfunctionret(ReactionMechanismSimulator.threadedsensitivities, Main.Any, Main.PyAny, Main.PyAny)
_threadedsensitivities = Main.pyfunctionret(ReactionMechanismSimulator.threadedsensitivities, Main.Any, Main.PyAny)
# Allow us the get the solution object in the julia wrapped object without
# it being converted into list of lists
get = Main.pyfunctionret(Main.get, Main.Any, Main.PyAny, Main.PyAny, Main.PyAny)
def pythreadedsensitivities(react, inds=None,
odesolver=Main.nothing, senssolver=Main.nothing,
odekwargs={"abstol": 1e-20, "reltol": 1e-6},
senskwargs={"abstol": 1e-6, "reltol": 1e-3}):
if inds is not None:
# the sol_dict returned here is a jlwrap object and is not easy to access the contents
sol_dict = _threadedsensitivities_inds(react, inds,
odesolver=odesolver, senssolver=senssolver,
odekwargs=odekwargs, senskwargs=senskwargs)
# pysol_dict is the python dictionary, but the solution object got turned into list of lists
pysol_dict = Main.PyObject(sol_dict)
# this remakes the dictionary with the values as julia wrapped object ODEsolution,
# but at the same time allows the user to access it normally like a dictionary
return {key: get(sol_dict, key, Main.Any) for key in pysol_dict.keys()}
else:
sol = _threadedsensitivities(react,
odesolver=odesolver, senssolver=senssolver,
odekwargs=odekwargs, senskwargs=senskwargs)
return sol

ReactionMechanismSimulator.threadedsensitivities = pythreadedsensitivities

for item in dir(ReactionMechanismSimulator):
try:
locals()[item] = getattr(ReactionMechanismSimulator,item)
except AttributeError:
pass
9 changes: 4 additions & 5 deletions pyrms/rmsTest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import unittest
import math

from pyrms import rms
from diffeqpy import de
from juliacall import Main as jl

class Testrms(unittest.TestCase):
def test_simulate(self):
Expand All @@ -11,8 +10,8 @@ def test_simulate(self):
rxns = phaseDict["gas"]["Reactions"]
ig = rms.IdealGas(spcs,rxns,name="gas")
initialconds = {"T":1000.0,"P":10.0e5,"H2":2.0,"O2":1.0}
domain,y0 = rms.ConstantTPDomain(phase=ig,initialconds=initialconds)
react = rms.Reactor(domain,y0,(0.0,10.001))
sol = de.solve(react.ode,de.CVODE_BDF(),abstol=1e-20,reltol=1e-8)
domain,y0,p = rms.ConstantTPDomain(phase=ig,initialconds=jl.convert(jl.Dict,initialconds))
react = rms.Reactor(domain,y0,(0.0,10.001),p=p)
sol = rms.solve(react.ode,rms.CVODE_BDF(),abstol=1e-20,reltol=1e-8)
sim = rms.Simulation(sol,domain)
self.assertAlmostEqual(rms.molefractions(sim,"H2",3.0),0.34445669,places=3)
Loading
Loading