Python Tutorial

FormationTemps.jl can be called from Python using juliacall. This requires a working Julia installation (v1.12+) and Python 3.12+ with uv (recommended) or pip. We strongly advise against Conda.

Prerequisites

Installation

From a Local Clone

If you have cloned the FormationTemps.jl repository:

cd FormationTemps.jl
uv sync
uv run python deps/setup.py

This installs FormationTemps.jl as a development package, meaning changes you make to the Julia source code are immediately reflected without reinstalling.

From the Julia Package Registry

If you want to use the released version of FormationTemps.jl without cloning the repository:

pip install juliacall juliapkg numpy matplotlib

Then set the following environment variables in your shell profile (e.g. ~/.bashrc, ~/.zshrc) so they persist across sessions:

export PYTHON_JULIAPKG_EXE=$(which julia)
export JULIA_NUM_THREADS=1

Finally, run the following once in Python to register the Julia dependency:

import juliapkg
juliapkg.require_julia("~1.12")
juliapkg.add("FormationTemps", "03bcd87b-2230-4045-a5fa-95a5fcdd1ff8", version="^1")
juliapkg.resolve()

This pulls the latest compatible version of FormationTemps.jl from the Julia General registry.

Warning

PYTHON_JULIAPKG_EXE is required on macOS with juliaup (see Troubleshooting below). JULIA_NUM_THREADS=1 prevents GC crashes in the PythonCall bridge — CPU multithreading is not available when calling from Python. The local clone setup script (deps/setup.py) handles both automatically.

Basic Usage

Once installed, FormationTemps.jl can be loaded in Python via:

from juliacall import Main as jl
jl.seval("using Korg")
jl.seval("using FormationTemps")
FT = jl.FormationTemps
Korg = jl.Korg

A complete example computing a formation temperature spectrum (following the Basic Tutorial) is shown below:

import os
import matplotlib
# matplotlib.use("Agg")

from juliacall import Main as jl
import numpy as np
import matplotlib.pyplot as plt

# load FormationTemps
jl.seval("using Korg")
jl.seval("using FormationTemps")
FT = jl.FormationTemps
Korg = jl.Korg

# apply project style
plt.style.use(os.path.join(str(FT.moddir), "fig.mplstyle"))

# read the linelist (ships with the package)
# slicing done on Julia side to preserve 1-based indexing
linelist = jl.seval(
    'Korg.read_linelist(joinpath(FormationTemps.datdir, "Sun_VALD.lin"))[16000:16100]'
)

# set stellar parameters (velocities in m/s)
star = FT.StellarProps(Teff=5777.0, logg=4.44, Fe_H=0.0,
                       vsini=2100.0, v_macro=3400.0, v_micro=850.0)

# compute formation temperatures (use_gpu=False for portability)
result = FT.calc_formation_temp(star, linelist, use_gpu=False, convolve=True, u1=0.43, u2=0.31)

# extract results into numpy arrays
wavs = np.asarray(result.wavs)
flux = np.asarray(result.flux)
temps = np.asarray(result.form_temps)

# plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9.6, 6.4), sharex=True)
ax1.plot(wavs, flux, c="k")
ax1.set_ylabel(r"{\rm Normalized Flux}")
ax2.plot(wavs, temps, c="k")
ax2.set_xlabel(r"{\rm Vacuum Wavelength [\AA]}")
ax2.set_ylabel(r"{\rm Formation Temperature [K]}")
fig.tight_layout()

# write out the file
import os
outpath = os.path.join(os.getcwd(), "formation_temps.png")
fig.savefig(outpath, dpi=150, bbox_inches="tight")
print(f"Saved {outpath}")
plt.close()

Troubleshooting

\"not a valid Julia executable\" on macOS

juliaup installs Julia 1.12+ as .app bundles that juliapkg cannot find. The setup script handles this automatically for local clones. For registry installs, set PYTHON_JULIAPKG_EXE before importing juliacall:

export PYTHON_JULIAPKG_EXE=$(which julia)
\"julia compat entries have empty intersection\" (OpenSSL conflict)

juliacall requires that Julia and Python share a compatible OpenSSL version. Julia 1.12 needs OpenSSL 3.5+, but some Python builds (notably uv's standalone builds) ship OpenSSL 3.0, causing the resolver to reject Julia 1.12 entirely. Check your Python's OpenSSL version:

import ssl; print(ssl.OPENSSL_VERSION)

If it reports < 3.5, recreate your venv pointing at a Python built against a newer OpenSSL:

rm -rf .venv
uv sync --python /path/to/python3

On macOS, Homebrew Python (/opt/homebrew/bin/python3) links against Homebrew's OpenSSL, which is typically up to date. On Linux, system Python from recent distros (Ubuntu 24.04+, Fedora 39+) ships OpenSSL 3.1+; on older distros, install a newer OpenSSL and rebuild Python via pyenv. On Windows, the python.org installer bundles its own OpenSSL (releases 3.13.4+ include OpenSSL 3.5). Note that uv's standalone Python builds (uv python install) currently bundle OpenSSL 3.0 and will not work.

Julia GC crash (SIGBUS/segfault) during long computations

Julia's multi-threaded garbage collector can conflict with PythonCall's runtime bridge, causing hard crashes. The setup script sets JULIA_NUM_THREADS=1 to avoid this. For registry installs, set it in your shell:

export JULIA_NUM_THREADS=1

This disables Julia's CPU multithreading but avoids the GC contention. GPU acceleration is unaffected — pass use_gpu=True to benefit from GPU parallelism even with a single Julia thread. If CPU multithreading is needed, run the computation in pure Julia and load the results in Python. See Parallelization for details.

Tips

Performance

The first import juliacall is slow — Julia compiles code on first use. Subsequent calls in the same session are fast. Precompilation happens once per environment.

GPU support (recommended for Python)

Since CPU multithreading is unavailable from Python (see above), GPU acceleration is the primary way to speed up disk integration from Python. Pass use_gpu=True to calc_formation_temp if you have a CUDA-capable GPU configured with Julia's CUDA.jl.

Type mapping between Python and Julia

Keyword arguments map directly to Julia kwargs (e.g., FT.calc_formation_temp(star, linelist, use_gpu=False, convolve=True)). Julia arrays can be converted to numpy with numpy.asarray(result.wavs), which is zero-copy for contiguous Float64 arrays. Python True/False map to Julia true/false automatically. Note that juliacall uses 0-based indexing from the Python side.

Matplotlib backend

Interactive backends (e.g., macosx, Qt5Agg) can crash under juliacall due to event loop conflicts. Use matplotlib.use("Agg") before importing pyplot and save figures to files instead of calling plt.show().