From 0805ca2433724e63856470caa2cf093d59674b9b Mon Sep 17 00:00:00 2001 From: mijahauan Date: Sun, 15 Mar 2026 20:26:30 -0500 Subject: [PATCH 1/9] fix: macOS/PHaRLAP 4.7.4 compatibility patches setup.py: - Cross-platform build: Darwin (maca/maci) + Linux support - Detect gfortran runtime dir via 'gfortran -print-file-name' - Link libgfortran + libgomp on macOS (required by maca/maci .a libs) - Remove Intel Fortran runtime deps on macOS (ifcore/imf/irc/svml/iomp5) - iri2016 -> iri2020 library name (PHaRLAP 4.7.4 consolidation) - Skip modules whose required legacy libs are absent (iri2007/iri2012) - Use setuptools instead of deprecated distutils - Print BUILD/SKIP per module for diagnostics modules/pylap/__init__.py: - Soft imports for all modules; missing ones silently skipped modules/source/igrf2016.c: - igrf2016_calc_ -> igrf2020_calc_ (renamed in PHaRLAP 4.7.4) modules/source/raytrace_2d.c: - verifyIonoGrid: void -> int, ASSERT -> ASSERT_INT, add return 1 - buidlIonoStruct: void -> int, ASSERT -> ASSERT_INT, add return 1 - Call sites wrapped in checked-return pattern - Fixes -Wreturn-mismatch errors under clang modules/source/raytrace_3d.c: - verifyIono: void -> int, ASSERT -> ASSERT_INT, add return 1 - buildIonoStruct: void -> int, ASSERT -> ASSERT_INT, add return 1 - clear_ionosphere: remove spurious return 1 (legitimately void) - Call sites wrapped in checked-return pattern --- modules/pylap/__init__.py | 34 +++--- modules/source/igrf2016.c | 4 +- modules/source/raytrace_2d.c | 69 ++++++------ modules/source/raytrace_3d.c | 80 +++++++------- setup.py | 201 +++++++++++++++++++++++------------ 5 files changed, 233 insertions(+), 155 deletions(-) diff --git a/modules/pylap/__init__.py b/modules/pylap/__init__.py index 47b9900..c96deb9 100644 --- a/modules/pylap/__init__.py +++ b/modules/pylap/__init__.py @@ -1,16 +1,18 @@ -from pylap.abso_bg import abso_bg -from pylap.dop_spread_eq import dop_spread_eq -from pylap.ground_bs_loss import ground_bs_loss -from pylap.ground_fs_loss import ground_fs_loss -from pylap.igrf2007 import igrf2007 -from pylap.igrf2011 import igrf2011 -from pylap.igrf2016 import igrf2016 -from pylap.iri2007 import iri2007 -from pylap.iri2012 import iri2012 -from pylap.iri2016 import iri2016 -from pylap.irreg_strength import irreg_strength -from pylap.nrlmsise00 import nrlmsise00 -from pylap.raytrace_2d import raytrace_2d -from pylap.raytrace_2d_sp import raytrace_2d_sp -from pylap.raytrace_3d import raytrace_3d -from pylap.raytrace_3d_sp import raytrace_3d_sp +# Soft imports — only expose modules that were actually compiled. +# Modules requiring legacy IRI libs (iri2007, iri2012) are absent on PHaRLAP 4.7.4. +_optional = [ + 'abso_bg', 'dop_spread_eq', 'ground_bs_loss', 'ground_fs_loss', + 'igrf2007', 'igrf2011', 'igrf2016', + 'iri2007', 'iri2012', 'iri2016', + 'irreg_strength', 'nrlmsise00', + 'raytrace_2d', 'raytrace_2d_sp', + 'raytrace_3d', 'raytrace_3d_sp', +] +for _m in _optional: + try: + from importlib import import_module as _imp + _mod = _imp('pylap.' + _m) + globals()[_m] = getattr(_mod, _m) + except (ImportError, AttributeError): + pass +del _optional, _m, _imp, _mod diff --git a/modules/source/igrf2016.c b/modules/source/igrf2016.c index 00e4ecc..4117100 100644 --- a/modules/source/igrf2016.c +++ b/modules/source/igrf2016.c @@ -1,6 +1,6 @@ #include "../include/pharlap.h" -extern void igrf2016_calc_(float *glat, float *glon, float *dec_year, +extern void igrf2020_calc_(float *glat, float *glon, float *dec_year, float *height, float *dipole_moment, float *babs, float *bnorth, float *beast, float *bdown, float *dip, float *dec, float *dip_lat, float *l_value, int *l_value_code); @@ -49,7 +49,7 @@ static PyObject *igrf2016(PyObject *self, PyObject *args) int l_value_code; /* Call subroutine. */ - igrf2016_calc_(&latitude, &longitude, &dec_year, &height, &dipole_moment, + igrf2020_calc_(&latitude, &longitude, &dec_year, &height, &dipole_moment, &babs, &bnorth, &beast, &bdown, &dip, &dec, &dip_lat, &l_value, &l_value_code); diff --git a/modules/source/raytrace_2d.c b/modules/source/raytrace_2d.c index d70b7cb..31dca9d 100644 --- a/modules/source/raytrace_2d.c +++ b/modules/source/raytrace_2d.c @@ -12,13 +12,13 @@ extern void raytrace_2d_(double *origin_lat, double *origin_lon, int *num_rays, int *nhops_attempted, double *ray_state_vec_in, int *npts_in_ray, double *ray_state_vec_out, double *elapsed_time); -static void verifyIonoGrid(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, +static int verifyIonoGrid(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, PyArrayObject *irreg_grid); static PyObject *buildOutput(int num_rays, int *nhops_attempted, int *npts_in_ray, double *ray_data, double *ray_path_data, double *freqs_data, double *elevs_data, int *ray_label, double *ray_state_vec_out); -static void buidlIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, +static int buidlIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, PyArrayObject *irreg_grid, double height_start, double height_inc, double range_inc); static struct ionosphere_struct ionosphere; @@ -30,14 +30,14 @@ static PyObject *raytrace_2d(PyObject *self, PyObject *args) Py_ssize_t num_args = PyTuple_Size(args); - ASSERT((num_args == 8 || num_args == 9 || num_args == 15 || num_args == 16), + ASSERT_INT((num_args == 8 || num_args == 9 || num_args == 15 || num_args == 16), PyExc_ValueError, "incorrect number of input arguments"); int init_ionosphere = 1; if (num_args == 8 || num_args == 9) { - ASSERT(iono_exist_in_mem, PyExc_RuntimeError, + ASSERT_INT(iono_exist_in_mem, PyExc_RuntimeError, "the ionosphere has not been initialized"); init_ionosphere = 0; @@ -65,7 +65,7 @@ static PyObject *raytrace_2d(PyObject *self, PyObject *args) /* Ensure that `input_ray_state` is the correct size (if supplied). */ if (num_args == 9 || num_args == 16) { - ASSERT((PyDict_Size(input_ray_state) == 9), PyExc_ValueError, + ASSERT_INT((PyDict_Size(input_ray_state) == 9), PyExc_ValueError, "incorrect number of fields in dictionary."); PyObject *items = PyDict_Values(input_ray_state); @@ -74,41 +74,41 @@ static PyObject *raytrace_2d(PyObject *self, PyObject *args) { PyObject *item = PyList_GetItem(items, i); - ASSERT(PyArray_Check(item), PyExc_ValueError, + ASSERT_INT(PyArray_Check(item), PyExc_ValueError, "field value must be a numpy array"); - ASSERT((PyArray_NDIM((PyArrayObject *)item) == 1), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM((PyArrayObject *)item) == 1), PyExc_ValueError, "invalid shape for field"); npy_intp *item_shape = PyArray_DIMS((PyArrayObject *)item); - ASSERT((item_shape[0] == elevs_shape[0]), PyExc_ValueError, + ASSERT_INT((item_shape[0] == elevs_shape[0]), PyExc_ValueError, "invalid size for field"); } } /* Ensure that `elevs` and `freqs` are the same (and correct) size. */ - ASSERT((PyArray_NDIM(elevs) == 1), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(elevs) == 1), PyExc_ValueError, "invalid shape for elevs"); - ASSERT((PyArray_NDIM(freqs) == 1), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(freqs) == 1), PyExc_ValueError, "invalid shape for freqs"); npy_intp *freqs_shape = PyArray_DIMS(elevs); - ASSERT((elevs_shape[0] == freqs_shape[0]), PyExc_ValueError, + ASSERT_INT((elevs_shape[0] == freqs_shape[0]), PyExc_ValueError, "shape of elevs and freqs must be identical"); /* Ensure the ionosphere grids are valid. */ if (init_ionosphere) { - verifyIonoGrid(iono_en_grid, iono_en_grid_5, collision_grid, irreg_grid); + if (!verifyIonoGrid(iono_en_grid, iono_en_grid_5, collision_grid, irreg_grid)) return NULL; } /* Ensure that `nhops` is valid (0 < nhops <= 50). */ - ASSERT((nhops > 0 && nhops <= 50), PyExc_ValueError, + ASSERT_INT((nhops > 0 && nhops <= 50), PyExc_ValueError, "number of hops is invalid; must be within the range of 1 through 50"); /* Ensure that `tol` is valid (can be an integer or list of 3 elements. */ - ASSERT(( + ASSERT_INT(( (PyList_CheckExact(in_tol) && PyList_Size(in_tol) == 3) || PyLong_CheckExact(in_tol) || PyFloat_CheckExact(in_tol)), @@ -140,8 +140,8 @@ static PyObject *raytrace_2d(PyObject *self, PyObject *args) /* Load ionosphere */ if (init_ionosphere) { - buidlIonoStruct(iono_en_grid, iono_en_grid_5, collision_grid, - irreg_grid, height_start, height_inc, range_inc); + if (!buidlIonoStruct(iono_en_grid, iono_en_grid_5, collision_grid, + irreg_grid, height_start, height_inc, range_inc)) return NULL; iono_exist_in_mem = 1; } @@ -165,14 +165,14 @@ static PyObject *raytrace_2d(PyObject *self, PyObject *args) "the field \"%s\" is missing from input_ray_state.", ray_state_fields[field]); - ASSERT((val != NULL), PyExc_ValueError, message); + ASSERT_INT((val != NULL), PyExc_ValueError, message); sprintf( message, "the field \"%s\" must be a NumPy array.", ray_state_fields[field]); - ASSERT(PyArray_Check(val), PyExc_ValueError, message); + ASSERT_INT(PyArray_Check(val), PyExc_ValueError, message); sprintf( message, @@ -182,8 +182,8 @@ static PyObject *raytrace_2d(PyObject *self, PyObject *args) PyArrayObject *arr = (PyArrayObject *)val; arr = (PyArrayObject *)PyArray_Cast(arr, NPY_DOUBLE); - ASSERT((PyArray_NDIM(arr) == 1), PyExc_ValueError, message); - ASSERT((PyArray_DIMS(arr)[0] >= num_rays), PyExc_ValueError, message); + ASSERT_INT((PyArray_NDIM(arr) == 1), PyExc_ValueError, message); + ASSERT_INT((PyArray_DIMS(arr)[0] >= num_rays), PyExc_ValueError, message); for (npy_intp i = 0; i < num_rays; i++) { @@ -240,17 +240,19 @@ static PyObject *raytrace_2d(PyObject *self, PyObject *args) return result; } -static void verifyIonoGrid(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, + + +static int verifyIonoGrid(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, PyArrayObject *irreg_grid) { - ASSERT((PyArray_NDIM(iono_en_grid) == 2), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(iono_en_grid) == 2), PyExc_ValueError, "invalid shape for iono_en_grid"); - ASSERT((PyArray_NDIM(iono_en_grid_5) == 2), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(iono_en_grid_5) == 2), PyExc_ValueError, "invalid shape for iono_en_grid_5"); - ASSERT((PyArray_NDIM(collision_grid) == 2), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(collision_grid) == 2), PyExc_ValueError, "invalid shape for collision_grid"); - ASSERT((PyArray_NDIM(irreg_grid) == 2), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(irreg_grid) == 2), PyExc_ValueError, "invalid shape for irreg_grid"); npy_intp *iono_en_grid_shape = PyArray_DIMS(iono_en_grid); @@ -258,32 +260,33 @@ static void verifyIonoGrid(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_g npy_intp *collision_grid_shape = PyArray_DIMS(collision_grid); npy_intp *irreg_grid_shape = PyArray_DIMS(irreg_grid); - ASSERT(( + ASSERT_INT(( iono_en_grid_shape[0] <= max_num_ht && iono_en_grid_shape[1] <= max_num_rng), PyExc_ValueError, "iono_en_grid is too large"); - ASSERT(( + ASSERT_INT(( iono_en_grid_5_shape[0] <= max_num_ht && iono_en_grid_5_shape[1] <= max_num_rng), PyExc_ValueError, "iono_en_grid_5 is too large"); - ASSERT(( + ASSERT_INT(( collision_grid_shape[0] <= max_num_ht && collision_grid_shape[1] <= max_num_rng), PyExc_ValueError, "collision_grid is too large"); - ASSERT(( + ASSERT_INT(( irreg_grid_shape[0] == 4 && irreg_grid_shape[1] <= max_num_rng), PyExc_ValueError, "irreg_grid is not the correct size"); - ASSERT(( + ASSERT_INT(( iono_en_grid_shape[0] == iono_en_grid_5_shape[0] && iono_en_grid_shape[0] == collision_grid_shape[0]), PyExc_ValueError, "ionosphere grids have inconsistent row counts"); - ASSERT(( + ASSERT_INT(( iono_en_grid_shape[1] == iono_en_grid_5_shape[1] && iono_en_grid_shape[1] == collision_grid_shape[1] && iono_en_grid_shape[1] == irreg_grid_shape[1]), PyExc_ValueError, "ionosphere grids have inconsistent column counts"); + return 1; } static PyObject *buildOutput(int num_rays, int *nhops_attempted, int *npts_in_ray, double *ray_data, @@ -390,7 +393,7 @@ static PyObject *buildOutput(int num_rays, int *nhops_attempted, int *npts_in_ra return PyTuple_Pack(3, py_rays, py_ray_paths, py_ray_states); } -static void buidlIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, +static int buidlIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, PyArrayObject *irreg_grid, double height_start, double height_inc, double range_inc) { @@ -439,8 +442,10 @@ static void buidlIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_ ionosphere.HtMin = height_start; // range_inc ionosphere.HtInc = height_inc; ionosphere.dRange = range_inc; + return 1; } + static PyMethodDef methods[] = { {"raytrace_2d", raytrace_2d, METH_VARARGS, ""}, {NULL, NULL, 0, NULL}}; diff --git a/modules/source/raytrace_3d.c b/modules/source/raytrace_3d.c index e5c0c2e..fe95759 100644 --- a/modules/source/raytrace_3d.c +++ b/modules/source/raytrace_3d.c @@ -17,10 +17,10 @@ extern void raytrace_3d_(double *start_lat, double *start_long, double *start_he static PyObject *buildOutput(int num_rays, int *nhops_attempted, int *npts_in_ray, double *ray_data, double *bearings_data, double *ray_path_data, double *freqs_data, double *elevs_data, int *ray_label, double *ray_state_vec_out); -static void buildIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, +static int buildIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, PyArrayObject *Bx, PyArrayObject *By, PyArrayObject *Bz, PyObject *iono_grid_parms, PyObject *geomag_grid_parms); -static void verifyIono(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, +static int verifyIono(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, PyArrayObject *Bx, PyArrayObject *By, PyArrayObject *Bz, PyObject *iono_grid_parms, PyObject *geomag_grid_parms); void stepmemcpyd(double *dest, double *src, int step, int num_vals); @@ -43,6 +43,8 @@ static void clear_ionosphere(void) } } + + PyObject *raytrace_3d(PyObject *self, PyObject *args) { char message[256]; @@ -110,9 +112,9 @@ PyObject *raytrace_3d(PyObject *self, PyObject *args) if (init_ionosphere) { - verifyIono(iono_en_grid, iono_en_grid_5, collision_grid, + if (!verifyIono(iono_en_grid, iono_en_grid_5, collision_grid, Bx, By, Bz, - iono_grid_parms, geomag_grid_parms); + iono_grid_parms, geomag_grid_parms)) return NULL;; } /* Ensure that `start_lat` is valid (-90 < nhops <= 90). */ @@ -157,9 +159,9 @@ PyObject *raytrace_3d(PyObject *self, PyObject *args) if (init_ionosphere) { - buildIonoStruct(iono_en_grid, iono_en_grid_5, collision_grid, + if (!buildIonoStruct(iono_en_grid, iono_en_grid_5, collision_grid, Bx, By, Bz, - iono_grid_parms, geomag_grid_parms); + iono_grid_parms, geomag_grid_parms)) return NULL; /* Now the ionosphere has been read in set the iono_exist_in_mem flag to indicate this for future raytrace calls */ iono_exist_in_mem = 1; @@ -400,93 +402,95 @@ static PyObject *buildOutput(int num_rays, int *nhops_attempted, int *npts_in_ra return PyTuple_Pack(3, py_rays, py_ray_paths, py_ray_states); } -static void verifyIono(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, +static int verifyIono(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, PyArrayObject *Bx, PyArrayObject *By, PyArrayObject *Bz, PyObject *iono_grid_parms, PyObject *geomag_grid_parms) { - ASSERT((PyArray_NDIM(iono_en_grid) == 3), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(iono_en_grid) == 3), PyExc_ValueError, "invalid shape for iono_en_grid"); - ASSERT((PyArray_NDIM(iono_en_grid_5) == 3), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(iono_en_grid_5) == 3), PyExc_ValueError, "invalid shape for iono_en_grid_5"); - ASSERT((PyArray_NDIM(collision_grid) == 3), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(collision_grid) == 3), PyExc_ValueError, "invalid shape for collision_grid"); npy_intp *iono_en_grid_shape = PyArray_DIMS(iono_en_grid); npy_intp *iono_en_grid_5_shape = PyArray_DIMS(iono_en_grid_5); npy_intp *collision_grid_shape = PyArray_DIMS(collision_grid); - ASSERT(( + ASSERT_INT(( iono_en_grid_shape[0] <= max_num_lat && iono_en_grid_shape[1] <= max_num_lon && iono_en_grid_shape[2] <= max_num_ht), PyExc_ValueError, "iono_en_grid is too large"); - ASSERT(( + ASSERT_INT(( iono_en_grid_5_shape[0] <= max_num_lat && iono_en_grid_5_shape[1] <= max_num_lon && iono_en_grid_5_shape[2] <= max_num_ht), PyExc_ValueError, "iono_en_grid_5 is too large"); - ASSERT(( + ASSERT_INT(( collision_grid_shape[0] <= max_num_lat && collision_grid_shape[1] <= max_num_lon && collision_grid_shape[2] <= max_num_ht), PyExc_ValueError, "collision_grid is too large"); - ASSERT(( + ASSERT_INT(( iono_en_grid_shape[0] == iono_en_grid_5_shape[0] && iono_en_grid_shape[0] == collision_grid_shape[0]), PyExc_ValueError, "ionosphere grids have inconsistent row counts"); - ASSERT(( + ASSERT_INT(( iono_en_grid_shape[1] == iono_en_grid_5_shape[1] && iono_en_grid_shape[1] == collision_grid_shape[1]), PyExc_ValueError, "ionosphere grids have inconsistent column counts"); // check B grid for shape and consistency - ASSERT((PyArray_NDIM(Bx) == 3), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(Bx) == 3), PyExc_ValueError, "invalid shape for Bx"); - ASSERT((PyArray_NDIM(By) == 3), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(By) == 3), PyExc_ValueError, "invalid shape for By"); - ASSERT((PyArray_NDIM(By) == 3), PyExc_ValueError, + ASSERT_INT((PyArray_NDIM(By) == 3), PyExc_ValueError, "invalid shape for Bz"); npy_intp *Bx_shape = PyArray_DIMS(Bx); npy_intp *By_shape = PyArray_DIMS(By); npy_intp *Bz_shape = PyArray_DIMS(Bz); - ASSERT(( + ASSERT_INT(( Bx_shape[0] <= 101 && Bx_shape[1] <= 101 && Bx_shape[2] <= 201), PyExc_ValueError, "Bx is too large"); - ASSERT(( + ASSERT_INT(( By_shape[0] <= 101 && By_shape[1] <= 101 && By_shape[2] <= 201), PyExc_ValueError, "By is too large"); - ASSERT(( + ASSERT_INT(( Bz_shape[0] <= 101 && Bz_shape[1] <= 101 && Bz_shape[2] <= 201), PyExc_ValueError, "Bz is too large"); - ASSERT(( + ASSERT_INT(( Bx_shape[0] == By_shape[0] && By_shape[0] == Bz_shape[0]), PyExc_ValueError, "B grids have inconsistent row counts"); - ASSERT(( + ASSERT_INT(( Bx_shape[1] == By_shape[1] && By_shape[1] == Bz_shape[1]), PyExc_ValueError, "B grids have inconsistent column counts"); - ASSERT(( + ASSERT_INT(( Bx_shape[2] == By_shape[2] && By_shape[2] == Bz_shape[2]), PyExc_ValueError, "B grids have inconsistent height counts"); // ensure the parms are valid - ASSERT((PyList_Size(iono_grid_parms) == 9), PyExc_ValueError, + ASSERT_INT((PyList_Size(iono_grid_parms) == 9), PyExc_ValueError, "invalid shape for iono_grid_parms"); - ASSERT((PyList_Size(geomag_grid_parms) == 9), PyExc_ValueError, + ASSERT_INT((PyList_Size(geomag_grid_parms) == 9), PyExc_ValueError, "invalid shape for geomag_grid_parms"); + return 1; } -static void buildIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, + +static int buildIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_grid_5, PyArrayObject *collision_grid, PyArrayObject *Bx, PyArrayObject *By, PyArrayObject *Bz, PyObject *iono_grid_parms, PyObject *geomag_grid_parms) { @@ -523,24 +527,24 @@ static void buildIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_ ptr_ionosphere->lat_min = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 0)); //num of columns in ion_en-grid_5 ptr_ionosphere->lat_inc = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 1)); //num of rows in iono_en_grid_5 // /* Ensure that `start_lat` is valid (-90 < nhops <= 90). */ - ASSERT((ptr_ionosphere->lat_min >= -90.0 && ptr_ionosphere->lat_min <= 90.0), PyExc_ValueError, + ASSERT_INT((ptr_ionosphere->lat_min >= -90.0 && ptr_ionosphere->lat_min <= 90.0), PyExc_ValueError, "lat_min is invalid; must be within the range of -90 through 90"); ptr_ionosphere->num_lat = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 2)); //range_inc /* Ensure that `start_lon` is valid (-180 < nhops <= 180). */ - ASSERT((grid_shape[0] == ptr_ionosphere->num_lat), PyExc_ValueError, + ASSERT_INT((grid_shape[0] == ptr_ionosphere->num_lat), PyExc_ValueError, "grid shape != num_lat"); ptr_ionosphere->lat_max = ptr_ionosphere->lat_min + (ptr_ionosphere->num_lat - 1) * ptr_ionosphere->lat_inc; ptr_ionosphere->lon_min = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 3)); - ASSERT((ptr_ionosphere->lon_min >= -180 && ptr_ionosphere->lon_min <= 180.0), PyExc_ValueError, + ASSERT_INT((ptr_ionosphere->lon_min >= -180 && ptr_ionosphere->lon_min <= 180.0), PyExc_ValueError, "lon_min must be within -180 and 180"); ptr_ionosphere->lon_inc = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 4)); ptr_ionosphere->num_lon = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 5)); - ASSERT((grid_shape[1] == ptr_ionosphere->num_lon), PyExc_ValueError, + ASSERT_INT((grid_shape[1] == ptr_ionosphere->num_lon), PyExc_ValueError, "grid shape[1] != num_lon"); ptr_ionosphere->lon_max = ptr_ionosphere->lon_min + @@ -548,7 +552,7 @@ static void buildIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_ ptr_ionosphere->ht_min = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 6)); ptr_ionosphere->ht_inc = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 7)); ptr_ionosphere->num_ht = PyFloat_AsDouble(PyList_GetItem(iono_grid_parms, 8)); - ASSERT((grid_shape[2] == ptr_ionosphere->num_ht), PyExc_ValueError, + ASSERT_INT((grid_shape[2] == ptr_ionosphere->num_ht), PyExc_ValueError, "grid shape[2] != num_ht"); ptr_ionosphere->ht_max = ptr_ionosphere->ht_min + @@ -570,24 +574,24 @@ static void buildIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_ } } geomag_field.lat_min = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 0)); - ASSERT((geomag_field.lat_min >= -90.0 && geomag_field.lat_min <= 90), PyExc_ValueError, + ASSERT_INT((geomag_field.lat_min >= -90.0 && geomag_field.lat_min <= 90), PyExc_ValueError, "The start latitude of the input geomagnetic field grid must be in the range -90 to 90 degrees."); geomag_field.lat_inc = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 1)); geomag_field.num_lat = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 2)); - ASSERT((dims[0] == geomag_field.num_lat), PyExc_ValueError, + ASSERT_INT((dims[0] == geomag_field.num_lat), PyExc_ValueError, "The number of latitudes in the input geomagnetic field grid does not match the input grid parameter."); geomag_field.lat_max = geomag_field.lat_min + (geomag_field.num_lat - 1) * geomag_field.lat_inc; geomag_field.lon_min = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 3)); - ASSERT((geomag_field.lon_min >= -180.0 && geomag_field.lon_min <= 180.0), PyExc_ValueError, + ASSERT_INT((geomag_field.lon_min >= -180.0 && geomag_field.lon_min <= 180.0), PyExc_ValueError, "he start longitude of the input geomagnetic field grid must be in the range -180 to 180 degrees."); geomag_field.lon_inc = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 4)); geomag_field.num_lon = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 5)); - ASSERT((dims[1] == geomag_field.num_lon), PyExc_ValueError, + ASSERT_INT((dims[1] == geomag_field.num_lon), PyExc_ValueError, "The number of longitudes in the input geomagnetic field grid does not match the input grid parameter."); geomag_field.lon_max = geomag_field.lon_min + @@ -595,12 +599,14 @@ static void buildIonoStruct(PyArrayObject *iono_en_grid, PyArrayObject *iono_en_ geomag_field.ht_min = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 6)); geomag_field.ht_inc = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 7)); geomag_field.num_ht = PyFloat_AsDouble(PyList_GetItem(geomag_grid_parms, 8)); - ASSERT((dims[2] == geomag_field.num_ht), PyExc_ValueError, + ASSERT_INT((dims[2] == geomag_field.num_ht), PyExc_ValueError, "The number of heights in the input geomagnetic field grid does not match the input grid parameter."); geomag_field.ht_max = geomag_field.ht_min + (geomag_field.num_ht - 1) * geomag_field.ht_inc; + return 1; } + static PyMethodDef methods[] = { {"raytrace_3d", raytrace_3d, METH_VARARGS, ""}, {NULL, NULL, 0, NULL}}; diff --git a/setup.py b/setup.py index af4d300..faf9db9 100644 --- a/setup.py +++ b/setup.py @@ -1,87 +1,152 @@ - import glob import os import platform +import struct -#import setuptools import numpy as np -from distutils.core import setup -from distutils.core import Extension - - -if platform.system() not in ['Linux']: - raise OSError('Unrecognized or unsupported operating system.') - -# -# Configure PHaRLAP. -# +try: + from setuptools import setup, Extension +except ImportError: + from distutils.core import setup + from distutils.core import Extension + + +# --------------------------------------------------------------------------- +# Platform detection +# --------------------------------------------------------------------------- +_sys = platform.system() +_machine = platform.machine() + +if _sys == 'Linux': + _lib_subdir = 'linux' + _intel_fortran_libs = ['ifcore', 'imf', 'iomp5', 'irc', 'svml'] +elif _sys == 'Darwin': + # PHaRLAP 4.7.4 ships two macOS variants: + # maca — Apple Silicon (arm64) + # maci — Intel (x86_64) + # The maca/maci static libs were compiled with GCC/gfortran and require + # the gfortran runtime (__gfortran_* symbols). No Intel Fortran needed. + _lib_subdir = 'maca' if _machine == 'arm64' else 'maci' + _intel_fortran_libs = [] # not needed + # Locate Homebrew gfortran runtime (required for __gfortran_* symbols). + import subprocess, shutil + _gfc = shutil.which('gfortran') + if _gfc: + _gfc_libdir = subprocess.check_output( + [_gfc, '-print-file-name=libgfortran.dylib'], + text=True + ).strip() + _gfc_libdir = os.path.dirname(_gfc_libdir) + else: + _gfc_libdir = '/opt/homebrew/lib/gcc/current' + _extra_darwin_lib_dirs = [_gfc_libdir] + _extra_darwin_libs = ['gfortran', 'gomp'] +else: + raise OSError(f'Unsupported operating system: {_sys}') + +_extra_lib_dirs = _extra_darwin_lib_dirs if _sys == 'Darwin' else [] +_extra_libs = _extra_darwin_libs if _sys == 'Darwin' else [] + +# --------------------------------------------------------------------------- +# Locate PHaRLAP +# --------------------------------------------------------------------------- if 'PHARLAP_HOME' not in os.environ: - raise OSError('The environment variable "PHARLAP_HOME" must be defined.') - -if 'LD_LIBRARY' not in os.environ: - raise OSError('The environment variable "LD_LIBRARY" must be defined.') - -if 'PYTHONPATH' not in os.environ: - raise OSError('The environment variable "PYTHONPATH" must be defined.') - -pharlap_path = os.getenv('PHARLAP_HOME') -intel_path = os.getenv('LD_LIBRARY') -py_path = os.getenv('PYTHONPATH') - -if not os.path.isdir(py_path): - raise OSError('The environment variable "PYTHONPATH" is invalid.') + raise OSError('Set PHARLAP_HOME to the PHaRLAP install directory before building.') +pharlap_path = os.environ['PHARLAP_HOME'] if not os.path.isdir(pharlap_path): - raise OSError('The environment variable "PHARLAP_HOME" is invalid.') - -if not os.path.isdir(intel_path): - raise OSError('The environment variable "LD_LIBRARY" is invalid.') + raise OSError(f'PHARLAP_HOME does not exist: {pharlap_path}') pharlap_include_path = os.path.join(pharlap_path, 'src', 'C') -pharlap_lib_path = os.path.join(pharlap_path, 'lib', 'linux') -# Define native modules. -# - +pharlap_lib_path = os.path.join(pharlap_path, 'lib', _lib_subdir) + +if not os.path.isdir(pharlap_lib_path): + raise OSError(f'PHaRLAP library path not found: {pharlap_lib_path}') + +# On Linux the Intel Fortran redistributable path is still required. +intel_lib_dirs = [] +if _sys == 'Linux': + if 'LD_LIBRARY' not in os.environ: + raise OSError('Set LD_LIBRARY to the Intel Fortran redistributable lib dir on Linux.') + intel_path = os.environ['LD_LIBRARY'] + if not os.path.isdir(intel_path): + raise OSError(f'LD_LIBRARY does not exist: {intel_path}') + intel_lib_dirs = [intel_path] + +# PHaRLAP 4.7.4 ships libiri2020 (consolidated; replaces iri2007/2012/2016). +# Modules that needed legacy IRI versions are skipped when those libs are absent. +_available_libs = { + os.path.splitext(f)[0].replace('lib', '') + for f in os.listdir(pharlap_lib_path) + if f.startswith('lib') +} + +# --------------------------------------------------------------------------- +# Module definitions +# --------------------------------------------------------------------------- native_modules = [] COMMON_GLOB = glob.glob('modules/source/common/*.c') + +def _libs(*base): + """Append Intel Fortran runtime libs on Linux; omit on macOS.""" + return list(base) + _intel_fortran_libs + + def create_module(name, libraries): + """Register a C-extension module if all required native libs are present.""" + missing = [lib for lib in libraries if lib not in _available_libs + and lib not in _intel_fortran_libs + and lib not in ('m',)] + if missing: + print(f' SKIP pylap.{name} (missing PHaRLAP libs: {missing})') + return + src = os.path.join('modules', 'source', name + '.c') + if not os.path.exists(src): + print(f' SKIP pylap.{name} (source not found: {src})') + return native_modules.append(Extension( 'pylap.' + name, - sources=['modules/source/' + name + '.c'] + COMMON_GLOB, - include_dirs=[np.get_include(), pharlap_include_path, "include"], - library_dirs=[pharlap_lib_path, intel_path], - libraries=libraries)) - pass - -create_module('abso_bg', ['propagation', 'maths', 'iri2016', 'ifcore', 'imf', 'irc', 'svml']) -create_module('dop_spread_eq', ['propagation', 'iri2016', 'ifcore', 'imf', 'irc', 'svml']) -create_module('ground_bs_loss', ['propagation', 'ifcore', 'imf']) -create_module('ground_fs_loss', ['propagation', 'ifcore', 'imf']) -create_module('igrf2007', ['maths', 'iri2007', 'ifcore', 'imf', 'irc']) -create_module('igrf2011', ['maths', 'iri2012', 'ifcore', 'imf', 'irc']) -create_module('igrf2016', ['maths', 'iri2016', 'ifcore', 'imf', 'irc', 'svml']) -create_module('iri2007', ['iri2007', 'ifcore', 'imf', 'irc', 'svml']) -create_module('iri2012', ['iri2012', 'ifcore', 'imf', 'irc', 'svml']) -create_module('iri2016', ['iri2016', 'ifcore', 'imf', 'irc', 'svml']) -create_module('irreg_strength', ['propagation', 'iri2016', 'ifcore', 'imf', 'irc', 'svml']) -create_module('nrlmsise00', ['maths', 'iri2016', 'ifcore', 'imf', 'irc', 'svml']) -create_module('raytrace_2d', ['propagation', 'maths', 'ifcore','imf','iomp5']) -create_module('raytrace_2d_sp', ['propagation', 'maths', 'ifcore', 'imf', 'iomp5']) -create_module('raytrace_3d', ['propagation', 'maths', 'ifcore', 'imf', 'iomp5']) -create_module('raytrace_3d_sp', ['propagation', 'maths', 'ifcore', 'imf', 'iomp5']) - -# -# Create module. -# -#with open('requirements.txt') as f: - # requirements = f.read().splitlines() + sources=[src] + COMMON_GLOB, + include_dirs=[np.get_include(), pharlap_include_path, + os.path.join('modules', 'include')], + library_dirs=[pharlap_lib_path] + intel_lib_dirs + _extra_lib_dirs, + libraries=libraries + _extra_libs, + )) + print(f' BUILD pylap.{name}') + + +# PHaRLAP 4.7.4: IRI consolidated into iri2020. Modules that required +# the legacy iri2007/iri2012/iri2016 archives are updated accordingly; +# those that only existed as thin wrappers for the legacy models are skipped. +print(f'\nConfiguring pyLAP for PHaRLAP on {_sys}/{_machine} (lib/{_lib_subdir})\n') + +create_module('abso_bg', _libs('propagation', 'maths', 'iri2020')) +create_module('dop_spread_eq', _libs('propagation', 'iri2020')) +create_module('ground_bs_loss',_libs('propagation')) +create_module('ground_fs_loss',_libs('propagation')) +# igrf2007/igrf2011 need legacy iri2007/iri2012 — skipped on PHaRLAP 4.7.4 +create_module('igrf2007', _libs('maths', 'iri2007')) +create_module('igrf2011', _libs('maths', 'iri2012')) +create_module('igrf2016', _libs('maths', 'iri2020')) +# Standalone IRI wrappers — skipped for legacy versions not in 4.7.4 +create_module('iri2007', _libs('iri2007')) +create_module('iri2012', _libs('iri2012')) +create_module('iri2016', _libs('iri2020')) +create_module('irreg_strength',_libs('propagation', 'iri2020')) +create_module('nrlmsise00', _libs('maths', 'iri2020')) +create_module('raytrace_2d', _libs('propagation', 'maths')) +create_module('raytrace_2d_sp',_libs('propagation', 'maths')) +create_module('raytrace_3d', _libs('propagation', 'maths')) +create_module('raytrace_3d_sp',_libs('propagation', 'maths')) + +print() setup( - name='pylap', - packages=['modules/pylap'], - #install_requires=requirements, - version='0.1.0-alpha', - description='A numpy-compatible Python 3 wrapper for the PHaRLAP ionospheric raytracer', - ext_modules=native_modules + name='pylap', + version='0.1.0-alpha', + description='A numpy-compatible Python 3 wrapper for the PHaRLAP ionospheric raytracer', + packages=['pylap'], + package_dir={'pylap': 'modules/pylap'}, + ext_modules=native_modules, ) From 651977dd9ff7b556514c0ef3eef95732af9a8840 Mon Sep 17 00:00:00 2001 From: mijahauan Date: Sun, 15 Mar 2026 20:39:27 -0500 Subject: [PATCH 2/9] fix: iri2016 module now calls IRI-2020 (PHaRLAP 4.7.4) modules/source/iri2016.c: - iri2016_calc_ -> iri2020_calc_ (renamed Fortran symbol in PHaRLAP 4.7.4) - check_ref_data("iri2016") -> check_ref_data("iri2020") (dat subdir renamed) modules/source/common/check_ref_data.c: - Add check_iri2020() function verifying dat/iri2020/ file layout: igrf2025.dat/s, dgrf1945-2010, apf107.dat, ig_rz.dat, mcsat11-22.dat, ccir11-22.asc, ursi11-22.asc - Add "iri2020" dispatch case in check_ref_data() --- modules/source/common/check_ref_data.c | 38 ++++++++++++++++++++++++++ modules/source/iri2016.c | 6 ++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/modules/source/common/check_ref_data.c b/modules/source/common/check_ref_data.c index cb029b7..2d0fc7e 100644 --- a/modules/source/common/check_ref_data.c +++ b/modules/source/common/check_ref_data.c @@ -8,6 +8,7 @@ int check_iri2007(const char *); int check_iri2012(const char *); int check_iri2016(const char *); +int check_iri2020(const char *); int check_land_sea(const char *); int check_file_exists(const char *); @@ -24,6 +25,8 @@ int check_ref_data(const char *files_to_check) return check_iri2012(refdata_dir); } else if (strncmp(files_to_check, "iri2016", 7) == 0) { return check_iri2016(refdata_dir); + } else if (strncmp(files_to_check, "iri2020", 7) == 0) { + return check_iri2020(refdata_dir); } else if (strncmp(files_to_check, "land_sea", 8) == 0) { return check_land_sea(refdata_dir); } @@ -134,6 +137,41 @@ int check_iri2016(const char *refdata_dir) return 1; } +int check_iri2020(const char *refdata_dir) +{ + char filename[256]; + + snprintf(filename, 256, "%s/iri2020/igrf2025.dat", refdata_dir); + ASSERT_INT_NOMSG(check_file_exists(filename)); + + snprintf(filename, 256, "%s/iri2020/igrf2025s.dat", refdata_dir); + ASSERT_INT_NOMSG(check_file_exists(filename)); + + for (int year = 1945; year <= 2010; year += 5) { + snprintf(filename, 256, "%s/iri2020/dgrf%d.dat", refdata_dir, year); + ASSERT_INT_NOMSG(check_file_exists(filename)); + } + + snprintf(filename, 256, "%s/iri2020/apf107.dat", refdata_dir); + ASSERT_INT_NOMSG(check_file_exists(filename)); + + snprintf(filename, 256, "%s/iri2020/ig_rz.dat", refdata_dir); + ASSERT_INT_NOMSG(check_file_exists(filename)); + + for (int month = 1; month <= 12; month++) { + snprintf(filename, 256, "%s/iri2020/mcsat%d.dat", refdata_dir, month+10); + ASSERT_INT_NOMSG(check_file_exists(filename)); + + snprintf(filename, 256, "%s/iri2020/ccir%d.asc", refdata_dir, month+10); + ASSERT_INT_NOMSG(check_file_exists(filename)); + + snprintf(filename, 256, "%s/iri2020/ursi%d.asc", refdata_dir, month+10); + ASSERT_INT_NOMSG(check_file_exists(filename)); + } + + return 1; +} + int check_land_sea(const char *refdata_dir) { char filename[256]; diff --git a/modules/source/iri2016.c b/modules/source/iri2016.c index 07b89cf..ea34cbb 100644 --- a/modules/source/iri2016.c +++ b/modules/source/iri2016.c @@ -1,6 +1,6 @@ #include "../include/pharlap.h" -extern void iri2016_calc_(int *jf, int *jmag, float *glat, float *glon, +extern void iri2020_calc_(int *jf, int *jmag, float *glat, float *glon, int *year, int *mmdd, float *dhour, float *heibeg, float *heiend, float *heistep, float *outf, float *oarr); @@ -21,7 +21,7 @@ static PyObject *iri2016(PyObject *self, PyObject *args) &PyList_Type, &tm, &height_start, &height_step, &num_heights, &PyDict_Type, &iri_options)); - ASSERT_NOMSG(check_ref_data("iri2016")); + ASSERT_NOMSG(check_ref_data("iri2020")); /* Parse UT. */ int *ut = (int *)malloc(5 * sizeof(int)); @@ -562,7 +562,7 @@ if(obj!=NULL){ int jmag = 0; - iri2016_calc_(jf, &jmag, &glat, &glon, &year, &mmdd, &dhour, &height_start, + iri2020_calc_(jf, &jmag, &glat, &glon, &year, &mmdd, &dhour, &height_start, &height_end, &height_step, outf, oarr); /* From 2efa41eabab87e447f6620bee48f627e17922613 Mon Sep 17 00:00:00 2001 From: mijahauan Date: Mon, 16 Mar 2026 05:29:33 -0500 Subject: [PATCH 3/9] Fix multi-hop stride: ray_data stride should be num_rays*19 not num_rays*9 The ray_data array is allocated as 19*nhops*num_rays doubles, so the hop-to-hop stride must be num_rays*19. The previous stride of num_rays*9 caused ground_range to be read from wrong offsets for hops > 1. --- modules/source/raytrace_2d.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/source/raytrace_2d.c b/modules/source/raytrace_2d.c index 31dca9d..406c67d 100644 --- a/modules/source/raytrace_2d.c +++ b/modules/source/raytrace_2d.c @@ -333,7 +333,7 @@ static PyObject *buildOutput(int num_rays, int *nhops_attempted, int *npts_in_ra tmp = PyArray_ZEROS(1, nhops_dims, NPY_DOUBLE, 0); tmp_data = PyArray_DATA((PyArrayObject *)tmp); - stepmemcpyd(tmp_data, &ray_data[idx], num_rays * 9, + stepmemcpyd(tmp_data, &ray_data[idx], num_rays * 19, nhops_attempted[ray_id]); PyDict_SetItemString(py_ray_data, rays_fields[field_id], tmp); } From beda5f505c2de1917b430935532aa858655024a5 Mon Sep 17 00:00:00 2001 From: mijahauan Date: Mon, 16 Mar 2026 05:43:23 -0500 Subject: [PATCH 4/9] gitignore: exclude compiled .so extensions and build directory --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a52a30c..e886008 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.pyc +*.so +build/ Ionosphere/__pycache__/* From 27f292cb5ff881983a7d991aff90ff0bd885949b Mon Sep 17 00:00:00 2001 From: mijahauan Date: Wed, 18 Mar 2026 09:16:18 +0000 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20ray=5Fdata=20buffer=2019=E2=86=9224?= =?UTF-8?q?=20fields=20per=20hop=20(PHaRLAP=204.7.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHaRLAP 4.7.4 outputs 24 doubles per hop in ray_data (up from 19). The undersized allocation caused heap corruption ('double free or corruption') and the wrong stride (num_rays*19 instead of num_rays*24) produced garbled output fields. --- modules/source/raytrace_2d.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/source/raytrace_2d.c b/modules/source/raytrace_2d.c index 406c67d..b0e3074 100644 --- a/modules/source/raytrace_2d.c +++ b/modules/source/raytrace_2d.c @@ -206,7 +206,7 @@ static PyObject *raytrace_2d(PyObject *self, PyObject *args) int *nhops_attempted = (int *)malloc(num_rays * sizeof(int)); int *npts_in_ray = (int *)malloc(num_rays * sizeof(int)); - ray_data = (double *)malloc(19 * nhops * num_rays * sizeof(double)); + ray_data = (double *)malloc(24 * nhops * num_rays * sizeof(double)); ray_path_data = (double *)malloc(9 * MAX_POINTS_IN_RAY * num_rays * sizeof(double)); int *ray_label = (int *)malloc(nhops * num_rays * sizeof(int)); @@ -333,7 +333,7 @@ static PyObject *buildOutput(int num_rays, int *nhops_attempted, int *npts_in_ra tmp = PyArray_ZEROS(1, nhops_dims, NPY_DOUBLE, 0); tmp_data = PyArray_DATA((PyArrayObject *)tmp); - stepmemcpyd(tmp_data, &ray_data[idx], num_rays * 19, + stepmemcpyd(tmp_data, &ray_data[idx], num_rays * 24, nhops_attempted[ray_id]); PyDict_SetItemString(py_ray_data, rays_fields[field_id], tmp); } From d5d4e6e2c00f87a687ee7a7a28cd4202a9239cb1 Mon Sep 17 00:00:00 2001 From: mijahauan Date: Wed, 18 Mar 2026 09:16:30 +0000 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20unify=20Linux/macOS=20build=20?= =?UTF-8?q?=E2=80=94=20drop=20Intel=20Fortran,=20use=20gfortran=20everywhe?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHaRLAP 4.7.4 ships GCC-compiled static libs on all platforms (Linux, macOS arm64, macOS x86_64). The previous setup.py required Intel Fortran (ifcore/imf/irc/svml/iomp5) and a LD_LIBRARY env var on Linux. Now both platforms use the same gfortran/gomp linking path: - Auto-detect gfortran via 'which gfortran' - Locate libgfortran.so/.dylib via 'gfortran -print-file-name' - Fallback to /opt/homebrew/lib/gcc/current on macOS only - No LD_LIBRARY env var needed on any platform --- setup.py | 59 +++++++++++++++++++++++--------------------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/setup.py b/setup.py index faf9db9..cc20281 100644 --- a/setup.py +++ b/setup.py @@ -17,35 +17,33 @@ _sys = platform.system() _machine = platform.machine() +# PHaRLAP 4.7.4 ships GCC-compiled static libraries on all platforms. +# We need gfortran runtime on both Linux and macOS. +import subprocess, shutil + if _sys == 'Linux': _lib_subdir = 'linux' - _intel_fortran_libs = ['ifcore', 'imf', 'iomp5', 'irc', 'svml'] elif _sys == 'Darwin': - # PHaRLAP 4.7.4 ships two macOS variants: - # maca — Apple Silicon (arm64) - # maci — Intel (x86_64) - # The maca/maci static libs were compiled with GCC/gfortran and require - # the gfortran runtime (__gfortran_* symbols). No Intel Fortran needed. _lib_subdir = 'maca' if _machine == 'arm64' else 'maci' - _intel_fortran_libs = [] # not needed - # Locate Homebrew gfortran runtime (required for __gfortran_* symbols). - import subprocess, shutil - _gfc = shutil.which('gfortran') - if _gfc: - _gfc_libdir = subprocess.check_output( - [_gfc, '-print-file-name=libgfortran.dylib'], - text=True - ).strip() - _gfc_libdir = os.path.dirname(_gfc_libdir) - else: - _gfc_libdir = '/opt/homebrew/lib/gcc/current' - _extra_darwin_lib_dirs = [_gfc_libdir] - _extra_darwin_libs = ['gfortran', 'gomp'] else: raise OSError(f'Unsupported operating system: {_sys}') -_extra_lib_dirs = _extra_darwin_lib_dirs if _sys == 'Darwin' else [] -_extra_libs = _extra_darwin_libs if _sys == 'Darwin' else [] +# Locate gfortran runtime library directory. +_extra_lib_dirs = [] +_gfc = shutil.which('gfortran') +if _gfc: + _libname = 'libgfortran.dylib' if _sys == 'Darwin' else 'libgfortran.so' + _gfc_libdir = subprocess.check_output( + [_gfc, f'-print-file-name={_libname}'], + text=True + ).strip() + _gfc_libdir = os.path.dirname(_gfc_libdir) + if _gfc_libdir: + _extra_lib_dirs = [_gfc_libdir] +elif _sys == 'Darwin': + _extra_lib_dirs = ['/opt/homebrew/lib/gcc/current'] + +_extra_libs = ['gfortran', 'gomp'] # --------------------------------------------------------------------------- # Locate PHaRLAP @@ -63,15 +61,7 @@ if not os.path.isdir(pharlap_lib_path): raise OSError(f'PHaRLAP library path not found: {pharlap_lib_path}') -# On Linux the Intel Fortran redistributable path is still required. -intel_lib_dirs = [] -if _sys == 'Linux': - if 'LD_LIBRARY' not in os.environ: - raise OSError('Set LD_LIBRARY to the Intel Fortran redistributable lib dir on Linux.') - intel_path = os.environ['LD_LIBRARY'] - if not os.path.isdir(intel_path): - raise OSError(f'LD_LIBRARY does not exist: {intel_path}') - intel_lib_dirs = [intel_path] +# No Intel Fortran needed — PHaRLAP 4.7.4 libs are GCC-compiled on all platforms. # PHaRLAP 4.7.4 ships libiri2020 (consolidated; replaces iri2007/2012/2016). # Modules that needed legacy IRI versions are skipped when those libs are absent. @@ -89,14 +79,13 @@ def _libs(*base): - """Append Intel Fortran runtime libs on Linux; omit on macOS.""" - return list(base) + _intel_fortran_libs + """Return the base PHaRLAP libs plus platform math lib.""" + return list(base) + ['m'] def create_module(name, libraries): """Register a C-extension module if all required native libs are present.""" missing = [lib for lib in libraries if lib not in _available_libs - and lib not in _intel_fortran_libs and lib not in ('m',)] if missing: print(f' SKIP pylap.{name} (missing PHaRLAP libs: {missing})') @@ -110,7 +99,7 @@ def create_module(name, libraries): sources=[src] + COMMON_GLOB, include_dirs=[np.get_include(), pharlap_include_path, os.path.join('modules', 'include')], - library_dirs=[pharlap_lib_path] + intel_lib_dirs + _extra_lib_dirs, + library_dirs=[pharlap_lib_path] + _extra_lib_dirs, libraries=libraries + _extra_libs, )) print(f' BUILD pylap.{name}') From 16b9b807cf5780d9729312c0dcef758898525f3e Mon Sep 17 00:00:00 2001 From: mijahauan Date: Wed, 18 Mar 2026 12:25:54 +0000 Subject: [PATCH 7/9] fix: add -Wno-error=incompatible-pointer-types for GCC 14+ GCC 14 (Debian 13/trixie) promotes -Wincompatible-pointer-types to error. pylap C extensions return PyArrayObject* as PyObject* (safe upcast via numpy API). This flag suppresses the false positive. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index cc20281..524ab77 100644 --- a/setup.py +++ b/setup.py @@ -101,6 +101,7 @@ def create_module(name, libraries): os.path.join('modules', 'include')], library_dirs=[pharlap_lib_path] + _extra_lib_dirs, libraries=libraries + _extra_libs, + extra_compile_args=["-Wno-error=incompatible-pointer-types"], )) print(f' BUILD pylap.{name}') From 51287b4ab88d2649272c01372de1df985940c6ad Mon Sep 17 00:00:00 2001 From: mijahauan Date: Mon, 20 Apr 2026 13:13:43 +0000 Subject: [PATCH 8/9] docs: document fork patches, simplify install (drop Intel Fortran) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "About this fork" section to README listing the patches that distinguish this fork from HamSCI/PyLap (PHaRLAP 4.7.4 support, unified gfortran build, multi-hop stride fix, GCC 14+ compat, IRI-2020 wrapper, Fortran SAVE/Ne-units caller notes). Drop Intel Fortran redistributable from install flow — commit d5d4e6e unified Linux/macOS on gfortran, but README and setup.sh still required the Intel libs. Remove Intel lib detection, LD_LIBRARY export, and compilervars.sh sourcing from setup.sh; remove corresponding sections from README. Add IRI build-verification smoke test to README so a collaborator can confirm linkage before running the raytracing examples. Ignore venv/ and the test-output PNG. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 +- README.md | 166 +++++++++++++++++++++++++++++++++++++++++------------ setup.sh | 99 ++++++++++++++++++++------------ 3 files changed, 194 insertions(+), 75 deletions(-) diff --git a/.gitignore b/.gitignore index e886008..27a8516 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ *.pyc *.so build/ - +venv/ +__pycache__/ Ionosphere/__pycache__/* Examples/results/* +iri2020_test_output.png *.nc .nc *.csv diff --git a/README.md b/README.md index d708213..a6e2442 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,166 @@ -# Experimental Software Warning +# PyLap -PyLap is based on the scientific software PHaRLAP and is not meant for operational use as is stated on PHaRLAP's website. any and all risk falls to you if you are using this software. +A Python 3 wrapper for the PHaRLAP ionospheric raytracer. -## Linux Install Instructions ## +> **Warning:** PyLap is based on the scientific software PHaRLAP and is not meant for operational use as stated on PHaRLAP's website. Any and all risk falls to you if you are using this software. -*Note* This install is only available for Ubuntu linux systems with X86 CPU's *Note* +## About this fork -*Note* It is also possible to install PyLap using WSL2 with Ubuntu for windows machines running windows 10 and windows 11 *Note* +This is a patched fork of [HamSCI/PyLap](https://github.com/HamSCI/PyLap) maintained for use with [hf-timestd](https://github.com/mijahauan/hf-timestd) and other HF-propagation projects. The upstream version will not build or run correctly against current PHaRLAP (4.7.4) without these patches: -Download Steps +- **PHaRLAP 4.7.4 support** — upstream targets 4.5.x; this fork's `setup.py` links against 4.7.4 static libraries and gracefully skips legacy IRI modules (`iri2007`, `iri2012`) when their `.a` files are absent. +- **Cross-platform builds** — adds macOS arm64 and macOS x86_64 alongside Linux x86_64 (upstream is Linux-only). +- **Unified gfortran build** — Intel Fortran redistributable is no longer required; Linux and macOS both build against gfortran, eliminating a significant install-time hurdle. +- **GCC 14+ compatibility** — adds `-Wno-error=incompatible-pointer-types` so builds succeed on Debian 13/trixie and other distros that ship GCC 14 (which promotes this warning to an error). +- **Multi-hop stride fix in `raytrace_2d.c`** — the `ray_data` C-array hop stride was `num_rays × 9`; corrected to `num_rays × 24` so fields past hop 0 are read from the right offsets under PHaRLAP 4.7.4. +- **`iri2016` module now calls IRI-2020** — PHaRLAP 4.7.4 replaced IRI-2016 internals with IRI-2020; the fork's wrapper was updated to match. +- **Fortran SAVE-variable segfault workaround (caller-side note)** — repeated `raytrace_2d` calls can crash due to persistent Fortran state; make a single call with `nhops=max_hops` and iterate hops in Python. +- **Ne unit convention (caller-side note)** — IRI-2020 returns electron density in m⁻³ but `raytrace_2d` expects cm⁻³; scale by 10⁻⁶ before passing the grid. -1. Create a folder on the home directory where you will place all of the downloaded resources. +## Requirements -2. Download PHaRLAP toolbox for matlab and unzip to the directory that you just created https://www.dst.defence.gov.au/our-technologies/pharlap-provision-high-frequency-raytracing-laboratory-propagation-studies . +- **OS:** Ubuntu/Debian Linux (x86_64), WSL2 on Windows 10/11, macOS (arm64 or x86_64) +- **Python:** 3.8+ +- **PHaRLAP:** 4.7.4 required for raytracing (request access from DST Australia — see below) +- **gfortran:** Required to build PyLap and the IRI-2020 Python package — `apt install gfortran` on Linux, `brew install gcc` on macOS. Intel Fortran is **not** required (removed in this fork). -3. Download Pylap from github in the same directory that you just created. +## Quick Start - *Note* This works for either cloning the github repository or downloading it as a zip file *Note* +### 1. Obtain PHaRLAP -4. Download Redistributable Libraries for Intel® C++ and Fortran 2020. Which is required because the original fortran code was compiled using the Intel fortran compiler. The one used originally is available at the following download link: https://www.intel.com/content/www/us/en/developer/articles/tool/redistributable-libraries-for-intel-c-and-fortran-2020-compilers-for-linux.html +PHaRLAP is **not** redistributed with this repo — you must obtain it separately. -5. cd within a terminal window into the intel libraries folder and run install.sh, follow the prompt until install complete. when promted with where to install this, the default should be the home drive (ex. [/home/UserName]). The default location is fine to use for the installation. +**PHaRLAP 4.7.4** (required for raytracing): +- Request access at https://www.dst.defence.gov.au/our-technologies/pharlap-provision-high-frequency-raytracing-laboratory-propagation-studies +- DST will email you a download link after reviewing your request (typically 1–3 business days) +- License restricts use to research; see PHaRLAP's own license terms +- After extracting, verify `lib/` contains `linux/`, `maca/`, `maci/` subdirectories (these hold the static libraries this fork links against) +Suggested layout: -File Directory model +``` +~/pylap_project/ +├── PyLap/ # This repository +└── pharlap_4.7.4/ # PHaRLAP toolbox (from DST) +``` -├── PyLap Project folder -  ├── PyLap\ -  ├── PHARLAP\ -  └── l_comp_lib_2020.4.304_comp.for_redist\ +### 2. Run Setup Script +```bash +cd ~/pylap_project/PyLap +. ./setup.sh +# Enter: ~/pylap_project +``` -## Setup with script file +The setup script will: +- Create a Python virtual environment at `PyLap/venv/` +- Install system dependencies (requires sudo) +- Install Python packages from `requirements.txt` +- Build and install PyLap +- Add a `pylap-activate` alias to your `.bashrc` -1. cd into the pylap project directory. +### 3. Activate and Run -2. Run the setup.sh script using the command “. ./setup.sh”. running this script will promt you to enter the filepath of the folder that all of your project is installed. +```bash +# Activate the virtual environment +source venv/bin/activate +# Or use the alias: +pylap-activate -3. everything should be setup correctly! run the example files that are in the examples folder to make sure everything is setup correctly! if for some reason the example files do not work check the bashrc file in your home directory by using the command "nano .bashrc"(must be in the home directory). - - *Note* This is a one time setup and does not need to be run again unless a new install is made *Note* +# Test IRI-2020 (works without PHaRLAP) +python3 Examples/test_iri2020.py +# Test raytracing (requires PHaRLAP) +python3 Examples/ray_test1.py +``` +> **Note:** The raytracing examples (`ray_test1.py`, etc.) require PHaRLAP to be installed. Use `test_iri2020.py` to verify your installation before PHaRLAP is available. -## Manual setup +### 4. Verify the Build -5. export PHARLAP_HOME="your path to pharlap install dir" +Quick smoke test that confirms PHaRLAP linked correctly and IRI is usable: -6. export PYTHONPATH="Pylap_install_dir" +```bash +python3 -c " +import importlib, math +iri = importlib.import_module('pylap.iri2016') +_, oarr = iri.iri2016(40.0, -90.0, 100.0, [2026, 1, 15, 18, 0], 100.0, 10.0, 50, {}) +foF2 = 8.98 * math.sqrt(max(oarr[0], 0)) / 1e6 +print(f'IRI OK — foF2={foF2:.1f} MHz, hmF2={oarr[1]:.0f} km') +" +``` -7. export LD_LIBRARY="/"YOUR PATH TO DIR"/l_comp_lib_2020.4.304_comp.for_redist/compilers_and_libraries_2020.4.304/linux/compiler/lib/intel64_lin" +Expected: `IRI OK — foF2=X.X MHz, hmF2=XXX km` (values depend on solar conditions at the queried date/time). If you see `ImportError` or a Fortran/link error, `PHARLAP_HOME` and `DIR_MODELS_REF_DAT` are probably unset or pointing at the wrong directory. -8. export DIR_MODELS_REF_DAT="{PHARLAP_HOME}/dat" +## IRI-2020 Ionosphere Model -9. sudo apt-get install python3-tk python3-pil python3-pil.imagetk libqt5gui5 python3-pyqt5 +PyLap now supports IRI-2020 via the `iri2020` Python package. This works **independently of PHaRLAP** for ionosphere generation. -10. sudo apt-get install libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev +### Supported Profile Types -11. source /home/{username}/bin/compilervars.sh intel64 +| Profile Type | Description | +|--------------|-------------| +| `'iri'` or `'iri2016'` | IRI-2016 model (default, requires PHaRLAP) | +| `'iri2020'` | IRI-2020 model (standalone, no PHaRLAP needed) | +| `'iri2012'` | IRI-2012 model (requires PHaRLAP) | +| `'iri2007'` | IRI-2007 model (requires PHaRLAP) | +| `'firi'` | IRI-2016 with FIRI D-region (requires PHaRLAP) | -12. sudo apt-get install python3-pip +### Example Usage -13. pip3 install matplotlib numpy scipy qtpy +```python +from Ionosphere import gen_iono_grid_2d as gen_iono -14. cd $PYTHONPATH +# Generate ionosphere using IRI-2020 (no PHaRLAP required) +iono_pf_grid, iono_pf_grid_5, collision_freq, irreg, iono_te_grid = \ + gen_iono.gen_iono_grid_2d( + origin_lat, origin_long, R12, UT, ray_bear, + max_range, num_range, range_inc, start_height, + height_inc, num_heights, kp, doppler_flag, + 'iri2020' # Use IRI-2020 + ) +``` -15. python3 setup.py install --user +## Manual Setup -16. Use Example folder files as templates to test the installation +If you prefer not to use the setup script: - *Note* This is not a one time setup and will have to be redone if the terminal is closed out or if the code project is closed out *Note* +```bash +# 1. Set environment variables +export PHARLAP_HOME="/path/to/pharlap_4.7.4" +export PYTHONPATH="/path/to/PyLap" +export DIR_MODELS_REF_DAT="${PHARLAP_HOME}/dat" +# 2. Install system dependencies (Linux) +sudo apt-get install python3-tk python3-pil python3-pil.imagetk \ + libqt5gui5 python3-pyqt5 python3-venv gfortran +sudo apt-get install libxcb-randr0-dev libxcb-xtest0-dev \ + libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev +# macOS equivalent +# brew install gcc python-tk -## for any questions or for help troubleshooting the install of PyLap please email Devin.diehl@scranton.edu ## +# 3. Create virtual environment +cd /path/to/PyLap +python3 -m venv venv +source venv/bin/activate + +# 4. Install Python packages +pip install --upgrade pip +pip install -r requirements.txt + +# 5. Build PyLap +python3 setup.py install +``` + +## Re-running Setup + +To redo the setup: + +1. Edit `~/.bashrc` and remove PyLap environment variables +2. Delete the venv: `rm -rf PyLap/venv` +3. Run `setup.sh` again + +## Contact + +For questions or help troubleshooting, email: Devin.diehl@scranton.edu diff --git a/setup.sh b/setup.sh index aa356ac..602bd89 100644 --- a/setup.sh +++ b/setup.sh @@ -1,17 +1,32 @@ +#!/bin/bash +# PyLap Setup Script +# Uses a Python virtual environment for package management -# check if the program is already setup +set -e -if [[ ${PHARLAP_HOME} = "" ]]; then +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_DIR="${SCRIPT_DIR}/venv" +# check if the program is already setup +if [[ ${PHARLAP_HOME} = "" ]]; then echo "First Time Setup" -echo "Please enter the filepath of the project" -echo "Example - /home/devindiehl/Pylap_project" +echo "Please enter the filepath of the project directory" +echo "Example - /home/username/pylap_project" read x echo ${x} -export PHARLAP_HOME="${x}/pharlap_4.5.1" -echo "export PHARLAP_HOME=${x}/pharlap_4.5.1" >> ~/.bashrc +# Detect PHaRLAP version (look for pharlap_* directory) +PHARLAP_DIR=$(find "${x}" -maxdepth 1 -type d -name "pharlap_*" 2>/dev/null | head -1) +if [[ -n "${PHARLAP_DIR}" ]]; then + export PHARLAP_HOME="${PHARLAP_DIR}" + echo "Found PHaRLAP at: ${PHARLAP_HOME}" +else + echo "Warning: PHaRLAP directory not found in ${x}" + echo "Please ensure PHaRLAP is installed before running raytracing examples" + export PHARLAP_HOME="${x}/pharlap_4.7.4" +fi +echo "export PHARLAP_HOME=${PHARLAP_HOME}" >> ~/.bashrc if [[ -d "${x}/PyLap" ]] ; then @@ -27,37 +42,49 @@ else echo "Pylap filepath not found" fi -export LD_LIBRARY="${x}/l_comp_lib_2020.4.304_comp.for_redist/compilers_and_libraries_2020.4.304/linux/compiler/lib/intel64_lin" -echo "export LD_LIBRARY=${x}/l_comp_lib_2020.4.304_comp.for_redist/compilers_and_libraries_2020.4.304/linux/compiler/lib/intel64_lin" >> ~/.bashrc - -export DIR_MODELS_REF_DAT="${x}/pharlap_4.5.1/dat" -echo "export DIR_MODELS_REF_DAT=${x}/pharlap_4.5.1/dat">> ~/.bashrc - -source /home/$USER/bin/compilervars.sh intel64 -echo "source /home/$USER/bin/compilervars.sh intel64" >> ~/.bashrc - - -# echo "python3 setup.py install --user" >> ~/.bashrc - -# export PHARLAP_HOME="/home/devindiehl/Pylap_project/PHARLAP/pharlap_4.5.1" - -# export PYTHONPATH="/home/devindiehl/Pylap_project/PyLap" - -# export LD_LIBRARY="/home/devindiehl/Pylap_project/l_comp_lib_2020.4.304_comp.for_redist/compilers_and_libraries_2020.4.304/linux/compiler/lib/intel64_lin" - -# export DIR_MODELS_REF_DAT="/home/devindiehl/Pylap_project/PHARLAP/pharlap_4.5.1/dat" - - -sudo apt-get install python3-tk python3-pil python3-pil.imagetk libqt5gui5 python3-pyqt5 - -sudo apt-get install libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev - -sudo apt-get install python3-pip - -pip3 install matplotlib numpy scipy qtpy +export DIR_MODELS_REF_DAT="${PHARLAP_HOME}/dat" +echo "export DIR_MODELS_REF_DAT=${DIR_MODELS_REF_DAT}" >> ~/.bashrc + +# Install system dependencies (gfortran provides the Fortran runtime — Intel Fortran is no longer required) +echo "Installing system dependencies..." +sudo apt-get update +sudo apt-get install -y python3-tk python3-pil python3-pil.imagetk libqt5gui5 python3-pyqt5 +sudo apt-get install -y libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev +sudo apt-get install -y python3-pip python3-venv gfortran + +# Create and activate virtual environment +echo "Creating Python virtual environment at ${VENV_DIR}..." +python3 -m venv "${VENV_DIR}" +source "${VENV_DIR}/bin/activate" + +# Install Python dependencies in venv +echo "Installing Python packages in virtual environment..." +pip install --upgrade pip +pip install -r "${SCRIPT_DIR}/requirements.txt" + +# Install IRI-2020 from GitHub (requires gfortran) +echo "Installing IRI-2020 from GitHub..." +pip install git+https://github.com/space-physics/iri2020.git + +# Build and install PyLap +echo "Building PyLap..." +python3 setup.py install + +# Add venv activation to bashrc +echo "# PyLap virtual environment" >> ~/.bashrc +echo "alias pylap-activate='source ${VENV_DIR}/bin/activate'" >> ~/.bashrc +echo "" +echo "Setup complete!" +echo "To activate the PyLap environment, run: source ${VENV_DIR}/bin/activate" +echo "Or use the alias: pylap-activate" -python3 setup.py install --user else echo "Pylap is already setup" -echo "if you wish to redo the setup enter the command 'nano .bashrc' in the home directory, delete the filepaths and then run the setup.sh script again" +echo "To activate the virtual environment: source ${SCRIPT_DIR}/venv/bin/activate" +echo "Or use the alias: pylap-activate" +echo "" +echo "If you wish to redo the setup:" +echo "1. Edit ~/.bashrc and remove the PyLap environment variables" +echo "2. Delete the venv directory: rm -rf ${SCRIPT_DIR}/venv" +echo "3. Run this script again" fi From a61ded200c1aea68ee6f7f553c27520087449adc Mon Sep 17 00:00:00 2001 From: mijahauan Date: Mon, 20 Apr 2026 13:14:11 +0000 Subject: [PATCH 9/9] feat: IRI-2020 profile support, example cleanups, requirements fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 'iri2020' profile type to gen_iono_grid_2d and gen_iono_grid_3d, backed by the space-physics/iri2020 package. Works without PHaRLAP, which lets collaborators verify the Python-side install before PHaRLAP access lands. Replace the string-of-ORs profile-type validation with a list membership check. Along the way fix a latent bug: two branches tested `profile_type.lower` (method reference — always truthy) instead of `profile_type.lower()`. iri2007 and iri2012 branches were therefore unreachable in the 2d path, and firi was unreachable in the 3d path. Add Examples/test_iri2020.py — smoke-test entry point referenced by the README for pre-PHaRLAP verification. Remove leftover `import ipdb` debug statements from five files (broke imports for anyone without ipdb installed). Fix requirements.txt: remove apt packages that never belonged in a pip file, fix typos (pthon3-phil, python3-phil.imagetk), add numpy and xarray (needed by iri2020), and add inline comments pointing at the apt/gfortran prerequisites. Co-Authored-By: Claude Opus 4.7 (1M context) --- Examples/SAMI3_ray_test1.py | 1 - Examples/run_raytraces.py | 1 - Examples/test_iri2020.py | 89 ++++++++++++++++++++++++++++ Ionosphere/gen_SAMI3_iono_grid_2d.py | 1 - Ionosphere/gen_iono_grid_2d.py | 61 +++++++++++++++---- Ionosphere/gen_iono_grid_3d.py | 71 ++++++++++++++++++---- Plotting/Plot_map.py | 1 - Plotting/plot_ray_iono_slice.py | 1 - requirements.txt | 18 +++--- 9 files changed, 211 insertions(+), 33 deletions(-) create mode 100644 Examples/test_iri2020.py diff --git a/Examples/SAMI3_ray_test1.py b/Examples/SAMI3_ray_test1.py index 15e89d2..3b2fca1 100644 --- a/Examples/SAMI3_ray_test1.py +++ b/Examples/SAMI3_ray_test1.py @@ -9,7 +9,6 @@ from Plotting import plot_ray_iono_slice as plot_iono import matplotlib.pyplot as plt import datetime as dt -import ipdb plt.switch_backend('tkagg') # Constants diff --git a/Examples/run_raytraces.py b/Examples/run_raytraces.py index 55a3f89..709b60b 100644 --- a/Examples/run_raytraces.py +++ b/Examples/run_raytraces.py @@ -11,7 +11,6 @@ import matplotlib.pyplot as plt import datetime as dt import pandas as pnd -import ipdb import os from geographiclib.geodesic import Geodesic geod = Geodesic.WGS84 diff --git a/Examples/test_iri2020.py b/Examples/test_iri2020.py new file mode 100644 index 0000000..bfa6324 --- /dev/null +++ b/Examples/test_iri2020.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +""" +Test IRI-2020 ionosphere model (works without PHaRLAP) + +This script demonstrates the IRI-2020 integration and can be run +before PHaRLAP is installed to verify the ionosphere model works. +""" + +import datetime +import numpy as np +import matplotlib.pyplot as plt + +# Import iri2020 package +import iri2020 + +def main(): + # Test parameters + lat = 40.0 # Latitude (degrees) + lon = -75.0 # Longitude (degrees) + dt = datetime.datetime(2024, 6, 21, 12, 0) # Summer solstice, noon UTC + + # Height range (km) - IRI2020 expects [min, max, step] + alt_min = 100 + alt_max = 600 + alt_step = 5 + heights = [alt_min, alt_max, alt_step] + + print(f"Running IRI-2020 for:") + print(f" Location: {lat}°N, {lon}°E") + print(f" Time: {dt}") + print(f" Heights: {alt_min} - {alt_max} km (step: {alt_step} km)") + print() + + # Call IRI-2020 + print("Calling IRI-2020...") + result = iri2020.IRI(dt, heights, lat, lon) + + # Reconstruct height array for plotting + height_arr = np.arange(alt_min, alt_max + alt_step, alt_step) + + # Extract electron density + ne = result['ne'].values # electrons/m^3 + + # Find F2 peak + f2_idx = np.nanargmax(ne) + f2_height = height_arr[f2_idx] + f2_density = ne[f2_idx] + + # Calculate plasma frequency at F2 peak + # fp = sqrt(ne * e^2 / (4 * pi^2 * epsilon_0 * m_e)) + # Simplified: fp (MHz) = 9e-6 * sqrt(ne) + fp_f2 = 9e-6 * np.sqrt(f2_density) + + print(f"Results:") + print(f" F2 peak height: {f2_height:.1f} km") + print(f" F2 peak density: {f2_density:.2e} electrons/m³") + print(f" F2 critical frequency (foF2): {fp_f2:.2f} MHz") + print() + + # Plot + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6)) + + # Electron density profile + ax1.plot(ne / 1e11, height_arr, 'b-', linewidth=2) + ax1.axhline(y=f2_height, color='r', linestyle='--', label=f'F2 peak: {f2_height:.0f} km') + ax1.set_xlabel('Electron Density (×10¹¹ m⁻³)') + ax1.set_ylabel('Height (km)') + ax1.set_title(f'IRI-2020 Electron Density Profile\n{lat}°N, {lon}°E, {dt}') + ax1.legend() + ax1.grid(True, alpha=0.3) + + # Temperature profiles + Te = result['Te'].values + Ti = result['Ti'].values + ax2.plot(Te, height_arr, 'r-', linewidth=2, label='Electron Temp (Te)') + ax2.plot(Ti, height_arr, 'b-', linewidth=2, label='Ion Temp (Ti)') + ax2.set_xlabel('Temperature (K)') + ax2.set_ylabel('Height (km)') + ax2.set_title('IRI-2020 Temperature Profiles') + ax2.legend() + ax2.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('iri2020_test_output.png', dpi=150) + print("Plot saved to: iri2020_test_output.png") + plt.show() + +if __name__ == '__main__': + main() diff --git a/Ionosphere/gen_SAMI3_iono_grid_2d.py b/Ionosphere/gen_SAMI3_iono_grid_2d.py index 18531ab..ab1fa7c 100644 --- a/Ionosphere/gen_SAMI3_iono_grid_2d.py +++ b/Ionosphere/gen_SAMI3_iono_grid_2d.py @@ -1,7 +1,6 @@ import netCDF4 as nc import scipy as sp import numpy as np -import ipdb import datetime as dt import pandas as pd import tqdm diff --git a/Ionosphere/gen_iono_grid_2d.py b/Ionosphere/gen_iono_grid_2d.py index 1924520..304e9ef 100644 --- a/Ionosphere/gen_iono_grid_2d.py +++ b/Ionosphere/gen_iono_grid_2d.py @@ -167,6 +167,8 @@ from pylap.iri2016 import iri2016 from pylap.iri2012 import iri2012 from pylap.iri2007 import iri2007 +import iri2020 as iri2020_pkg +import datetime # #function [iono_pf_grid,iono_pf_grid_5,collision_freq,irreg,iono_te_grid] = ... # gen_iono_grid_2d(origin_lat, origin_lon, R12, UT, azim, ... @@ -194,14 +196,11 @@ def gen_iono_grid_2d(origin_lat, origin_lon, R12, UT, azim, # defalut value - if profile_type.lower() != 'chapman_fllhc' and \ - profile_type.lower() != 'chapman' and \ - profile_type.lower() != 'iri' and \ - profile_type.lower() != 'iri2007' and \ - profile_type.lower() != 'iri2012' and \ - profile_type.lower() != 'iri2016' and \ - profile_type.lower() != 'firi': - print('invalid profile type') + valid_profile_types = ['chapman_fllhc', 'chapman', 'iri', 'iri2007', + 'iri2012', 'iri2016', 'iri2020', 'firi'] + if profile_type.lower() not in valid_profile_types: + print('invalid profile type: {}. Valid types: {}'.format( + profile_type, valid_profile_types)) sys.exit('gen_iono_grid_2d') #* fllhc_flag = 0 @@ -380,7 +379,7 @@ def gen_iono_profile(lat, lon, num_heights, start_height, height_inc, #M%%%%%%%%%%%%%%%%% #MThis is IRI2012 % #M%%%%%%%%%%%%%%%%% - elif profile_type.lower == 'iri2012': + elif profile_type.lower() == 'iri2012': # #M call IRI 2012 iono, iono_extra = iri2012.iri2012(lat, lon, R12, UT, start_height, @@ -417,7 +416,7 @@ def gen_iono_profile(lat, lon, num_heights, start_height, height_inc, #M%%%%%%%%%%%%%%%%% #MThis is IRI2007 % #M%%%%%%%%%%%%%%%%% - elif profile_type.lower == 'iri2007': + elif profile_type.lower() == 'iri2007': #M IRI2007 only returns 100 values for electron density with height - so #M determine the number of multiple calls required. max_iri_numhts = 100 @@ -471,6 +470,48 @@ def gen_iono_profile(lat, lon, num_heights, start_height, height_inc, iono_ti_prof[idx] = ion_temp iono_ti_prof[iono_ti_prof == -1] = np.nan iono_te_prof[iono_te_prof == -1] = np.nan + + #M%%%%%%%%%%%%%%%%% + #MThis is IRI2020 % + #M%%%%%%%%%%%%%%%%% + elif profile_type.lower() == 'iri2020': + # Use the iri2020 Python package (space-physics/iri2020) + # Convert UT array [year, month, day, hour, minute] to datetime + dt_time = datetime.datetime(UT[0], UT[1], UT[2], UT[3], UT[4]) + + # Generate height array + height_arr = np.arange(num_heights) * height_inc + start_height + + # Call IRI2020 - returns xarray Dataset + iri_result = iri2020_pkg.IRI(dt_time, height_arr, lat, lon) + + # Extract electron density (m^-3) + elec_dens = iri_result['ne'].values + elec_dens[np.isnan(elec_dens)] = 0 + elec_dens[elec_dens < 0] = 0 + + # Extract temperatures + iono_te_prof = iri_result['Te'].values + iono_ti_prof = iri_result['Ti'].values + iono_te_prof[iono_te_prof < 0] = np.nan + iono_ti_prof[iono_ti_prof < 0] = np.nan + + # Calculate plasma frequency (MHz) + iono_pf_prof = np.sqrt(pfsq_conv * elec_dens) + + if doppler_flag: + # UT 5 minutes later + dt_time_5 = dt_time + datetime.timedelta(minutes=5) + iri_result_5 = iri2020_pkg.IRI(dt_time_5, height_arr, lat, lon) + elec_dens5 = iri_result_5['ne'].values + elec_dens5[np.isnan(elec_dens5)] = 0 + elec_dens5[elec_dens5 < 0] = 0 + iono_pf_prof5 = np.sqrt(pfsq_conv * elec_dens5) + else: + iono_pf_prof5 = iono_pf_prof.copy() + + # iono_extra placeholder - IRI2020 package returns different structure + iono_extra = None print('leave gen_iono_profile') diff --git a/Ionosphere/gen_iono_grid_3d.py b/Ionosphere/gen_iono_grid_3d.py index 95d73ad..fc30604 100644 --- a/Ionosphere/gen_iono_grid_3d.py +++ b/Ionosphere/gen_iono_grid_3d.py @@ -154,6 +154,8 @@ from pylap.iri2016 import iri2016 from pylap.iri2012 import iri2012 from pylap.iri2007 import iri2007 +import iri2020 as iri2020_pkg +import datetime def gen_iono_grid_3d(UT, R12, iono_grid_parms, geomag_grid_parms, doppler_flag, @@ -167,15 +169,12 @@ def gen_iono_grid_3d(UT, R12, iono_grid_parms, else: iri_options = {} - if profile_type.lower() != 'chapman_fllhc' and \ - profile_type.lower() != 'chapman' and \ - profile_type.lower() != 'iri' and \ - profile_type.lower() != 'iri2007' and \ - profile_type.lower() != 'iri2012' and \ - profile_type.lower() != 'iri2016' and \ - profile_type.lower() != 'firi': - print('invalid profile type') - sys.exit('gen_iono_grid_2d') + valid_profile_types = ['chapman_fllhc', 'chapman', 'iri', 'iri2007', + 'iri2012', 'iri2016', 'iri2020', 'firi'] + if profile_type.lower() not in valid_profile_types: + print('invalid profile type: {}. Valid types: {}'.format( + profile_type, valid_profile_types)) + sys.exit('gen_iono_grid_3d') #* fllhc_flag = 0 if profile_type.lower() == 'chapman_fllhc': @@ -365,7 +364,7 @@ def gen_iono_subgrid(lat, lon_min, lon_inc, lon_max, ht_min, ht_inc, # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # % This is IRI2016 with FIRI option on % # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - elif profile_type.lower == 'firi': + elif profile_type.lower() == 'firi': num_heights = round((ht_max - ht_min)/ht_inc) + 1 @@ -520,5 +519,57 @@ def gen_iono_subgrid(lat, lon_min, lon_inc, lon_max, ht_min, ht_inc, collision_freq_subgrid[lon_idx,:] = \ eff_coll_freq.eff_coll_freq(T_e, T_ion, elec_dens, neutral_dens) +# %%%%%%%%%%%%%%%%%%% +# % This is IRI2020 % +# %%%%%%%%%%%%%%%%%%% + elif profile_type.lower() == 'iri2020': + # Use the iri2020 Python package (space-physics/iri2020) + num_heights = round((ht_max - ht_min)/ht_inc) + 1 + height_arr = np.arange(num_heights) * ht_inc + ht_min + + # Convert UT array [year, month, day, hour, minute] to datetime + dt_time = datetime.datetime(UT[0], UT[1], UT[2], UT[3], UT[4]) + + # Call IRI2020 - returns xarray Dataset + iri_result = iri2020_pkg.IRI(dt_time, height_arr, lat, lon) + + # Extract electron density (m^-3) + elec_dens = iri_result['ne'].values + elec_dens[np.isnan(elec_dens)] = 0 + elec_dens[elec_dens < 0] = 0 + + # Calculate plasma frequency (MHz) + iono_pf_subgrid[lon_idx] = np.sqrt(elec_dens * pfsq_conv) + + if doppler_flag: + dt_time_5 = dt_time + datetime.timedelta(minutes=5) + iri_result_5 = iri2020_pkg.IRI(dt_time_5, height_arr, lat, lon) + elec_dens5 = iri_result_5['ne'].values + elec_dens5[np.isnan(elec_dens5)] = 0 + elec_dens5[elec_dens5 < 0] = 0 + iono_pf_subgrid_5[lon_idx] = np.sqrt(elec_dens5 * pfsq_conv) + else: + iono_pf_subgrid_5[lon_idx] = np.sqrt(elec_dens * pfsq_conv) + + # Extract temperatures + T_e = iri_result['Te'].values + T_e[T_e < 0] = np.nan + + T_ion = iri_result['Ti'].values + T_ion[T_ion < 0] = np.nan + + # Neutral densities + lat_arr = lat * np.ones_like(height_arr) + lon_arr = lon * np.ones_like(height_arr) + if R12 == -1: + [neutral_dens, temp] = nrlmsise00(lat_arr, lon_arr, height_arr, UT) + else: + f107 = 63.75 + R12*(0.728 + R12*0.00089) + [neutral_dens, temp] = nrlmsise00(lat_arr, lon_arr, height_arr, UT, f107, f107, 4) + + # Calculate collision frequency + collision_freq_subgrid[lon_idx,:] = \ + eff_coll_freq.eff_coll_freq(T_e, T_ion, elec_dens, neutral_dens) + return iono_pf_subgrid, iono_pf_subgrid_5, collision_freq_subgrid diff --git a/Plotting/Plot_map.py b/Plotting/Plot_map.py index 36240ae..0da9ace 100644 --- a/Plotting/Plot_map.py +++ b/Plotting/Plot_map.py @@ -7,7 +7,6 @@ import os import netCDF4 as nc import datetime as dt -import ipdb def calculate_scale(data,stddevs=2.,lim='auto'): diff --git a/Plotting/plot_ray_iono_slice.py b/Plotting/plot_ray_iono_slice.py index be5fb6d..546f089 100644 --- a/Plotting/plot_ray_iono_slice.py +++ b/Plotting/plot_ray_iono_slice.py @@ -74,7 +74,6 @@ import sys import platform from qtpy.QtWidgets import QApplication -import ipdb def plot_ray_iono_slice(iono_grid, start_range, end_range, range_inc, diff --git a/requirements.txt b/requirements.txt index 56735a8..207c245 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,14 @@ +# Python packages (install with pip) scipy matplotlib -python3-tk -pthon3-phil -python3-phil.imagetk +numpy qtpy PyQt5 -libxcb-randr0-dev -libxcb-xtest0-dev -libxcb-xinerama0-dev -libxcb-shape0-dev -libxcb-xkb-dev \ No newline at end of file +xarray + +# IRI-2020 must be installed from GitHub (requires gfortran): +# pip install git+https://github.com/space-physics/iri2020.git + +# System packages (install with apt-get, not pip): +# sudo apt-get install python3-tk python3-pil python3-pil.imagetk gfortran +# sudo apt-get install libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev \ No newline at end of file