Adding a New Solar Position Algorithm

This tutorial walks you through the process of adding a new solar positioning algorithm to SolarPosition.jl. We'll implement a simplified algorithm step by step, covering all the necessary components: the algorithm struct, core computation, refraction handling, exports, and tests.

Overview

Adding a new algorithm involves these steps:

  1. Create the algorithm struct - Define a type that subtypes SolarAlgorithm.
  2. Implement the core function - Write _solar_position for your algorithm.
  3. Handle refraction - Define how your algorithm interacts with DefaultRefraction.
  4. Export the algorithm - Make it available to users.
  5. Write tests - Validate correctness against reference values.
  6. Document - Add docstrings and update documentation.
  7. Run pre-commit checks - Ensure code quality and formatting.
Underscore

Note the underscore prefix in _solar_position. This function is internal and should not be called directly by users. Instead, they will use the public solar_position function, which dispatches to your implementation based on the algorithm type struct.

Step 1: Create the Algorithm Struct

Create a new file in src/Positioning/ for your algorithm. For this example, we'll create a simplified algorithm called SimpleAlgorithm.

The struct must:

  • Subtype SolarAlgorithm
  • Include a docstring with TYPEDEF and TYPEDFIELDS macros
  • Document accuracy and provide literature references
# src/Positioning/simple.jl

"""
    \$(TYPEDEF)

Simple solar position algorithm for demonstration purposes.

This algorithm uses basic spherical trigonometry to compute solar positions.
It is provided as a teaching example and is NOT suitable for production use.

# Accuracy
This is a simplified algorithm with limited accuracy (±1°).

# Literature
Based on basic solar geometry principles.

# Fields
\$(TYPEDFIELDS)
"""
struct SimpleAlgorithm <: SolarAlgorithm
    "Optional configuration parameter"
    param::Float64
end

# Provide a default constructor
SimpleAlgorithm() = SimpleAlgorithm(1.0)

Step 2: Implement the Core Function

The core of any algorithm is the _solar_position function. This function:

  • Takes an Observer, DateTime, and your algorithm type
  • Returns a SolPos{T} with azimuth, elevation, and zenith angles
  • Should be type-stable and performant

Here's the basic structure:

function _solar_position(obs::Observer{T}, dt::DateTime, alg::SimpleAlgorithm) where {T}
    # 1. Convert datetime to Julian date
    jd = datetime2julian(dt)

    # 2. Calculate days since J2000.0 epoch
    n = jd - 2451545.0

    # 3. Compute solar coordinates (declination, hour angle, etc.)
    # ... your algorithm's calculations here ...

    # 4. Calculate local horizontal coordinates
    # ... azimuth and elevation calculations ...

    # 5. Return the result
    return SolPos{T}(azimuth_deg, elevation_deg, zenith_deg)
end

Key Implementation Notes

  1. Use helper functions from utils.jl:

    • fractional_hour(dt) - Convert time to decimal hours
    • deg2rad(x) / rad2deg(x) - Angle conversions
  2. Observer properties are pre-computed for efficiency:

    • obs.latitude, obs.longitude, obs.altitude - Input values
    • obs.latitude_rad, obs.longitude_rad - Radians versions
    • obs.sin_lat, obs.cos_lat - Precomputed trigonometric values
  3. Type parameter T ensures numerical precision is preserved from the Observer

  4. Angle conventions:

    • Azimuth: 0° = North, positive clockwise, range [0°, 360°]
    • Elevation: angle above horizon, range [-90°, 90°]
    • Zenith: 90° - elevation, range [0°, 180°]

Step 3: Handle Default Refraction

Each algorithm must specify how it handles DefaultRefraction. There are two common patterns:

Pattern A: No Refraction by Default (like PSA)

If your algorithm should NOT apply refraction by default:

function _solar_position(obs, dt, alg::SimpleAlgorithm, ::DefaultRefraction)
    return _solar_position(obs, dt, alg, NoRefraction())
end

# Return type for DefaultRefraction
result_type(::Type{SimpleAlgorithm}, ::Type{DefaultRefraction}, ::Type{T}) where {T} =
    SolPos{T}

Pattern B: Apply Refraction by Default (like NOAA)

If your algorithm should apply a specific refraction model by default:

using ..Refraction: HUGHES, DefaultRefraction

function _solar_position(obs, dt, alg::SimpleAlgorithm, ::DefaultRefraction)
    return _solar_position(obs, dt, alg, HUGHES())
end

# Return type for DefaultRefraction
result_type(::Type{SimpleAlgorithm}, ::Type{DefaultRefraction}, ::Type{T}) where {T} =
    ApparentSolPos{T}

The result_type function tells the system what return type to expect, enabling type-stable code for vectorized operations.

Step 4: Export the Algorithm

After implementing your algorithm, you need to export it so users can access it.

4.1 Include in Positioning Module

Edit src/Positioning/Positioning.jl to include your new file:

# Near the bottom of the file, with other includes
include("utils.jl")
include("deltat.jl")
include("psa.jl")
include("noaa.jl")
include("walraven.jl")
include("usno.jl")
include("spa.jl")
include("simple.jl")  # Add your new file

# Add to the export list
export Observer,
    PSA,
    NOAA,
    Walraven,
    USNO,
    SPA,
    SimpleAlgorithm,  # Add your algorithm
    solar_position,
    solar_position!,
    SolPos,
    ApparentSolPos,
    SPASolPos

4.2 Export from Main Module

Edit src/SolarPosition.jl to re-export your algorithm:

using .Positioning:
    Observer, PSA, NOAA, Walraven, USNO, SPA, SimpleAlgorithm, solar_position, solar_position!

# ... later in exports ...
export PSA, NOAA, Walraven, USNO, SPA, SimpleAlgorithm

Step 5: Write Tests

Create a test file following the naming convention test/test-simple.jl.

Generating Validation Data

It is required to validate your algorithm against known reference values. You can use a reference implementation of your algorithm (if available) or compare against trusted solar position calculators. Store these reference values in your test file and use @test statements to ensure your implementation matches them. See the existing test files like test/test-psa.jl for examples of how to structure these tests.

Running Tests

Tests are automatically discovered by runtests.jl. Run them with:

julia --project=. -e 'using Pkg; Pkg.test()'

Or from the Julia REPL:

using Pkg
Pkg.activate(".")
Pkg.test()

Step 6: Document Your Algorithm

Add to Documentation Pages

Update docs/src/positioning.md to include your algorithm in the algorithm reference section.

Add Literature References

If your algorithm is based on published work, add the reference to docs/src/refs.bib:

@article{YourReference,
    author = {Author Name},
    title = {Algorithm Title},
    journal = {Journal Name},
    year = {2024},
    volume = {1},
    pages = {1-10}
}

Then cite it in your docstring using [YourReference](@cite).

Step 7: Run Pre-commit Checks (Recommended)

Before submitting a pull request, it's recommended to run pre-commit hooks locally to catch formatting and linting issues early. This saves time during code review and ensures your code meets the project's quality standards. The pre-commit configuration is defined in the .pre-commit-config.yaml file at the root of the repository.

CI Runs Pre-commit

Even if you skip this step locally, GitHub CI will automatically run pre-commit checks on your pull request. However, running them locally first helps you catch and fix issues before pushing.

Installing Pre-commit

# Install pre-commit (requires Python)
pip install pre-commit

# Install the git hooks (run once per clone)
pre-commit install

Running Pre-commit

# Run all hooks on all files
pre-commit run --all-files

# Or run on staged files only
pre-commit run

Pre-commit runs several checks including:

  • JuliaFormatter - Ensures consistent code formatting
  • ExplicitImports - Checks for explicit imports
  • markdownlint - Lints markdown files
  • typos - Catches common spelling mistakes

If any checks fail, fix the issues and run pre-commit again until all checks pass.

Checklist

Before submitting your algorithm for review, ensure you've completed the following:

TaskDescription
Algorithm structSubtypes SolarAlgorithm
DocstringIncludes TYPEDEF, TYPEDFIELDS, accuracy, and references
_solar_positionFunction implemented with correct signature
Default refractionHandling defined for DefaultRefraction
result_typeFunction defined for DefaultRefraction
ExportAlgorithm exported from both modules
TestsCover basic functionality, refraction, vectors, and edge cases
Test coverageEnsure tests cover all new code paths
Pre-commitChecks pass (recommended locally, required in CI)
DocumentationUpdate algorithm lists in positioning.md, README.md, and refraction.md
LiteratureReferences added to refs.bib and cited in docstrings

Additional Resources

  • See existing implementations in src/Positioning/ for reference:
    • psa.jl - Simple algorithm with no default refraction (PSA)
    • noaa.jl - Algorithm with default HUGHES refraction (NOAA)
    • spa.jl - Complex algorithm with additional output fields (SPA)
  • Check the Contributing Guidelines for general contribution workflow
  • Review the Solar Positioning Algorithms page for context