Julia Scientific Computing skill for Claude Code
Type-stable Julia, measured before and after. Claude writes Julia kernels in functions with concrete types, then proves they are fast with BenchmarkTools, @code_warntype and JET instead of trusting the language's reputation. Projects get their own Project.toml and Manifest.toml, DataFrames work uses transform and combine rather than row loops, and first-call latency is handled with PrecompileTools or a sysimage when users feel it.
- name
julia-scientific - size236 lines · ≈2,700 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Writes fast, reproducible Julia for scientific computing - type-stable functions checked with @code_warntype and JET, no untyped globals, multiple-dispatch designs without type piracy, Project.toml and Manifest.toml environments with Pkg and juliaup, BenchmarkTools measurements, DataFrames.jl and CSV.jl workflows, Makie or Plots, time-to-first-plot fixes with PrecompileTools and sysimages, and interop with Python via PythonCall and C via ccall. Use when writing or optimizing .jl code, when Julia is slower than expected or allocates heavily, when setting up a Julia project or package, or when deciding between Julia and Python with NumPy. Try asking
- This Julia simulation is slower than my NumPy version. Find out why
- Set up a reproducible Julia project for my thesis analysis with tests
- Plotting takes 40 seconds on first run in Julia. Can we fix the startup time?
Use it when
- Writing or optimizing .jl numerical code
- Julia code allocates heavily or shows Any in @code_warntype
- Setting up Julia environments, packages or CI
- Deciding whether a workload should move from Python to Julia
Skip it when
- The analysis is tabular work in pandas — use pandas-analysis
- Profiling slow Python code — use python-profiling
How to check the result
pkg> test passes after instantiate on a fresh clone, hot functions show no Any in @code_warntype, and @btime before and after shows the speedup with zero allocations in the kernel.
The common mistake it prevents
Benchmarking the first call or a top-level loop over globals, then concluding Julia is slow when the time is compilation and dynamic dispatch.
Done well means: hot functions are type-stable and allocation-free in their inner loops,
proved with @code_warntype and @btime rather than assumed; every project runs from
its own Project.toml with a committed Manifest.toml for applications; code is organized
around functions and dispatch, not scripts mutating globals; and the first-run latency is
known and, where it matters, paid once by precompilation.
Versions
- Install and switch Julia with juliaup:
juliaup add release,juliaup default release,juliaup status. Stable in late 2026 is 1.13.x; the LTS channel (juliaup add lts) has been 1.10. Pin the minimum supported version in[compat]for packages. - Start every session in the project:
julia --project=. -t auto(-t autoenables threads; Julia starts single-threaded by default).
Ground rules
- Put performance-critical code in functions. Top-level script code and non-
constglobals are type-unstable and slow. - Never benchmark the first call; it includes compilation. Use BenchmarkTools with
$interpolation for arguments. - Struct fields must have concrete types (or type parameters).
x::Realor an untyped field makes every access dynamic. - Do not add methods to functions you do not own for types you do not own (type piracy). It changes behavior for every package loaded in the session.
- Arrays are column-major: the first index varies fastest. Loop
for j in axes(A,2), i in axes(A,1). - Prove before using
@inboundsor@fastmath; both trade safety or IEEE semantics for speed. Run tests with--check-bounds=yesregardless.
Workflow
- [ ] 1. Set up the environment (Project.toml, Manifest.toml, Revise)
- [ ] 2. Write it plainly in functions, with tests
- [ ] 3. Measure: @btime / @benchmark on realistic sizes
- [ ] 4. Check type stability: @code_warntype, JET @report_opt
- [ ] 5. Remove allocations in hot loops; profile the rest
- [ ] 6. Parallelize only after the serial version is efficient
- [ ] 7. Reduce latency (precompile workloads, sysimage) if users feel it
1. Environment
julia> ] # Pkg REPL
(@v1.13) pkg> activate . # creates/uses ./Project.toml
(proj) pkg> add DataFrames CSV CairoMakie BenchmarkTools
(proj) pkg> instantiate # on a fresh clone: install exactly what Manifest.toml records
(proj) pkg> status --outdated
| Project type | Commit Manifest.toml? | Compat |
|---|---|---|
| Analysis, paper, application, service | Yes (exact reproducibility) | Optional |
| Registered package / library | No | Required [compat] for every dependency and julia |
- Keep the global environment (
@v1.x) to dev tools only: Revise, BenchmarkTools, JET, maybe OhMyREPL. Everything else goes in the project. using Revisebefore loading your code so edits reload without restarting.- Structure non-trivial work as a package (
pkg> generate MyAnalysis) withsrc/andtest/runtests.jl; scripts then justusing MyAnalysis.
3. Measure
using BenchmarkTools
x = rand(10^6)
@btime mysum($x) # min time and allocations; $ avoids global-variable overhead
@benchmark mysum($x) # distribution (median, GC time)
@allocated mysum(x) # bytes for one call (after warm-up)
Targets: an inner kernel over plain arrays should report 0 allocations. A loop that
allocates per iteration is the most common 10-100x slowdown.
4. Type stability
@code_warntype f(x) # red Any / Union{...} in Body or variables = instability
using JET
@report_opt f(x) # lists runtime dispatch sites through the call tree
Common causes and fixes are in
references/performance-patterns.md; read it when
@code_warntype shows Any or @btime shows unexpected allocations.
Quick rules:
- A variable must keep one type:
s = 0thens += 0.5makessaUnion. Uses = zero(eltype(x)). - Return the same type on every branch (
nothingvs a number is a Union; small Unions are fine,Anyis not). - Containers:
Float64[], not[](which isVector{Any}). - Closures capturing a variable that is reassigned get boxed (
Core.Box); assign once or uselet. - Function barriers: when data arrives with an unknown type (parsed file, Dict{String,Any}), pass it to a separate kernel function; dispatch specializes the kernel once.
5. Allocations and profiling
- Preallocate outputs and write mutating versions:
mul!(C, A, B),f!(out, x). - Fuse broadcasts with dots:
@. y = a * x + bmakes one loop, no temporaries. - Slices copy:
A[:, j]allocates; use@viewsorview(A, :, j). - Small fixed-size vectors (3D points, 4x4 matrices): StaticArrays
SVector/SMatrix. - Profile:
using Profile; @profile run(); Profile.print(format=:flat, sortedby=:count), or@profviewin the VS Code Julia extension for a flame graph.
6. Parallelism
| Work shape | Tool |
|---|---|
| Independent loop iterations, shared memory | Threads.@threads or OhMyThreads.tmap with -t auto |
| Task-based or uneven work | Threads.@spawn + fetch |
| Multiple machines or processes | Distributed (addprocs, pmap) |
| Dense linear algebra | Already multithreaded via BLAS; check BLAS.get_num_threads() and avoid oversubscription with Julia threads |
| GPU | CUDA.jl / AMDGPU.jl / Metal.jl with broadcast or KernelAbstractions |
Never write to shared accumulators from threads without a reduction or per-task
buffers; Threads.threadid()-indexed buffers are unsafe because tasks can migrate.
7. Latency (time to first X)
| Problem | Fix |
|---|---|
| Your package’s first call is slow for users | PrecompileTools.@compile_workload with a representative workload inside the package module |
| Your own scripts load many packages slowly | A local Startup package that usings them and runs a small @compile_workload |
| CLI tool or deployment must start in well under a second | PackageCompiler create_sysimage with a precompile execution file, or experimental juliac --trim (1.12+) for small static-dispatch programs |
| Iterating interactively | Keep one session alive with Revise; do not restart |
using PrecompileTools: @setup_workload, @compile_workload
@setup_workload begin
data = rand(100, 3)
@compile_workload begin
fit_model(data)
end
end
Invalidations (using SnoopCompileCore; invalidations = @snoop_invalidations using X) undo
precompilation; type piracy and overly generic methods are the usual cause.
Dispatch design
- Define behavior on abstract types and functions, data in concrete structs:
abstract type Kernel end; struct Gaussian <: Kernel; σ::Float64; end; evaluate(k::Gaussian, r) = exp(-r^2 / (2k.σ^2)). - Parametric structs for generic numerics:
struct Particle{T<:AbstractFloat}; x::T; v::T; end. - Accept abstract argument types (
AbstractVector{<:Real}) for generality; type annotations on arguments do not make code faster, concrete field and container types do. - Extend Base generics (
Base.length,Base.getindex,Base.show) for your own types to plug into the ecosystem.
DataFrames and plotting
using CSV, DataFrames, Statistics
df = CSV.read("runs.csv", DataFrame; types=Dict(:id => String), missingstring="NA")
clean = dropmissing(df, :energy)
summary = combine(groupby(clean, :material), :energy => mean => :mean_E, nrow => :n)
transform!(clean, [:mass, :v] => ByRow((m, v) -> 0.5m * v^2) => :kinetic)
- Use
select/transform/combinewithsource => function => target; row loops over a DataFrame are type-unstable because columns have heterogeneous types. - For a hot loop over columns, extract them first (
x = df.x) and pass to a kernel.
| Plotting need | Choose |
|---|---|
| Publication figures, PDF/SVG/PNG | CairoMakie |
| Interactive 3D or large data | GLMakie (needs OpenGL) |
| Quick plots in notebooks with many backends | Plots.jl |
Save explicitly: save("fig.pdf", fig) (Makie) or savefig("fig.pdf") (Plots).
Interop
using PythonCall # Python from Julia; deps recorded in CondaPkg.toml
np = pyimport("numpy")
v = pyconvert(Vector{Float64}, np.linspace(0, 1, 5))
a = PyArray(np.zeros(10)) # no-copy view of a NumPy buffer
- Add Python packages with
pkg> conda add scipy(afterusing CondaPkg) and commitCondaPkg.toml. To use an existing interpreter instead, setJULIA_CONDAPKG_BACKEND=NullandJULIA_PYTHONCALL_EXE=/path/to/python. - From Python,
juliacallexposes Julia (from juliacall import Main as jl). - C and Fortran:
@ccall libm.cos(1.0::Cdouble)::Cdouble; pass arrays asPtr{Cdouble}and keep them rooted withGC.@preservewhile C holds the pointer.
Julia vs Python + NumPy
| Situation | Better choice |
|---|---|
| Custom loops that do not vectorize (ODE right-hand sides, Monte Carlo with branching, agent models, custom kernels) | Julia: loops compile to native code |
| Differential equations, optimization, AD-heavy models | Julia (DifferentialEquations.jl, JuMP, Enzyme/ForwardDiff) is a strong default |
| Workload is already vectorized NumPy/BLAS calls | Roughly equal; switching gains little |
| Deep learning training, cloud ML services, broad library needs | Python (PyTorch/JAX ecosystem) |
| Short scripts run once, cold start matters, team only knows Python | Python; Julia’s compile latency dominates |
| Mixed team | Python front end calling Julia kernels via juliacall, or the reverse |
Do not claim speedups without a benchmark of both versions on the user’s data size.
Gotchas
- Integer overflow wraps silently:
2^64 == 0. Usebig(2)^64orInt128when ranges can exceedInt64. 1:nwithn = 0is empty, and arrays are 1-based;eachindex(A)andaxes(A, d)are correct for offset arrays too.a = bfor arrays aliases; usecopy(b).==vsisequal/===:NaN == NaNis false;missing == 1ismissing.- Precompilation can fail silently in a broken environment; run
pkg> precompileand read the errors. Threads.@threadswith a sharedpush!is a data race; it may “work” on small inputs.
Verification
pkg> testpasses from a fresh clone afterinstantiate.@code_warntypeon each hot function shows noAny;@report_optreports nothing in your code.@btimebefore and after, at the target data size, with allocations; report both.- Numerical results match the previous implementation within a stated tolerance
(
isapprox(a, b; rtol=1e-10)), including on edge-case inputs.
If speed did not improve, profile before guessing; the bottleneck is often I/O, a DataFrame row loop, or a type-unstable function barrier one level up.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/performance-patterns.md
# Julia performance patterns: diagnosis and fixes
Read this when `@code_warntype` shows `Any`/`Union` types, `@btime` reports allocations
in a kernel that should allocate nothing, or a function is far slower than the
equivalent C. Each entry: symptom, cause, fix.
## 1. Non-const global
```julia
# slow: `scale` is an untyped global; every use is a dynamic lookup
scale = 2.0
f(x) = scale * x
# fix A: make it const (value may change, type may not)
const SCALE = 2.0
# fix B: typed global (1.8+), still slower than passing it
scale::Float64 = 2.0
# fix C (best): pass it in
f(x, scale) = scale * x
```
Symptom: `@code_warntype f(1.0)` shows `Main.scale::Any`.
## 2. Accumulator changes type
```julia
function total(v)
s = 0 # Int
for x in v
s += x # becomes Float64 when v is Vector{Float64}: s::Union{Int,Float64}
end
s
end
# fix
function total(v)
s = zero(eltype(v))
for x in v
s += x
end
s
end
```
## 3. Abstract or untyped struct fields
```julia
struct Bad; data; scale::Real; end # every access dynamic
struct Good{T<:AbstractFloat, V<:AbstractVector{T}}
data::V
scale::T
end
```
Check with `isconcretetype(fieldtype(Good{Float64,Vector{Float64}}, :data))`.
Containers: `Vector{Real}` and `Dict{String,Any}` are abstract-element containers; each
element is boxed.
## 4. Type-unstable return
```julia
findfirst_pos(v) = (i = findfirst(>(0), v); i === nothing ? -1 : i) # stable: always Int
safe_div(a, b) = b == 0 ? nothing : a / b # Union{Nothing,Float64}: OK, small union
parse_any(s) = occursin('.', s) ? parse(Float64, s) : parse(Int, s) # Union, spreads to callers
```
Small unions with `nothing`/`missing` are optimized by the compiler; unions that flow
into heavy arithmetic are not. Pick one output type.
## 5. Captured variable boxed in a closure
```julia
function g(xs)
a = 1
a = 2 # reassignment makes `a` a Core.Box in the closure below
map(x -> x + a, xs)
end
# fix
function g(xs)
a = 2
let a = a
map(x -> x + a, xs)
end
end
```
Symptom: `@code_warntype` shows `Core.Box`.
## 6. Function barrier for dynamic data
```julia
function process(path)
cfg = JSON3.read(read(path, String)) # fields have unknown types at compile time
n = Int(cfg.n); dt = Float64(cfg.dt) # convert at the boundary
simulate(n, dt) # kernel gets concrete types, compiles once
end
```
## 7. Allocation per iteration
| Pattern | Fix |
|---|---|
| `y = A * x` inside a loop | `mul!(y, A, x)` into a preallocated `y` |
| `v[i:j]` slices | `@views` on the block or `view(v, i:j)` |
| `y = a .* x .+ b` creating a new array each time | `y .= a .* x .+ b` or `@. y = a * x + b` |
| `push!` in a hot loop with known length | `Vector{T}(undef, n)` then index |
| Small vectors `[x, y, z]` | `SVector(x, y, z)` from StaticArrays |
| String building with `*` | `IOBuffer` + `print`, or `join` |
Measure each fix: `@btime` should show the allocation count drop.
## 8. Memory order
```julia
# slow: inner loop walks rows (stride n)
for i in 1:n, j in 1:n; A[i, j] = f(i, j); end # j varies fastest here
# fast: inner loop walks columns (contiguous)
for j in axes(A, 2), i in axes(A, 1); A[i, j] = f(i, j); end
```
Julia's `for a in X, b in Y` nests `b` innermost.
## 9. Keyword arguments and splatting
Keyword arguments with abstract defaults and `f(args...)` with heterogeneous tuples can
defeat specialization in hot paths. Keep kernels positional with concrete types; use
keywords in the outer API.
## 10. Functions as arguments
Julia specializes on function arguments when they are called, but not when only passed
through. If a higher-order function only forwards `f`, add a type parameter:
`apply(f::F, x) where {F} = inner(f, x)`.
## Profiling flow
```julia
using Profile
f(x) # warm up (compile)
Profile.clear()
@profile for _ in 1:100; f(x); end
Profile.print(format=:flat, sortedby=:count, mincount=50)
```
- In VS Code (or with ProfileView.jl), `@profview f(x)` shows a flame graph that
color-highlights frames with runtime dispatch and garbage collection; start there.
- `@time` reports compilation percentage; if most time is compilation, the problem is
latency (precompile), not throughput.
- `Cthulhu.@descend f(x)` walks the inferred call tree interactively to find where a
type first becomes `Any`.
## Checklist before declaring a kernel optimized
- [ ] `@code_warntype` clean (no `Any`, no `Core.Box`)
- [ ] `@btime` shows 0 allocations for the inner kernel
- [ ] Loops follow column-major order
- [ ] Results unchanged (`isapprox` with a stated tolerance) on edge cases
- [ ] Benchmarks recorded before and after, same machine, same thread count
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.