Parallel Computing with OhMyThreads.jl

SolarPosition.jl provides a parallel computing extension using OhMyThreads.jl for efficient multithreaded solar position calculations across large time series. This extension is particularly useful when processing thousands of timestamps, where parallelization can provide significant speedups.

Installation

The OhMyThreads extension is loaded automatically when both SolarPosition.jl and OhMyThreads.jl are loaded:

using SolarPosition
using OhMyThreads
Thread Configuration

Julia must be started with multiple threads to benefit from parallelization. Use julia --threads=auto or set the JULIA_NUM_THREADS environment variable. Check the number of available threads with Threads.nthreads().

Quick Start

The extension adds new methods to solar_position and solar_position! that accept an OhMyThreads.Scheduler as the last argument. These methods automatically parallelize computations across the provided timestamp vector.

using SolarPosition
using OhMyThreads
using Dates
using StructArrays

# Create observer location
obs = Observer(51.5, -0.18, 15.0)  # London

# Generate a year of minute timestamps
times = collect(DateTime(2024, 1, 1):Minute(1):DateTime(2025, 1, 1))

# Parallel computation with DynamicScheduler
t0 = time()
positions = solar_position(obs, times, PSA(), NoRefraction(), DynamicScheduler())
dt_parallel = time() - t0
println("Time taken (parallel): $(round(dt_parallel, digits=5)) seconds")
Time taken (parallel): 0.74498 seconds

Now we compare this to the serial version:

# Serial computation (no scheduler argument)
t0 = time()
positions_serial = solar_position(obs, times, PSA(), NoRefraction())
dt_serial = time() - t0
println("Time taken (serial): $(round(dt_serial, digits=5)) seconds")
Time taken (serial): 0.11031 seconds

We observe a speedup of:

speedup = dt_serial / dt_parallel
println("Speedup: $(round(speedup, digits=2))×")
Speedup: 0.15×

Simplified Syntax

You can also use the simplified syntax with the scheduler as the third argument, which uses the default algorithm (PSA) and no refraction correction:

# Simplified syntax with default algorithm
positions = solar_position(obs, times, DynamicScheduler())
@show first(positions, 3)
3-element StructArray(::Vector{Float64}, ::Vector{Float64}, ::Vector{Float64}) with eltype SolPos{Float64}:
 SolPos(azimuth=358.1624783703248°, elevation=-61.55014545423738°, zenith=151.5501454542374°)
 SolPos(azimuth=358.64510532358224°, elevation=-61.554425651495485°, zenith=151.55442565149548°)
 SolPos(azimuth=359.12783221156707°, elevation=-61.55739562548632°, zenith=151.55739562548632°)

Implementation Details

The extension uses OhMyThreads' tmap and tmap! for task-based parallelism. Each timestamp is processed independently, making the computation embarrassingly parallel with no inter-thread communication required.

The results from tmap are automatically converted to a StructVector for efficient columnar storage compatible with the rest of SolarPosition.jl's API.

Available Schedulers

OhMyThreads.jl provides different scheduling strategies optimized for various workload characteristics:

DynamicScheduler

The DynamicScheduler is the default and recommended scheduler for most workloads. It dynamically balances tasks among threads, making it suitable for non-uniform workloads where computation times may vary. Please visit the OhMyThreads.jl documentation for more details.

# Dynamic scheduling (recommended)
positions = solar_position(obs, times, PSA(), NoRefraction(), DynamicScheduler());

StaticScheduler

The StaticScheduler partitions work statically among threads. This can be more efficient for uniform workloads where all computations take approximately the same time.

# Static scheduling for uniform workloads
positions = solar_position(obs, times, PSA(), NoRefraction(), StaticScheduler())

In-Place Computation

For maximum performance and minimal allocations, use the in-place version solar_position! with a pre-allocated StructVector:

using StructArrays

# Pre-allocate output array
positions = StructVector{SolPos{Float64}}(undef, length(times))

# Compute in-place
solar_position!(positions, obs, times, PSA(), NoRefraction(), DynamicScheduler())

The in-place version avoids allocating the output array and minimizes intermediate allocations, making it ideal for repeated computations or memory-constrained environments.

Refraction Correction

Atmospheric refraction corrections can be applied in parallel computations:

# Parallel computation with Bennett refraction correction
positions_refracted = solar_position(
    obs,
    times,
    PSA(),
    BENNETT(),
    DynamicScheduler()
)

println("First position with refraction:")
println("  Apparent elevation: $(round(positions_refracted.apparent_elevation[1], digits=2))°")
First position with refraction:
  Apparent elevation: -61.56°

See Also