Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/build-wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Build Wheels

on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
workflow_dispatch:

jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-13, macos-14]

steps:
- uses: actions/checkout@v4

- name: Set up MSYS2 (Windows only)
if: runner.os == 'Windows'
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
mingw-w64-x86_64-gcc-fortran
mingw-w64-x86_64-meson
mingw-w64-x86_64-ninja

- name: Install Fortran (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y gfortran

- name: Install Fortran (macOS)
if: runner.os == 'macOS'
run: |
brew install gcc

- name: Build wheels
uses: pypa/cibuildwheel@v2.17.0
env:
CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*
CIBW_SKIP: "*-musllinux_* pp*"
# Windows: Copy runtime DLLs to libs/ before build
CIBW_BEFORE_BUILD_WINDOWS: |
python -c "from pathlib import Path; (Path('src/lowtran/libs')).mkdir(parents=True, exist_ok=True)"
if exist C:\msys64\mingw64\bin\libgfortran-5.dll (
copy C:\msys64\mingw64\bin\libgfortran-5.dll src\lowtran\libs\
copy C:\msys64\mingw64\bin\libgcc_s_seh-1.dll src\lowtran\libs\
copy C:\msys64\mingw64\bin\libquadmath-0.dll src\lowtran\libs\
copy C:\msys64\mingw64\bin\libwinpthread-1.dll src\lowtran\libs\
)
# Ensure meson and ninja are available
CIBW_BEFORE_BUILD: pip install meson-python ninja
# Test that the wheel works
CIBW_TEST_REQUIRES: pytest numpy
CIBW_TEST_COMMAND: pytest {project}/tests || python -c "import lowtran; lowtran.check()"

- uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl

build_sdist:
name: Build source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build sdist
run: pipx run build --sdist

- uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
.mypy_cache/
.pytest_cache/
.cache/
.claude/
.vscode/


build/
dist/

__pycache__/
*.py[cod]
*.so
*.dylib
*.egg-info/

# Runtime DLLs (bundled during wheel build)
src/lowtran/libs/*.dll
46 changes: 0 additions & 46 deletions CMakeLists.txt

This file was deleted.

114 changes: 114 additions & 0 deletions example/LowTran.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "59875f03-1b3d-4e3c-8193-1c88c8d4b013",
"metadata": {},
"outputs": [],
"source": [
"import lowtran\n",
"from lowtran.plot import horiz\n",
"import pylab as pb\n",
"m = 1.0\n",
"km = 1000*m\n",
"deg = 1.0\n",
"nm = 1e-9*m\n",
"μm = 1e-6*m\n",
"cm = 1e-2*m"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "bdbf8548-0883-492b-9985-d98849cc656f",
"metadata": {
"editable": true,
"slideshow": {
"slide_type": ""
},
"tags": []
},
"outputs": [
{
"ename": "ImportError",
"evalue": "DLL load failed while importing lowtran7: The specified module could not be found.",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mImportError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 17\u001b[39m\n\u001b[32m 6\u001b[39m wlstep = \u001b[32m20.0\u001b[39m/cm\n\u001b[32m 8\u001b[39m c1 = {\n\u001b[32m 9\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mzmdl\u001b[39m\u001b[33m\"\u001b[39m: observer_altitude/km,\n\u001b[32m 10\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mh1\u001b[39m\u001b[33m\"\u001b[39m: observer_altitude/km,\n\u001b[32m (...)\u001b[39m\u001b[32m 14\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mwlstep\u001b[39m\u001b[33m\"\u001b[39m: wlstep/(\u001b[32m1.0\u001b[39m/cm),\n\u001b[32m 15\u001b[39m }\n\u001b[32m---> \u001b[39m\u001b[32m17\u001b[39m T_atm = \u001b[43mlowtran\u001b[49m\u001b[43m.\u001b[49m\u001b[43mhoriztrans\u001b[49m\u001b[43m(\u001b[49m\u001b[43mc1\u001b[49m\u001b[43m)\u001b[49m.squeeze()\n\u001b[32m 18\u001b[39m horiz(T_atm,c1)\n\u001b[32m 19\u001b[39m \u001b[38;5;66;03m#pb.show()\u001b[39;00m\n",
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\CAFFOUDA\\.local\\venv\\sandbox\\Lib\\site-packages\\lowtran\\scenarios.py:113\u001b[39m, in \u001b[36mhoriztrans\u001b[39m\u001b[34m(c1)\u001b[39m\n\u001b[32m 101\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mhoriztrans\u001b[39m(c1: \u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any]):\n\u001b[32m 103\u001b[39m c1.update(\n\u001b[32m 104\u001b[39m {\n\u001b[32m 105\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mmodel\u001b[39m\u001b[33m\"\u001b[39m: \u001b[32m5\u001b[39m, \u001b[38;5;66;03m# 5: subartic winter\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 110\u001b[39m }\n\u001b[32m 111\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m113\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mgolowtran\u001b[49m\u001b[43m(\u001b[49m\u001b[43mc1\u001b[49m\u001b[43m)\u001b[49m\n",
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\CAFFOUDA\\.local\\venv\\sandbox\\Lib\\site-packages\\lowtran\\base.py:147\u001b[39m, in \u001b[36mgolowtran\u001b[39m\u001b[34m(c1)\u001b[39m\n\u001b[32m 141\u001b[39m \u001b[38;5;66;03m# %% invoke lowtran\u001b[39;00m\n\u001b[32m 142\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 143\u001b[39m \u001b[33;03mNote we invoke case \"3a\" from table 14, only observer altitude and apparent\u001b[39;00m\n\u001b[32m 144\u001b[39m \u001b[33;03mangle are specified\u001b[39;00m\n\u001b[32m 145\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m147\u001b[39m lowtran7 = \u001b[43mcheck\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 149\u001b[39m Tx, V, Alam, trace, unif, suma, irrad, sumvv = lowtran7.lwtrn7(\n\u001b[32m 150\u001b[39m \u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[32m 151\u001b[39m nwl,\n\u001b[32m (...)\u001b[39m\u001b[32m 168\u001b[39m c1[\u001b[33m\"\u001b[39m\u001b[33mrange_km\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m 169\u001b[39m )\n\u001b[32m 171\u001b[39m dims = (\u001b[33m\"\u001b[39m\u001b[33mtime\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mwavelength_nm\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mangle_deg\u001b[39m\u001b[33m\"\u001b[39m)\n",
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\CAFFOUDA\\.local\\venv\\sandbox\\Lib\\site-packages\\lowtran\\base.py:18\u001b[39m, in \u001b[36mcheck\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m 13\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcheck\u001b[39m() -> ModuleType:\n\u001b[32m 14\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Import the lowtran7 extension module.\u001b[39;00m\n\u001b[32m 15\u001b[39m \n\u001b[32m 16\u001b[39m \u001b[33;03m The extension should be built with Meson and installed alongside this package.\u001b[39;00m\n\u001b[32m 17\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m18\u001b[39m lowtran7 = \u001b[43mimport_f2py_mod\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mlowtran7\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 19\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m lowtran7\n",
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\CAFFOUDA\\.local\\venv\\sandbox\\Lib\\site-packages\\lowtran\\base.py:42\u001b[39m, in \u001b[36mimport_f2py_mod\u001b[39m\u001b[34m(name)\u001b[39m\n\u001b[32m 40\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m spec \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 41\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mModuleNotFoundError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m not found in \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmod_file\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m---> \u001b[39m\u001b[32m42\u001b[39m mod = \u001b[43mimportlib\u001b[49m\u001b[43m.\u001b[49m\u001b[43mutil\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmodule_from_spec\u001b[49m\u001b[43m(\u001b[49m\u001b[43mspec\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 43\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m mod \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 44\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mImportError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mcould not import \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m from \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmod_file\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
"\u001b[36mFile \u001b[39m\u001b[32m<frozen importlib._bootstrap>:816\u001b[39m, in \u001b[36mmodule_from_spec\u001b[39m\u001b[34m(spec)\u001b[39m\n\u001b[32m 812\u001b[39m module = \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m 813\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(spec.loader, \u001b[33m'\u001b[39m\u001b[33mcreate_module\u001b[39m\u001b[33m'\u001b[39m):\n\u001b[32m 814\u001b[39m \u001b[38;5;66;03m# If create_module() returns `None` then it means default\u001b[39;00m\n\u001b[32m 815\u001b[39m \u001b[38;5;66;03m# module creation should be used.\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m816\u001b[39m module = \u001b[43mspec\u001b[49m\u001b[43m.\u001b[49m\u001b[43mloader\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate_module\u001b[49m\u001b[43m(\u001b[49m\u001b[43mspec\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 817\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(spec.loader, \u001b[33m'\u001b[39m\u001b[33mexec_module\u001b[39m\u001b[33m'\u001b[39m):\n\u001b[32m 818\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mImportError\u001b[39;00m(\u001b[33m'\u001b[39m\u001b[33mloaders that define exec_module() \u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m 819\u001b[39m \u001b[33m'\u001b[39m\u001b[33mmust also define create_module()\u001b[39m\u001b[33m'\u001b[39m)\n",
"\u001b[36mFile \u001b[39m\u001b[32m<frozen importlib._bootstrap_external>:1056\u001b[39m, in \u001b[36mExtensionFileLoader.create_module\u001b[39m\u001b[34m(self, spec)\u001b[39m\n\u001b[32m 1054\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcreate_module\u001b[39m(\u001b[38;5;28mself\u001b[39m, spec):\n\u001b[32m 1055\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Create an uninitialized extension module\"\"\"\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1056\u001b[39m module = \u001b[43m_bootstrap\u001b[49m\u001b[43m.\u001b[49m\u001b[43m_call_with_frames_removed\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 1057\u001b[39m \u001b[43m \u001b[49m\u001b[43m_imp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate_dynamic\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mspec\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1058\u001b[39m _bootstrap._verbose_message(\u001b[33m'\u001b[39m\u001b[33mextension module \u001b[39m\u001b[38;5;132;01m{!r}\u001b[39;00m\u001b[33m loaded from \u001b[39m\u001b[38;5;132;01m{!r}\u001b[39;00m\u001b[33m'\u001b[39m,\n\u001b[32m 1059\u001b[39m spec.name, \u001b[38;5;28mself\u001b[39m.path)\n\u001b[32m 1060\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m module\n",
"\u001b[36mFile \u001b[39m\u001b[32m<frozen importlib._bootstrap>:491\u001b[39m, in \u001b[36m_call_with_frames_removed\u001b[39m\u001b[34m(f, *args, **kwds)\u001b[39m\n\u001b[32m 483\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_call_with_frames_removed\u001b[39m(f, *args, **kwds):\n\u001b[32m 484\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"remove_importlib_frames in import.c will always remove sequences\u001b[39;00m\n\u001b[32m 485\u001b[39m \u001b[33;03m of importlib frames that end with a call to this function\u001b[39;00m\n\u001b[32m 486\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 489\u001b[39m \u001b[33;03m module code)\u001b[39;00m\n\u001b[32m 490\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m491\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mf\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n",
"\u001b[31mImportError\u001b[39m: DLL load failed while importing lowtran7: The specified module could not be found."
]
}
],
"source": [
"observer_altitude = 10*km\n",
"target_range = 100*km\n",
"zenith_angle = 40.0*deg\n",
"wlshort = 100*nm\n",
"wllong = 20*μm\n",
"wlstep = 20.0/cm\n",
"\n",
"c1 = {\n",
" \"zmdl\": observer_altitude/km,\n",
" \"h1\": observer_altitude/km,\n",
" \"range_km\": target_range/km,\n",
" \"wlshort\": wlshort/nm,\n",
" \"wllong\": wllong/nm,\n",
" \"wlstep\": wlstep/(1.0/cm),\n",
" }\n",
"\n",
"T_atm = lowtran.horiztrans(c1).squeeze()\n",
"horiz(T_atm,c1)\n",
"#pb.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "57e96fa4-707a-43b3-ba1b-650bdd542153",
"metadata": {},
"outputs": [],
"source": [
"T_atm['wavelength_nm'].to_numpy()[::-1]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fee86c38-0985-4d72-a131-8c30d100904a",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "sandbox",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
68 changes: 68 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
project('lowtran',
['c', 'fortran'],
version : '0.1',
meson_version: '>= 1.1.0',
default_options : [
'warning_level=1',
'buildtype=release',
#'fortran_args=-Wno-line-truncation -Wno-conversion -Wno-unused-variable -Wno-maybe-uninitialized -Wno-unused-dummy-argument -Wno-compare-reals',
'fortran_std=legacy',
],
)

py_mod = import('python')
py = py_mod.find_installation()
py_dep = py.dependency()
# Get the system-specific include directory

py_include_dir = run_command(py,
['-c', 'import sysconfig; print(sysconfig.get_path("include"))'],
check : true
).stdout().strip()
message('Python include dir', py_include_dir)

py_install_dir = run_command(py,
['-c', 'import sysconfig; print(sysconfig.get_path("purelib"))'],
check : true
).stdout().strip()

message('Meson will install app in', py_install_dir)

subdir('src/lowtran/fortran')

py.install_sources(
[
'src/lowtran/__init__.py', 'src/lowtran/base.py', 'src/lowtran/plot.py', 'src/lowtran/scenarios.py',
],
pure : false,
subdir : 'lowtran',
# install_dir: py_install_dir
)

# Install bundled DLLs for Windows (Fortran runtime dependencies)
# DLLs are copied during wheel build process (see .github/workflows/build-wheels.yml)
# For local development, DLLs are not required if mingw64 is in PATH
if host_machine.system() == 'windows'
dll_files = [
'src/lowtran/libs/libgfortran-5.dll',
'src/lowtran/libs/libgcc_s_seh-1.dll',
'src/lowtran/libs/libquadmath-0.dll',
'src/lowtran/libs/libwinpthread-1.dll',
]

# Only install DLLs if they exist (e.g., during wheel build)
dll_list = []
foreach dll : dll_files
if import('fs').exists(dll)
dll_list += [dll]
endif
endforeach

if dll_list.length() > 0
py.install_sources(
dll_list,
pure : false,
subdir : 'lowtran/libs'
)
endif
endif
19 changes: 12 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build-system]
requires = ["setuptools>=61.0.0", "wheel", "numpy"]
build-backend = "setuptools.build_meta"
requires = ["meson-python>=0.16.0", "numpy"]
build-backend = "mesonpy"

[project]
name = "lowtran"
Expand All @@ -16,21 +16,26 @@ classifiers = [
"Programming Language :: Fortran",
"Topic :: Scientific/Engineering :: Atmospheric Science"
]
dynamic = ["readme"]
readme = {"file" = "README.md", "content-type" = "text/markdown"}

maintainers = [
{ name = "Chaffra Affouda", email = "caffouda@anduril.com" },
]

requires-python = ">=3.9"
dependencies = ["numpy", "xarray", "python-dateutil"]
dependencies = ["numpy", "python-dateutil", "pandas"]

[project.optional-dependencies]
tests = ["pytest"]
test = ["pytest"]
lint = ["flake8", "flake8-bugbear", "flake8-builtins", "flake8-blind-except", "mypy", "types-python-dateutil"]

[tool.setuptools.dynamic]
readme = {file = ["README.md"], content-type = "text/markdown"}

[tool.black]
line-length = 100

[tool.mypy]
files = ["src", "example"]
allow_redefinition = true
show_error_context = false
show_column_numbers = true
ignore_missing_imports = true
Loading