Skip to content

hfpytrace

Package

Package bootstrap and PHaRLAP library initialization helpers.

Key Methods

Function ensure_pharlap_lib()

API

hfpytrace

hfpytrace — HF ray-tracing toolkit.

This package provides Python wrappers and utilities for high-frequency (HF) ionospheric ray-tracing using the PHaRLAP MATLAB library, together with ionospheric electron density models (IRI, SAMI3, WACCM-X, GEMINI, GITM, WAM-IPE), collision frequency computation (NRLMSISE-00), geomagnetic grids (IGRF via PyIRI), and ray-homing solvers.

Bootstrap behaviour

On import the package checks for, and optionally downloads, the PHaRLAP MATLAB helper library into ~/.hfpytrace/pharlap_lib/. Automatic download is disabled by default; set HFPYTRACE_AUTO_BOOTSTRAP=1 to enable it, or call :func:ensure_pharlap_lib explicitly.

Environment variables

HFPYTRACE_CACHE_DIR Override the default cache root (default ~/.hfpytrace). HFPYTRACE_AUTO_BOOTSTRAP Set to 1, true, or yes to download PHaRLAP automatically. HFPYTRACE_SKIP_PHARLAP_DOWNLOAD Set to 1, true, or yes to suppress any download attempt. HFPYTRACE_PHARLAP_ARCHIVE_URLS Comma-separated list of archive URLs to try instead of the defaults.

Public API

ensure_pharlap_lib() Download PHaRLAP library if absent (skipped when HFPYTRACE_SKIP_PHARLAP_DOWNLOAD set). bootstrap_pharlap_lib() Conditional wrapper called at import time. HomingConfig, HomingResult, Homing2D, Homing3D Lazily imported from :mod:hfpytrace.homing.

__getattr__(name)

Lazy import of homing classes — keeps bootstrap side-effect free.

Source code in hfpytrace/__init__.py
def __getattr__(name: str):
    """Lazy import of homing classes — keeps bootstrap side-effect free."""
    if name in _HOMING_NAMES:
        from hfpytrace.homing import Homing2D  # noqa: PLC0415
        from hfpytrace.homing import Homing3D, HomingConfig, HomingResult

        return {
            "HomingConfig": HomingConfig,
            "HomingResult": HomingResult,
            "Homing2D": Homing2D,
            "Homing3D": Homing3D,
        }[name]
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

ensure_pharlap_lib()

Ensure the PHaRLAP library is present in the local cache.

Checks PHARLAP_LIB_PATH for any pharlap_* subdirectory. If none is found and HFPYTRACE_SKIP_PHARLAP_DOWNLOAD is not set, the archive is downloaded from GitHub and extracted.

Returns

Path Path to the pharlap_lib cache directory.

Raises

RuntimeError If all download candidates fail.

Source code in hfpytrace/__init__.py
def ensure_pharlap_lib() -> Path:
    """Ensure the PHaRLAP library is present in the local cache.

    Checks ``PHARLAP_LIB_PATH`` for any ``pharlap_*`` subdirectory.  If none
    is found and ``HFPYTRACE_SKIP_PHARLAP_DOWNLOAD`` is not set, the archive
    is downloaded from GitHub and extracted.

    Returns
    -------
    Path
        Path to the pharlap_lib cache directory.

    Raises
    ------
    RuntimeError
        If all download candidates fail.
    """
    if os.environ.get("HFPYTRACE_SKIP_PHARLAP_DOWNLOAD", "").lower() in {
        "1",
        "true",
        "yes",
    }:
        return PHARLAP_LIB_PATH
    if _has_pharlap_lib(PHARLAP_LIB_PATH):
        return PHARLAP_LIB_PATH

    _download_pharlap_lib(PHARLAP_LIB_PATH)
    return PHARLAP_LIB_PATH

bootstrap_pharlap_lib()

Optional import-time bootstrap hook.

Enabled only when HFPYTRACE_AUTO_BOOTSTRAP=1|true|yes is set.

Source code in hfpytrace/__init__.py
def bootstrap_pharlap_lib() -> Path:
    """
    Optional import-time bootstrap hook.

    Enabled only when `HFPYTRACE_AUTO_BOOTSTRAP=1|true|yes` is set.
    """
    auto = os.environ.get("HFPYTRACE_AUTO_BOOTSTRAP", "").lower() in {
        "1",
        "true",
        "yes",
    }
    if not auto:
        return PHARLAP_LIB_PATH
    try:
        return ensure_pharlap_lib()
    except Exception as exc:  # pragma: no cover - network/platform specific
        logger.warning(f"pharlap_lib bootstrap skipped: {exc}")
        return PHARLAP_LIB_PATH

Source Code

hfpytrace/__init__.py
"""hfpytrace — HF ray-tracing toolkit.

This package provides Python wrappers and utilities for high-frequency (HF)
ionospheric ray-tracing using the PHaRLAP MATLAB library, together with
ionospheric electron density models (IRI, SAMI3, WACCM-X, GEMINI, GITM,
WAM-IPE), collision frequency computation (NRLMSISE-00), geomagnetic grids
(IGRF via PyIRI), and ray-homing solvers.

Bootstrap behaviour
-------------------
On import the package checks for, and optionally downloads, the PHaRLAP
MATLAB helper library into ``~/.hfpytrace/pharlap_lib/``.  Automatic
download is disabled by default; set ``HFPYTRACE_AUTO_BOOTSTRAP=1`` to
enable it, or call :func:`ensure_pharlap_lib` explicitly.

Environment variables
---------------------
HFPYTRACE_CACHE_DIR
    Override the default cache root (default ``~/.hfpytrace``).
HFPYTRACE_AUTO_BOOTSTRAP
    Set to ``1``, ``true``, or ``yes`` to download PHaRLAP automatically.
HFPYTRACE_SKIP_PHARLAP_DOWNLOAD
    Set to ``1``, ``true``, or ``yes`` to suppress any download attempt.
HFPYTRACE_PHARLAP_ARCHIVE_URLS
    Comma-separated list of archive URLs to try instead of the defaults.

Public API
----------
ensure_pharlap_lib()
    Download PHaRLAP library if absent (skipped when HFPYTRACE_SKIP_PHARLAP_DOWNLOAD set).
bootstrap_pharlap_lib()
    Conditional wrapper called at import time.
HomingConfig, HomingResult, Homing2D, Homing3D
    Lazily imported from :mod:`hfpytrace.homing`.
"""

from __future__ import annotations

import os
import shutil
import tempfile
import urllib.request
import zipfile
from pathlib import Path

from loguru import logger

_DEFAULT_CACHE_ROOT = Path.home() / ".hfpytrace"
_DEFAULT_GITHUB_ARCHIVES = (
    "https://codeload.github.com/shibaji7/trace/zip/refs/heads/main",
    "https://codeload.github.com/shibaji7/trace/zip/refs/heads/master",
)

CACHE_ROOT = Path(os.environ.get("HFPYTRACE_CACHE_DIR", _DEFAULT_CACHE_ROOT))
PHARLAP_LIB_PATH = CACHE_ROOT / "pharlap_lib"
__version__ = "0.0.2"
__all__ = [
    "CACHE_ROOT",
    "PHARLAP_LIB_PATH",
    "ensure_pharlap_lib",
    "bootstrap_pharlap_lib",
    # homing
    "HomingConfig",
    "HomingResult",
    "Homing2D",
    "Homing3D",
]

_HOMING_NAMES = {"HomingConfig", "HomingResult", "Homing2D", "Homing3D"}


def __getattr__(name: str):
    """Lazy import of homing classes — keeps bootstrap side-effect free."""
    if name in _HOMING_NAMES:
        from hfpytrace.homing import Homing2D  # noqa: PLC0415
        from hfpytrace.homing import Homing3D, HomingConfig, HomingResult

        return {
            "HomingConfig": HomingConfig,
            "HomingResult": HomingResult,
            "Homing2D": Homing2D,
            "Homing3D": Homing3D,
        }[name]
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def _has_pharlap_lib(path: Path) -> bool:
    return any(path.glob("pharlap_*"))


def _extract_pharlap_lib_from_archive(archive_path: Path, destination: Path) -> None:
    with zipfile.ZipFile(archive_path) as zf:
        pharlap_prefix = None
        for member in zf.namelist():
            if member.endswith("pharlap_lib/rt_2D.m"):
                pharlap_prefix = member.removesuffix("rt_2D.m")
                break
        if pharlap_prefix is None:
            raise RuntimeError("Archive does not contain pharlap_lib/rt_2D.m")

        staged = destination.parent / f"{destination.name}.tmp"
        if staged.exists():
            shutil.rmtree(staged)
        staged.mkdir(parents=True, exist_ok=True)

        for member in zf.namelist():
            if not member.startswith(pharlap_prefix) or member.endswith("/"):
                continue
            rel_path = member[len(pharlap_prefix) :]
            if rel_path in {"rt_2D.m", "startup_2D.m"}:
                continue
            out_file = staged / rel_path
            out_file.parent.mkdir(parents=True, exist_ok=True)
            out_file.write_bytes(zf.read(member))

        if destination.exists():
            shutil.rmtree(destination)
        staged.rename(destination)


def _download_pharlap_lib(destination: Path) -> None:
    urls = os.environ.get("HFPYTRACE_PHARLAP_ARCHIVE_URLS")
    candidates = (
        tuple(u.strip() for u in urls.split(",")) if urls else _DEFAULT_GITHUB_ARCHIVES
    )
    last_error = None

    for url in candidates:
        try:
            with tempfile.TemporaryDirectory(prefix="hfpytrace_") as td:
                archive = Path(td) / "trace.zip"
                with urllib.request.urlopen(url, timeout=60) as resp:
                    archive.write_bytes(resp.read())
                destination.parent.mkdir(parents=True, exist_ok=True)
                _extract_pharlap_lib_from_archive(archive, destination)
            logger.info(f"Downloaded pharlap_lib from {url} to {destination}")
            return
        except Exception as exc:  # pragma: no cover - network/platform specific
            last_error = exc

    raise RuntimeError(f"Unable to download pharlap_lib from GitHub: {last_error}")


def ensure_pharlap_lib() -> Path:
    """Ensure the PHaRLAP library is present in the local cache.

    Checks ``PHARLAP_LIB_PATH`` for any ``pharlap_*`` subdirectory.  If none
    is found and ``HFPYTRACE_SKIP_PHARLAP_DOWNLOAD`` is not set, the archive
    is downloaded from GitHub and extracted.

    Returns
    -------
    Path
        Path to the pharlap_lib cache directory.

    Raises
    ------
    RuntimeError
        If all download candidates fail.
    """
    if os.environ.get("HFPYTRACE_SKIP_PHARLAP_DOWNLOAD", "").lower() in {
        "1",
        "true",
        "yes",
    }:
        return PHARLAP_LIB_PATH
    if _has_pharlap_lib(PHARLAP_LIB_PATH):
        return PHARLAP_LIB_PATH

    _download_pharlap_lib(PHARLAP_LIB_PATH)
    return PHARLAP_LIB_PATH


def bootstrap_pharlap_lib() -> Path:
    """
    Optional import-time bootstrap hook.

    Enabled only when `HFPYTRACE_AUTO_BOOTSTRAP=1|true|yes` is set.
    """
    auto = os.environ.get("HFPYTRACE_AUTO_BOOTSTRAP", "").lower() in {
        "1",
        "true",
        "yes",
    }
    if not auto:
        return PHARLAP_LIB_PATH
    try:
        return ensure_pharlap_lib()
    except Exception as exc:  # pragma: no cover - network/platform specific
        logger.warning(f"pharlap_lib bootstrap skipped: {exc}")
        return PHARLAP_LIB_PATH


# Default: do not perform network/bootstrap work during package import.
bootstrap_pharlap_lib()