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

Use julia_project to manage Julia dependency #100

Closed
wants to merge 7 commits into from
Closed
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ docs/_build/

# PyBuilder
target/

**/Manifest.toml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does julia_project generate files inside diffeqpy directory? I don't think that's a good idea. For example, the directory may not be writable after the installation.

I see Pluto is using ~/.julia/environments/__pluto_$VERSION for its internal env. So, similarly, maybe you can generate ~/.julia/environments/__python_julia_project_$VERSION_$SLUG/ where $VERSION is the Julia version and $SLUG is a hash of the path of the current python environment (say).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does. But, more than just Manifest.toml and Project.toml. It optionally puts a depo in ./depot in the diffeqpy directory. I also don't like it, but this was easier to start with. I agree it should be changed. Probably under ~/.julia/julia_project. Both juliaup and pythoncall claim directories there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's very rare that you'd need a separate depot. If a Julia environment is enough, I think that's much better. It definitely helps reduce precompile time. It's different from how Python virtualenv/venv works but I think a slug-based mapping is enough for bridging the gap.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather just use a Julia environment as well. In fact, I do use one. But the separate depot is just to work around the problem of incompatible libpython. I want to avoid having multiple python projects fighting over PyCall, always rebuilding it.... Or maybe I don't follow what you mean here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a great understanding of the problem with incompatible libpython. My assumption, which I am not sure of, is that it is not enough to have a separate .ji file for PyCall for each python project. Some other .ji files might need to be recompiled and be incompatible as well. Or, is it enough to use multiple depots and one of them only contains PyCall (including the compiled .ji file) ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the separate depot is just to work around the problem of incompatible libpython.

I'm pretty sure the direction of JuliaPy/PyCall.jl#945 makes this hack unnecessary. It let us configure libpython for each Julia project and so PyCall.jl can be precompiled for each libpython. It'll be a game changer for how Julia packages are used from Python.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow. I looked around quite a bit, but missed this. It's a big deal. I'll have to do some reading to absorb it all.

**/*.so
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

The MIT License (MIT)

Copyright (c) 2018: Chris Rackauckas.
Copyright (c) 2018, 2022: Chris Rackauckas.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
include README.md
include LICENSE.md
include docs/*.txt
include diffeqpy/sys_image/*.jl
include diffeqpy/sys_image/Project.toml
include diffeqpy/Project.toml
include diffeqpy/*.jl
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,14 @@ To install diffeqpy, use pip:
pip install diffeqpy
```

Using diffeqpy requires that Julia is installed and in the path, along
with DifferentialEquations.jl and PyCall.jl. To install Julia,
download a generic binary from
You can either install Julia yourself, or allow diffeqpy to do this for you.
To install Julia yourself, download a generic binary from
[the JuliaLang site](https://julialang.org/downloads/) and add it to your path.
To install Julia packages required for diffeqpy, open up Python
interpreter then run:

```pycon
>>> import diffeqpy
>>> diffeqpy.install()
```

and you're good! In addition, to improve the performance of your code it is
Expand Down
5 changes: 5 additions & 0 deletions diffeqpy/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[deps]
DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
38 changes: 2 additions & 36 deletions diffeqpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,3 @@
import os
import shutil
import subprocess
import sys
from ._julia_project import julia, compile_diffeqpy, update_diffeqpy

from jill.install import install_julia

script_dir = os.path.dirname(os.path.realpath(__file__))


def _find_julia():
# TODO: this should probably fallback to query jill
return shutil.which("julia")


def install(*, confirm=False):
"""
Install Julia (if required) and Julia packages required for diffeqpy.
"""
julia = _find_julia()
if not julia:
print("No Julia version found. Installing Julia.")
install_julia(confirm=confirm)
julia = _find_julia()
if not julia:
raise RuntimeError(
"Julia installed with jill but `julia` binary cannot be found in the path"
)
env = os.environ.copy()
env["PYTHON"] = sys.executable
subprocess.check_call([julia, os.path.join(script_dir, "install.jl")], env=env)


def _ensure_installed(*kwargs):
if not _find_julia():
# TODO: this should probably ensure that packages are installed too
install(*kwargs)
from ._version import __version__
38 changes: 38 additions & 0 deletions diffeqpy/_julia_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import julia
import logging

from julia_project import JuliaProject

import os
diffeqpy_path = os.path.dirname(os.path.abspath(__file__))

julia_project = JuliaProject(
name="diffeqpy",
package_path=diffeqpy_path,
preferred_julia_versions = ['1.7', '1.6', 'latest'],
env_prefix = 'DIFFEQPY_',
logging_level = logging.INFO, # or logging.WARN,
console_logging=False
)
Comment on lines +9 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason why to do this in Python? Since Chris is the maintainer of this package, it's very likely he and his contributors want to deal with Julia programs than in Python. That's why I wrote things mainly in Julia (e.g., install.jl) and put a thin wrapper of Python.

Is this because of julia binary discovery and installation?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the gist, but maybe not precisely what you are asking. Yes, JuliaProject does binary discovery and installation via another python package, find_julia, which uses jill.py. That part could be separated out, and the much of the rest could be done in Julia. I considered this. One argument for using Python is that errors and stack traces are more likely to be in Python this way. And a Python user is more likely to be able to spot an error, or make a suggestion or improvement. I'm trying, to the extent possible, to hide Julia from the end user. Admittedly, in diffeqpy, the interface to Julia is rather low level. But in other python projects, Julia is better hidden behind a Python API. I often work in environments where 0% of the people have enough interest in Julia to ever try it for anything. And 100% of the people love Python. Something like julia_project is a good way to introduce the possibility of Julia. On the other hand, I'll think more about whether more of it might be better written in Julia.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I often work in environments where 0% of the people have enough interest in Julia to ever try it for anything. And 100% of the people love Python. Something like julia_project is a good way to introduce the possibility of Julia. On the other hand, I'll think more about whether more of it might be better written in Julia.

I 100% agree with this. The only reason I could get my team to agree to use diffeqpy in our project is because of the speedup relative to SciPy ODE solvers. But the users of my project would not be interested in having to work with Julia code.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also strongly agree with @jlapeyre , I like Julia very much but I have to force myself to use Python every time because the organization I am associated with deals only in Python, I doubt if anyone will be ready to deal with Julia while working on a project which is all in python. It will be a good idea to hide Julia altogether while appreciating all the benefits that Julia provides :)


julia_project.run()
Copy link
Contributor

@tkf tkf Jan 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly suggest avoiding side-effects on import, at least from the top-level module (i.e., on import diffeqpy). It makes it impossible to provide APIs that can/should be used before initialization. Maybe it's OK to do it in import diffeqpy.de.

Copy link
Author

@jlapeyre jlapeyre Jan 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes it impossible to provide APIs that can/should be used before initialization

I don't see at the moment when this would happen. But, I suspect you are correct. So, you would have to do import diffeqpy, then diffeqpy.setup() or something like that I suppose.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import diffeqpy, then diffeqpy.setup()

Yes, that's the idea. It'd be better if

>>> import diffeqpy
>>> diffeqpy.update(julia=True)  # hypothetical API to update DifferentialEquations and Julia
>>> from diffeqpy import de

works. If we initialize things in import diffeqpy, it'd be impossible to use updated julia and DifferentialEquations (though the latter is somewhat possible if we integrate Revise).

I also consider that it's a language-agnostic best practice to avoid "magic" initialization on module/library import.

Re #100 (comment)

It installs packages and builds a system image in both the source dir and the environment created for tox.

I don't think this is related to the package initialization, though. Do you generate a sysimage for each libpython? If so, it might be due to that tox (or rather virtualenv) creates a Python executable and libpython for each environment (IIRC). I remember venv to be more "light weight" than virtualenv since it doesn't copy executable and libpython (IIRC). So maybe using tox-venv could help.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also consider that it's a language-agnostic best practice to avoid "magic" initialization

Yes, the idea of import package reading a bunch of state from the filesystem plus implicit inputs (environment variables) and then changing a bunch of state seems like its abusing the import system. I can't see an actual serious problem, but it doesn't feel right. I really like the idea of the user not being required to remember to do anything to use the package. But, requiring setup or install the first time is perhaps not so bad.

it'd be impossible to use updated julia and DifferentialEquations

You could require that the user restart after upgrading. I think people are pretty used to software that tells them they have to do that.

Do you generate a sysimage for each libpython?

If there is a conflicting libpython then julia_project gives the user the option of either rebuilding PyCall (and breaking whatever built it previously) or using a depot particular to the project and building a new PyCall (and all other packages) there. (I don't know of a more fine-grained option at the moment) In the tox test, so far, I only choose the latter. But, I think it may not be that. I did not spend much time debugging tox so far.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, requiring setup or install the first time is perhaps not so bad.

I'm OK with from diffeqpy import de working out-of-the-box by doing some magic by default. I'm just suggesting to provide an option to do some operations before booting up PyJulia and DifferentialEquations. In fact, this is exactly why diffeqpy does the initialization in diffeqpy/de.py and not in diffeqpy/__init__.py.

You could require that the user restart after upgrading. I think people are pretty used to software that tells them they have to do that.

I don't think we need to copy badly designed software when there is a simple and better solution that is already implemented.


# logger = julia_project.logger

def compile_diffeqpy():
"""
Compile a system image for `diffeqpy` in the subdirectory `./sys_image/`. This
system image will be loaded the next time you import `diffeqpy`.
"""
julia_project.compile_julia_project()


def update_diffeqpy():
"""
Remove possible stale Manifest.toml files and compiled system image.
Update Julia packages and rebuild Manifest.toml file.
Before compiling, it's probably a good idea to call this method, then restart Python.
"""
julia_project.update()


2 changes: 2 additions & 0 deletions diffeqpy/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Define version number here and read it from setup.py automatically"""
__version__ = "1.2.0"
6 changes: 0 additions & 6 deletions diffeqpy/de.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import os
import sys

from . import _ensure_installed

# This is terrifying to many people. However, it seems SciML takes pragmatic approach.
_ensure_installed()

# PyJulia have to be loaded after `_ensure_installed()`
from julia import Main

script_dir = os.path.dirname(os.path.realpath(__file__))
Expand Down
8 changes: 0 additions & 8 deletions diffeqpy/install.jl

This file was deleted.

6 changes: 6 additions & 0 deletions diffeqpy/sys_image/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[deps]
DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
51 changes: 51 additions & 0 deletions diffeqpy/sys_image/compile_exercise_script.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using DifferentialEquations
const DE = DifferentialEquations

# This is curiously slow
function sde_exercise()
f = (u,p,t) -> 1.01*u
g = (u,p,t) -> 0.87*u
u0 = 0.5
tspan = (0.0,1.0)
prob = DE.SDEProblem(f,g,u0,tspan)
sol = DE.solve(prob,reltol=1e-3,abstol=1e-3)
return nothing
end

function ode_exercise()
f = (u,p,t) -> -u
u0 = 0.5
tspan = (0., 1.)
prob = DE.ODEProblem(f, u0, tspan)
sol = DE.solve(prob)
return nothing
end

function ode_exercise2()
f = function(u,p,t)
x, y, z = u
sigma, rho, beta = p
return [sigma * (y - x), x * (rho - z) - y, x * y - beta * z]
end
u0 = [1.0,0.0,0.0]
tspan = (0., 100.)
p = [10.0,28.0,8/3]
prob = DE.ODEProblem(f, u0, tspan, p)
sol = DE.solve(prob,saveat=0.01)
return nothing
end

# From ODE docs
function ode_exercise3()
f(u,p,t) = 1.01*u
u0 = 1/2
tspan = (0.0,1.0)
prob = ODEProblem(f,u0,tspan)
sol = solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8)
return nothing
end

ode_exercise()
ode_exercise2()
ode_exercise3()
sde_exercise()
11 changes: 11 additions & 0 deletions diffeqpy/sys_image/compile_julia_project.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using PackageCompiler
using Libdl: Libdl

packages = [:PyCall, :DiffEqBase, :DifferentialEquations, :OrdinaryDiffEq]

sysimage_path = joinpath(@__DIR__, "sys_julia_project." * Libdl.dlext)

#create_sysimage(packages; sysimage_path=sysimage_path)

create_sysimage(packages; sysimage_path=sysimage_path,
precompile_execution_file=joinpath(@__DIR__, "compile_exercise_script.jl"))
11 changes: 9 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ def readme():
with open('README.md') as f:
return f.read()


version = {}
with open("./diffeqpy/_version.py") as fp:
exec(fp.read(), version)


setup(name='diffeqpy',
version='1.2.0',
version=version['__version__'],
description='Solving Differential Equations in Python',
long_description=readme(),
long_description_content_type="text/markdown",
Expand All @@ -24,6 +30,7 @@ def readme():
author_email='[email protected]',
license='MIT',
packages=['diffeqpy','diffeqpy.tests'],
install_requires=['julia>=0.2', 'jill'],
install_requires=['julia>=0.2',
'julia_project>=0.0.24'],
include_package_data=True,
zip_safe=False)
5 changes: 4 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ deps =
pytest
pytest-cov
commands =
python -c 'import diffeqpy; diffeqpy.install()'
python -c 'import diffeqpy'
py.test \
--pyargs diffeqpy \
{posargs}
Expand All @@ -12,6 +12,9 @@ whitelist_externals =
setenv =
# Do not use matplotlib GUI backend during tests.
MPLBACKEND = agg
DIFFEQPY_INSTALL_JULIA = y
DIFFEQPY_COMPILE = y
DIFFEQPY_DEPOT = y
passenv =
# Allow a workaround for "error initializing LibGit2 module":
# https://github.com/JuliaLang/julia/issues/18693
Expand Down