diff --git a/.github/workflows/cff-validator.yml b/.github/workflows/cff-validator.yml new file mode 100644 index 0000000..6fce818 --- /dev/null +++ b/.github/workflows/cff-validator.yml @@ -0,0 +1,27 @@ +name: Validate CITATION.cff + +on: + push: + paths: + - 'CITATION.cff' + - '.github/workflows/cff-validator.yml' + pull_request: + paths: + - 'CITATION.cff' + - '.github/workflows/cff-validator.yml' + workflow_dispatch: + +jobs: + Validate-CITATION-cff: + runs-on: ubuntu-latest + name: Validate CITATION.cff + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Validate CITATION.cff + uses: dieghernan/cff-validator@v3 + with: + install-r: true diff --git a/.gitignore b/.gitignore index 82f9275..ac9216b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,10 @@ __pycache__/ # C extensions *.so +*.so.* +*.dylib +*.DLL +*.dll # Distribution / packaging .Python diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c7ec2e1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,41 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-ast + types: [file, python] + - id: check-case-conflict + - id: check-docstring-first + types: [file, python] + - id: check-merge-conflict + - id: check-yaml + types: [file, yaml] + - id: debug-statements + types: [file, python] + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/asottile/pyupgrade + rev: v3.19.0 + hooks: + - id: pyupgrade + args: ["--py39-plus"] + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + types: [file, python] + args: ["--profile", "black", "--filter-files", "--gitignore"] + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + types: [file, python] + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.43.0 + hooks: + - id: markdownlint + types: [file, markdown] + exclude: GitHubRepoPublicReleaseApproval.md + args: ["--disable", "MD013"] diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 0000000..67993b1 --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,43 @@ +{ + "upload_type": "software", + "publication_date": "TODO-TEMPLATE", + "title": "Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python Wrapper", + "creators": [ + { + "name": "Heroy, Chen", + "affiliation": "U.S. Department of Commerce, National Telecommunications and Information Administration, Institute for Telecommunication Sciences", + "orcid": "0009-0006-8728-4502" + }, + { + "name": "Romaniello, Anthony W.", + "affiliation": "U.S. Department of Commerce, National Telecommunications and Information Administration, Institute for Telecommunication Sciences", + "orcid": "0000-0001-8437-6504" + } + ], + "description": "This code repository contains a Python wrapper for the NTIA/ITS implementation of the Low Frequency / Medium Frequency (LF/MF) Propagation Model.", + "access_right": "open", + "keywords": [ + "propagation", + "communications", + "antennas", + "radio wave propagation" + ], + "related_identifiers": [ + { + "identifier": "https://github.com/NTIA/LFMF", + "relation": "isSupplementTo", + "resource_type": "software" + }, + { + "identifier": "https://github.com/NTIA/LFMF-test-data", + "relation": "isSupplementedBy", + "resource_type": "dataset" + }, + { + "identifier": "https://ntia.github.io/propagation-library-wiki/models/LFMF/", + "relation": "isDocumentedBy", + "resource_type": "softwaredocumentation" + } + ], + "version": "1.1.0" +} diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..a4ce4c3 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,42 @@ +cff-version: 1.2.0 +title: >- + Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python Wrapper +message: Please cite this software using these metadata. +type: software +authors: + - family-names: Heroy + given-names: Chen + affiliation: >- + U.S. Department of Commerce, National + Telecommunications and Information Administration, + Institute for Telecommunication Sciences + orcid: 'https://orcid.org/0009-0006-8728-4502' + email: cheroy.ctr@ntia.gov + - family-names: Romaniello + given-names: Anthony W. + affiliation: >- + U.S. Department of Commerce, National + Telecommunications and Information Administration, + Institute for Telecommunication Sciences + orcid: 'https://orcid.org/0000-0001-8437-6504' + email: aromaniello@ntia.gov + - name: >- + U.S. Department of Commerce, National + Telecommunications and Information Administration, + Institute for Telecommunication Sciences + address: 325 Broadway + city: Boulder + country: US + post-code: '80305' + region: Colorado + alias: NTIA/ITS + email: code@ntia.gov + website: 'https://its.ntia.gov' +repository-code: 'https://github.com/NTIA/LFMF-python' +url: 'https://ntia.github.io/propagation-library-wiki/models/LFMF' +keywords: + - its + - propagation + - lfmf + - antennas +version: 1.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f4c5d11 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,350 @@ +# NTIA/ITS Propagation Library Contribution Guide + +Thank you for your interest in contributing to this open source software. On this +page you will get an overview of the contribution workflow from opening an issue, +creating a PR, reviewing, +and merging the PR. This page also includes some information about the project +structures, development workflows, and code styles which are used throughout the +ITS Propagation Library. + +If you are instead interested in usage documentation, please refer to the +[Propagation Library Wiki](https://ntia.github.io/propagation-library-wiki). + +## Contents + +- [Found a Bug?](#found-a-bug) +- [Background for New Contributors](#background-for-new-contributors) +- [Notes on Code Style](#notes-on-code-style) +- [Project Structure and CMake](#project-structure-and-cmake) +- [Documenting Code](#documenting-code) +- [Testing Code](#testing-code) + +## Found a Bug? + +If you spot a problem with this software, +[search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). +If a related issue doesn't exist, we encourage you to open one (even if you +don't plan to contribute a resolution yourself). Issues may be opened for bugs, +documentation errors, or feature requests. + +## Background for new contributors + +The workflow we recommend and describe here follows from best and common +practices in the Git and GitHub ecosystems. We aim to leverage this workflow, +especially the elements of code review and approval, to enable open source +development of robust, trustworthy radio propagation software. Here are some +resources to help you get started with open source contributions: + +- [Set up Git](https://docs.github.com/en/get-started/getting-started-with-git/set-up-git) +- [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow) +- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) +- [Basic explanation of Git submodules](https://gist.github.com/gitaarik/8735255) +by [**@gitaarik**](https://github.com/gitaarik) + +### Git Branches + +Our repositories use the following approach to organize and keep track of branches. +The `main` branch typically represents the most recently released version of the software. +The `dev` branch stages changes before they are merged into `main` and a new release is created. +New features or bug fixes should be developed on individual "feature branches" with descriptive names. +When complete, features branches should merge into `dev`. + +### Git Submodules + +Software in the ITS Propagation Library is implemented primarily in C++. Each piece +of software has a primary repository which contains the base C++ implementation, +test data and resources, and common files used by the multi-language wrappers. +Interfaces for additional programming languages are provided in separate repositories, +which are linked to the primary repository as [Git submodules](https://gist.github.com/gitaarik/8735255). +When cloning the primary repository, the submodules are not additionally cloned +by default. This can be done with the `git submodule init` command. Initializing +the submodule as part of the parent repository will let you use the build +configuration from the primary repository to compile the C++ source and place it +appropriately for use by the wrapper code. If you choose to independently clone +the wrapper repository, you will likely need to separately download the compiled +library (for example, a DLL from a GitHub release). + +### Contributing on GitHub + +If you'd like to solve an existing issue, add a new feature, or modify this software, +follow these steps when making your changes. + +1. Fork the repository. This allows you to make your changes without affecting the +original project until you're ready to merge them. You can create a fork +[with GitHub Desktop](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop) +or [using the command line](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) + +1. Create a working branch and start with your changes! Commit changes +incrementally to your fork. See the sections below for details about unit tests, +code style, and documentation. + +1. When you're done making changes, create a pull request (PR). In your PR, please include +a meaningful description of the changes you've made. If your PR solves an issue, +[link to it](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)! + +Once you submit your PR, a maintainer will review your changes to determine +whether or not they should be merged. We may ask questions or request additional +changes which must be addressed. For example, we may request changes so that the code +meets structure, formatting, accuracy, or testing requirements. + +If your PR is approved and merged, your changes will be a part of the `dev` +branch of the repository, where they will stay until a new release is made. At that +point, `dev` will merge into `main` and a new release will be created. The maintainers +of a repository hold the authority on when a new release should be created. For example, +important bug fixes may take higher priority, while small improvements may stay on `dev` +for a while. Rest assured, even if a new release is not immediately made, your approved +changes will be always packaged into the next release. + +## Notes on Code Style + +- In general, variables follow the naming convention in which a single underscore +denotes a subscript (pseudo-LaTeX format), where a double underscore is followed +by the units, i.e. `h_1__meter`. +- Variables are named to match their corresponding mathematical variables in the +underlying text, when applicable. +- Wherever possible, equation numbers are provided. It is assumed that a user +reviewing this source code would have a copy of the relevant text available +as a primary reference. +- _For base/C++ repositories_, a `.clang-format` file is included in the root directory. +Most IDEs support this type of file, which can and should be used to apply uniform +code styling to C++ source and header files. +- _For Python wrapper repositories_, a `.pre-commit-config.yaml` file is included +in the root directory. This file implements multiple hooks for the [pre-commit](https://pre-commit.com/) +tool, which apply automated formatting to files when they are committed to Git. +It is recommended to use this tool to autoformat Python code when checked in. + +## Project Structure and CMake + +Software in the ITS Propagation Library is primarily implemented in C++, then +wrapped with interfaces exposing the C++ library to users of other languages. The +primary repository for each software package uses [CMake](https://cmake.org/) to +handle cross-platform C++ build configuration, C++ unit tests (with +[GoogleTest](https://github.com/google/googletest) and +[CTest](https://cmake.org/cmake/help/latest/manual/ctest.1.html)), and generation of +API documentation (with [Doxygen](https://www.doxygen.nl/)). Many IDEs support CMake +integration in some form or fashion, and it is recommended that you familiarize yourself +with any such functionality of your chosen IDE. + +This section shows a typical project structure for a primary (i.e., non-wrapper) +repository. For details about wrapper repositories, refer to their own README files. + +```bash +app/ # The command-line driver which can run the library + data/ # Example input and output files for use with the driver + include/ # Headers used by the command-line driver + src/ # Source code for the command-line driver + tests/ # Header and source files for testing the command-line driver + CMakeLists.txt # Configuration for the command-line driver and its tests + README.md # Usage information for the command-line driver +docs/ + CMakeLists.txt # Doxygen configuration + ... # Static files (images, HTML, CS, Markdown) used by Doxygen +extern/ + ... # External Git submodules/dependencies +include/ + / # Include namespace folder, e.g. "ITS.Propagation.ITM" + .h # Library header files go here, e.g. "ITM.h" and "ErrorCodes.h" +src/ + .cpp # Source files go here, e.g. "LongleyRice.cpp" and "FreeSpaceLoss.cpp" + CMakeLists.txt # Configures cross-platform build +tests/ + data/ + .csv # Testing data goes here. Does not have to be CSV. + .cpp # Unit tests, usually one test file per source file. + .h # Any headers used by tests go here as well. + CMakeLists.txt # CTest+GTest config. Files containing tests must be included here. +wrap/ + dotnet/ # C#/.NET wrapper submodule. Should contain CMakeLists.txt + matlab/ # MATLAB wrapper submodule. Should contain CMakeLists.txt + python/ # Python wrapper submodule. Should contain CMakeLists.txt +CMakeLists.txt # Top-level CMakeLists.txt: project metadata and options +CMakePresets.json # Presets for CMake, e.g. "release", "debug", etc. +... +``` + +### CMake Options and CMake Presets + +As you can see, multiple `CMakeLists.txt` files exist within the project. Each +one contains configurations relevant to the directory where it is stored. For +example, the `tests/CMakeLists.txt` file configures unit tests using CMake. The +top-level `CMakeLists.txt` stores the primary project configuration and includes +the lower-level configurations based on the preset or specified CMake options. + +The following CMake options are used for top-level project configuration: + +| Option | Default | Definition | +|--------------------|---------|------------------------------------------| +| `BUILD_DOCS` | `ON` | Generate documentation site with Doxygen | +| `BUILD_DRIVER` | `ON` | Build the command-line driver executable | +| `RUN_DRIVER_TESTS` | `ON` | Test the command-line driver executable | +| `DOCS_ONLY` | `OFF` | Skip all steps _except_ generating the documentation site | +| `RUN_TESTS` | `ON` | Run unit tests for the main library | +| `COPY_TO_WRAPPERS` | `ON` | Copy the compiled shared library into wrapper submodules | + +[CMake Presets](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html) are +provided to support common build configurations. These are specified in the +`CMakePresets.json` file. The `release` preset will compile the library and driver +with optimizations, build the documentation site, and run all unit tests. The `debug` preset +will skip building the documentation site, driver, and driver tests, which can be useful for +rapid development and testing. Additionally, the Debug configuration will attempt to pass +debug flags to the compiler. Finally, the "docsOnly" preset skips all steps except for +generating the Doxygen documentation site. + +| Option | `release` preset | `debug` preset | `docsOnly` preset | +|--------------------|------------------|----------------|-------------------| +| `DOCS_ONLY` | `OFF` | `OFF` | `ON` | +| `RUN_TESTS` | `ON` | `ON` | `OFF` | +| `CMAKE_BUILD_TYPE` | `Release` | `Debug` | not set | +| `BUILD_DOCS` | `ON` | `OFF` | `ON` | +| `BUILD_DRIVER` | `ON` | `OFF` | `OFF` | +| `RUN_DRIVER_TESTS` | `ON` | `OFF` | `OFF` | + +Below are some examples of how CMake can be called to compile this software. + +```bash +# Configure and compile in release configuration +cmake --preset release +cmake --build --preset release + +# Use the release configuration but don't build Doxygen docs +cmake --preset release -DBUILD_DOCS=OFF +cmake --build --preset release + +# Configure and compile in debug configuration +cmake --preset debug +cmake --build --preset debug + +# Use the release configuration but don't run driver tests +cmake --preset release -DRUN_DRIVER_TESTS=OFF +cmake --build --preset release +``` + +### Supported Platforms and Build Options + +The provided `CMakeLists.txt` and `CMakePresets.json` files aim to be flexible +for development from the platform of your choosing. The approach taken is to make +few assumptions about your toolchain to implicitly enable cross-platform and +multi-environment development as much as possible. However, we cannot guarantee +that all compilers, tools, and platforms will work without requiring some additional +configuration which is not documented here. If you find an issue or would like to +see a change to support your chosen platform or tools, open an issue or create a +pull request! + +## Documenting Code + +### C++ Base Libraries + +The C++ source code is documented with Doxygen. A GitHub Action is configured to +build and deploy the documentation using GitHub Pages. This action will ensure +that any new code has been accompanied by Doxygen-formatted documentation. Code +will not be merged until and unless it is completely documented using Doxygen, +and the GitHub action successfully generates the documentation site. Below is an +example showing the expected documentation formats. Except for inline documentation, +use the JavaDoc banner style [described by Doxygen](https://www.doxygen.nl/manual/docblocks.html) + +```cpp +constexpr double = PI 3.1415; /**< Inline format, e.g. for constants */ + +/******************************************************************************* + * This is a brief description of the function. + * + * This is an optional, longer description of the function. It can include + * LaTeX formatting, for example: this function doubles its input @f$ x @f$ and + * returns a value @f$ y @f$ with @f$ y = 2x @f$. This whole documentation block + * is using the JavaDoc banner style! + * + * @param[in] x The input and its expected units + * @return The result @f$ y = 2x @f$ + ******************************************************************************/ +double doubleTheInput(double x) +{ + return 2 * x; +} +``` + +### Doxygen for C++ Libraries + +The base C++ libraries include Doxygen configurations which generate static +websites from code comments. These documentation sites are published as developer +reference documentation using GitHub Pages. When building the Doxygen site locally, +The site is generated in `docs/html/` and the main page can be accessed at `docs/html/index.html`. +When new releases are made, GitHub Actions workflows are triggered which build and deploy +the Doxygen site to GitHub Pages. + +### MATLAB Wrappers + +Most code in the MATLAB wrapper is actually written in C. In these files, the same +documentation style as noted above for C++ should be used. + +### Python Wrappers + +The Python wrapper code is documented in the [Sphinx](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html) +format. It is recommended to include docstrings for all primary functions, classes, +or structures provided by the Python wrapper. Further, function signatures should +include [type annotation](https://docs.python.org/3/library/typing.html) for inputs +and returned values. Inline or other comments should be included to explain other +variables or functionalities of the code. Below is an example showing the recommended +documentation format. + +```python + +CONSTANT_EXPOSED_BY_MODULE = 42 # A brief comment could explain what this is + +def double_the_input(x: float) -> float: + """This is a brief description of the function. + + This is an optional, longer description of the function. + It can span multiple lines. + + :param x: The input value, and its expected units. + :return: The result y = 2*x + """ + return 2 * x +``` + +### C#/.NET Wrappers + +In C#/.NET, documentation comments are written in +[XML format](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments) +and are used to generate documentation through tools like Visual Studio. Use `` tags to +provide brief descriptions of classes, constants, functions, etc. Functions should +include `` and `` elements for all inputs and outputs. An example +of this documentation style is shown below. + +```csharp +/// +/// Represents a class that contains constants and methods related to calculations. +/// +public class CalculationUtils +{ + /// + /// A constant value exposed by the module. + /// + public const int CONSTANT_EXPOSED_BY_MODULE = 42; + + /// + /// Doubles the input value. + /// + /// The input value to be doubled. + /// The doubled value of the input. + public double DoubleTheInput(double x) + { + // Brief comment explaining what this function does. + return 2 * x; + } +} +``` + +## Testing Code + +When modifying or extending this software, ensure that unit tests are added to +cover your new code. In general, each C++ file in `src/` has a corresponding C++ +file in `tests/` which implements unit tests. If you've added a new file in `tests/`, +make sure to add that file to the executable in `tests/CMakeLists.txt`. + +After compiling the library, you can run unit tests as follows. First, change your +working directory to the `build` directory, then run: + +```bash +ctest +``` diff --git a/GitHubRepoPublicReleaseApproval.md b/GitHubRepoPublicReleaseApproval.md new file mode 100644 index 0000000..102de20 --- /dev/null +++ b/GitHubRepoPublicReleaseApproval.md @@ -0,0 +1,32 @@ +# GitHub Repository Public Release Approval + +**Project Name:** NTIA/OSM Research and Development + +**Software Name:** Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python Wrapper + +The project identified above, which is contained within the repository this +document is stored in, has met the following criteria for public release: + +1. [ ] The project, including the test criteria, meets the requirements defined +in the ITS Software Development Publication Policy for making a repository public. +The major pre-established criteria for publication are listed below, and the check +mark next to each attests that the criterion has been met. + * [ ] Unit tests are available and the software has been tested against the unit tests. + * [ ] The software can be compiled and/or used on Windows, macOS, and Linux. + * [ ] The repository structure and contents follow from the ITS PropLib template, and + all template or placeholder code has been removed. + * [ ] The repository includes the appropriate `LICENSE.md` file +2. [ ] Any test data necessary for the code and its unit tests to function is included in this +GitHub repository, either directly or as a linked Git submodule. +3. [ ] The README.md file has passed editorial review from the ITS Publications Office. +4. [ ] The project complies with the ITS Code Style Guide or an appropriate style +guide as agreed to by the sponsor, project lead, or Supervising Division Chief. +5. [ ] Approved disclaimer and licensing language has been included. + +In order to complete this approval, please create a new branch, upload and commit +your version of this Markdown document to that branch, then create a pull request +for that branch. The following must login to GitHub and approve that pull request +before the pull request can be merged and this repo made public: + +* Project Lead: William Kozma, Jr. +* Supervising Division Chief or Release Authority: Chris Anderson diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..add8ca7 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,34 @@ +# SOFTWARE DISCLAIMER / RELEASE + +This software was developed by employees of the National Telecommunications and Information +Administration (NTIA), an agency of the Federal Government and is provided to you +as a public service. Pursuant to Title 15 United States Code Section 105, works +of NTIA employees are not subject to copyright protection within the United States. + +The software is provided by NTIA “AS IS.” NTIA MAKES NO WARRANTY OF ANY KIND, EXPRESS, +IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NTIA does +not warrant or make any representations regarding the use of the software or the +results thereof, including but not limited to the correctness, accuracy, reliability +or usefulness of the software. + +To the extent that NTIA holds rights in countries other than the United States, +you are hereby granted the non-exclusive irrevocable and unconditional right to +print, publish, prepare derivative works and distribute the NTIA software, in any +medium, or authorize others to do so on your behalf, on a royalty-free basis throughout +the World. + +You may improve, modify, and create derivative works of the software or any portion +of the software, and you may copy and distribute such modifications or works. Modified +works should carry a notice stating that you changed the software and should note +the date and nature of any such change. + +You are solely responsible for determining the appropriateness of using and distributing +the software and you assume all risks associated with its use, including but not +limited to the risks and costs of program errors, compliance with applicable laws, +damage to or loss of data, programs or equipment, and the unavailability or interruption +of operation. This software is not intended to be used in any situation where a failure +could cause risk of injury or damage to property. + +Please provide appropriate acknowledgments of NTIA’s creation of the software in +any copies or derivative works of this software. diff --git a/README.md b/README.md index b138a56..8483437 100644 --- a/README.md +++ b/README.md @@ -1 +1,126 @@ -# LFMF-python \ No newline at end of file +# Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python® Wrapper # + +[![NTIA/ITS PropLib][proplib-badge]][proplib-link] +[![GitHub Issues][gh-issues-badge]][gh-issues-link] +[![PyPI Release][pypi-release-badge]][pypi-release-link] +[![GitHub Actions Unit Test Status][gh-actions-test-badge]][gh-actions-test-link] +[![DOI][doi-badge]][doi-link] + +[proplib-badge]: https://img.shields.io/badge/PropLib-badge?label=%F0%9F%87%BA%F0%9F%87%B8%20NTIA%2FITS&labelColor=162E51&color=D63E04 +[proplib-link]: https://ntia.github.io/propagation-library-wiki +[gh-actions-test-badge]: https://img.shields.io/github/actions/workflow/status/NTIA/LFMF-python/tox.yml?branch=main&logo=pytest&logoColor=ffffff&label=Tests&labelColor=162E51 +[gh-actions-test-link]: https://github.com/NTIA/LFMF-python/actions/workflows/tox.yml +[pypi-release-badge]: https://img.shields.io/pypi/v/proplib-lfmf?logo=pypi&logoColor=ffffff&label=Release&labelColor=162E51&color=D63E04 +[pypi-release-link]: https://pypi.org/project/proplib-lfmf +[gh-issues-badge]: https://img.shields.io/github/issues/NTIA/LFMF-python?logo=github&label=Issues&labelColor=162E51 +[gh-issues-link]: https://github.com/NTIA/LFMF-python/issues +[doi-badge]: https://zenodo.org/badge/896234119.svg +[doi-link]: https://zenodo.org/badge/latestdoi/896234119 + +This code repository contains a Python wrapper for the NTIA/ITS implementation of the +Low Frequency / Medium Frequency (LF/MF) Propagation Model. LF/MF predicts basic transmission +loss in the frequency range 0.01 - 30 MHz for propagation paths over a smooth Earth and antenna +heights less than 50 meters. This Python package wraps the [NTIA/ITS C++ implementation](https://github.com/NTIA/LFMF). + +## Getting Started ## + +This software is distributed on [PyPI](https://pypi.org/project/proplib-lfmf) and is easily installable +using the following command. + +```cmd +pip install proplib-lfmf +``` + +General information about using this model is available on +[its page on the **NTIA/ITS Propagation Library Wiki**](https://ntia.github.io/propagation-library-wiki/models/LFMF/). +Additionally, Python-specific instructions and code examples are available +[here](https://ntia.github.io/propagation-library-wiki/models/LFMF/python). + +If you're a developer and would like to contribute to or extend this repository, +please review the guide for contributors [here](CONTRIBUTING.md) or open an +[issue](https://github.com/NTIA/LFMF-python/issues) to start a discussion. + +## Development ## + +This repository contains code which wraps [the C++ shared library](https://github.com/NTIA/LFMF) +as an importable Python module. If you wish to contribute to this repository, +testing your changes will require the inclusion of this shared library. You may retrieve +this either from the +[relevant GitHub Releases page](https://github.com/NTIA/LFMF/releases), or by +compiling it yourself from the C++ source code. Either way, ensure that the shared library +(`.dll`, `.dylib`, or `.so` file) is placed in `src/ITS/Propagation/LFMF/`, alongside `__init__.py`. + +Below are the steps to build and install the Python package from the source code. +Working installations of Git and a [currently-supported version](https://devguide.python.org/versions/) +of Python are required. Additional requirements exist if you want to compile the shared +library from C++ source code; see relevant build instructions +[here](https://github.com/NTIA/LFMF?tab=readme-ov-file#configure-and-build). + +1. Optionally, configure and activate a virtual environment using a tool such as +[`venv`](https://docs.python.org/3/library/venv.html) or +[`conda`](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html). + +1. Clone this repository, then initialize the Git submodule containing the test data. + + ```cmd + # Clone the repository + git clone https://github.com/NTIA/LFMF-python + cd LFMF-python + + # Initialize Git submodule containing test data + git submodule init + + # Clone the submodule + git submodule update + ``` + +1. Download the shared library (`.dll`, `.so`, or `.dylib`) from a +[GitHub Release](https://github.com/NTIA/LFMF/releases). Then place the +downloaded file in `src/ITS/Propagation/LFMF/` (alongside `__init__.py`). + +1. Install the local package and development dependencies into your current environment: + + ```cmd + pip install .[dev] + ``` + +1. To build the wheel for your platform: + + ```cmd + hatchling build + ``` + +### Running Tests ### + +Python unit tests can be run to confirm successful installation. You will need to +clone this repository's test data submodule (as described above). Then, run the tests +with pytest using the following command. + +```cmd +pytest +``` + +## References ## + +- [ITS Propagation Library Wiki](https://ntia.github.io/propagation-library-wiki) +- [LFMF Wiki Page](https://ntia.github.io/propagation-library-wiki/models/LFMF) +- [`ITS.Propagation.LFMF` C++ API Reference](https://ntia.github.io/LFMF) +- Bremmer, H. "Terrestrial Radio Waves" _Elsevier_, 1949. +- DeMinco, N. "Medium Frequency Propagation Prediction Techniques and Antenna Modeling for Intelligent Transportation Systems (ITS) Broadcast Applications", [_NTIA Report 99-368_](https://www.its.bldrdoc.gov/publications/2399.aspx), August 1999 +- DeMinco, N. "Ground-wave Analysis Model For MF Broadcast System", [_NTIA Report 86-203_](https://www.its.bldrdoc.gov/publications/2226.aspx), September 1986 +- Sommerfeld, A. "The propagation of waves in wireless telegraphy", _Ann. Phys._, 1909, 28, p.665 +- Wait, J. "Radiation From a Vertical Antenna Over a Curved Stratified Ground", _Journal of Research of the National Bureau of Standards_. Vol 56, No. 4, April 1956. Research Paper 2671 + +## License ## + +See [LICENSE](./LICENSE.md). + +"Python" and the Python logos are trademarks or registered trademarks of the Python Software Foundation, used by the National Telecommunications and Information Administration with permission from the Foundation. + +## Contact ## + +For technical questions, contact . + +## Disclaimer ## + +Certain commercial equipment, instruments, or materials are identified in this project were used for the convenience of the developers. In no case does such identification imply recommendation or endorsement by the National Telecommunications and Information Administration, nor does it imply that the material or equipment identified is necessarily the best available for the purpose. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7ffe4d1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,92 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "LFMF" +dynamic = ["version"] +description = "A Python wrapper for the NTIA/ITS implementation of the Low Frequency / Medium Frequency (LF/MF) Propagation Model" +readme = "README.md" +requires-python = ">=3.9" +license = { file = "LICENSE.md" } + +authors = [ + { name = "The Institute for Telecommunication Sciences", email = "code@ntia.gov" }, +] + +keywords = [ + "propagation", + "LFMF", + "NTIA", + "ITS", +] + +classifiers = [ + "Intended Audience :: Science/Research", + "Intended Audience :: Telecommunications Industry", + "License :: Public Domain", + "Natural Language :: English", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +[project.optional-dependencies] +tests = [ + "pytest>=8.2.0,<9.0", + "pytest-cov>=6.0.0,<7.0", + "tox>=4.21.1,<5.0", # Keep in sync with tool.tox.min_version +] +dev = [ + "hatchling>=1.25.0,<2.0", + "pre-commit>=4.0.1,<5.0", + "LFMF[tests]", +] + +[project.urls] +"Python Wrapper Source" = "https://github.com/NTIA/LFMF-python" +"Python Wrapper Bug Tracker" = "https://github.com/NTIA/LFMF-python/issues" +"C++ Source" = "https://github.com/NTIA/LFMF" +"NTIA GitHub" = "https://github.com/NTIA" +"ITS Website" = "https://its.ntia.gov" + +[tool.hatch.version] +path = "src/ITS/Propagation/LFMF/__init__.py" + +[tool.hatch.build.targets.wheel] +packages = ["src/ITS"] +ignore-vcs = true + +[tool.cibuildwheel] +test-command = "pytest ." +test-requires = "pytest" + +[tool.tox] +min_version = "4.21.1" +env_list = ["3.9", "3.10", "3.11", "3.12", "3.13"] +skip_missing_interpreters = true + +[tool.tox.env.testenv] +description = "Run tests with pytest and generate coverage report" +extras = "tests" +commands = [ + "pytest", + "--cov-report=term-missing", + "--no-cov-on-fail", + "--cov", + { replace = "posargs", extend = true }, +] + +[tool.tox.gh.python] # tox-gh config for GitHub Actions testing +"3.9" = ["3.9"] +"3.10" = ["3.10"] +"3.11" = ["3.11"] +"3.12" = ["3.12"] +"3.13" = ["3.13"] diff --git a/src/ITS/Propagation/LFMF/LFMF.py b/src/ITS/Propagation/LFMF/LFMF.py new file mode 100644 index 0000000..fa1e12e --- /dev/null +++ b/src/ITS/Propagation/LFMF/LFMF.py @@ -0,0 +1,81 @@ +from ctypes import * +from enum import IntEnum + +from .proplib_loader import PropLibCDLL + + +class Result(Structure): + # C Struct for library outputs + _fields_ = [ + ("A_btl__db", c_double), + ("E__dBuVm", c_double), + ("P_rx__dbm", c_double), + ("method", c_int), + ] + + +# Load the shared library +lib = PropLibCDLL("LFMF-1.1") + +# Define function prototypes +lib.LFMF.restype = c_int +lib.LFMF.argtypes = ( + c_double, + c_double, + c_double, + c_double, + c_double, + c_double, + c_double, + c_double, + c_int, + POINTER(Result), +) + + +def LFMF( + h_tx__meter: float, + h_rx__meter: float, + f__mhz: float, + P_tx__watt: float, + N_s: float, + d__km: float, + epsilon: float, + sigma: float, + pol: int, +) -> Result: + """ + Compute the Low Frequency / Medium Frequency (LF/MF) propagation prediction + + :param h_tx__meter: Height of the transmitter, in meters + :param h_rx__meter: Height of the receiver, in meters + :param f__mhz: Frequency, in MHz + :param P_tx__watt: Transmitter power, in watts + :param N_s: Surface refractivity, in N-Units + :param d__km: Path distance, in kilometers + :param epsilon: Relative permittivity (dimensionless) + :param sigma: Conductivity, in siemens per meter + :param pol: Polarization (enum value) + + :raises ValueError: If any input parameter is not in its valid range. + :raises Exception: If an unknown error is encountered. + + :return: In Result class. + """ + result = Result() + lib.err_check( + lib.LFMF( + c_double(h_tx__meter), + c_double(h_rx__meter), + c_double(f__mhz), + c_double(P_tx__watt), + c_double(N_s), + c_double(d__km), + c_double(epsilon), + c_double(sigma), + c_int(int(pol)), + byref(result), + ) + ) + + return result diff --git a/src/ITS/Propagation/LFMF/__init__.py b/src/ITS/Propagation/LFMF/__init__.py new file mode 100644 index 0000000..1af35eb --- /dev/null +++ b/src/ITS/Propagation/LFMF/__init__.py @@ -0,0 +1,7 @@ +# Version X.Y.Z: X.Y is the version of the C++ source, +# and Z is the version of this Python wrapper +__version__ = "1.1.0" + +from .LFMF import * + +__all__ = ["LFMF"] diff --git a/src/ITS/Propagation/LFMF/proplib_loader.py b/src/ITS/Propagation/LFMF/proplib_loader.py new file mode 100644 index 0000000..ff8312d --- /dev/null +++ b/src/ITS/Propagation/LFMF/proplib_loader.py @@ -0,0 +1,102 @@ +""" +This module defines a class for interacting with a compiled shared library using +the ctypes library in Python. It manages loading the library, defining some +expected function prototypes, and parsing exit codes returned by the library functions. + +The class `PropLibCDLL` is a thin wrapper for `ctypes.CDLL` which automatically +determines the appropriate shared library file based on the operating system and +provides methods for checking function return codes. + +Classes: +-------- +- PropLibCDLL: A subclass of `ctypes.CDLL` that manages loading a PropLib shared + library and provides error checking for its functions. + +Methods: +-------- +- __init__(name): + Initializes the `PropLibCDLL` instance by loading the specified library and + setting up the expected function prototypes. + +- get_lib_name(lib_name: str) -> str: + Static method that constructs the full filename of the library based on the + current platform. + +- err_check(rtn_code: int) -> None: + Checks the return code from the library's function call and raises a RuntimeError + with the associated error message if the return code indicates an error. + +Usage: +------ +1. Create an instance of `PropLibCDLL` with the name of the shared library (without + extension). +2. Call functions from the library using the instance. +3. Use `err_check` to handle error codes returned by those functions. + +Example: +-------- +```python +lib = PropLibCDLL("SomePropLibLibrary-1.0") +return_code = lib.SomeLibraryFunction() +lib.err_check(return_code) +``` +""" + +import platform +from ctypes import * +from pathlib import Path + + +class PropLibCDLL(CDLL): + def __init__(self, name): + full_name = self.get_lib_name(name) + super().__init__(full_name) + # Define expected function prototypes + self.GetReturnStatusCharArray.restype = POINTER(c_char_p) + self.GetReturnStatusCharArray.argtypes = (c_int,) + self.FreeReturnStatusCharArray.restype = None + self.FreeReturnStatusCharArray.argtypes = (POINTER(c_char_p),) + + @staticmethod + def get_lib_name(lib_name: str) -> str: + """Get the full filename of the library specified by `lib_name`. + + This function appends the correct file extension based on the current platform, + and prepends the full absolute file path. The shared library is expected + to exist in the same directory as this file. + + :param lib_name: The library name, with no extension or path, e.g., "P2108-1.0" + :raises NotImplementedError: For platforms other than Windows, Linux, or macOS. + :return: The full filename, including path and extension, of the library. + """ + # Load the compiled library + if platform.uname()[0] == "Windows": + lib_name += ".dll" + elif platform.uname()[0] == "Linux": + lib_name += ".so" + elif platform.uname()[0] == "Darwin": + lib_name += ".dylib" + else: + raise NotImplementedError("Your OS is not yet supported") + # Library should be in the same directory as this file + lib_path = Path(__file__).parent / lib_name + return str(lib_path.resolve()) + + def err_check(self, rtn_code: int) -> None: + """Parse the library's return code and raise an error if one occurred. + + Returns immediately for `rtn_code == 0`, otherwise retrieves the + status message string from the underlying library and raises a + RuntimeError with the status message. + + :param rtn_code: Integer return code from the underlying library. + :raises RuntimeError: For any non-zero inputs. + :return: None + """ + if rtn_code == 0: + return + else: + msg = self.GetReturnStatusCharArray(c_int(rtn_code)) + msg_str = cast(msg, c_char_p).value.decode("utf-8") + self.FreeReturnStatusCharArray(msg) + raise RuntimeError(msg_str) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_lfmf.py b/tests/test_lfmf.py new file mode 100644 index 0000000..db2d577 --- /dev/null +++ b/tests/test_lfmf.py @@ -0,0 +1,21 @@ +import pytest + +from ITS.Propagation import LFMF + +from .test_utils import ABSTOL__DB, read_csv_test_data + + +@pytest.mark.parametrize( + "inputs,rtn,expected", + read_csv_test_data("LFMF_Examples.csv"), +) +def test_lfmf(inputs, rtn, expected): + if rtn == 0: + result = LFMF.LFMF(*inputs) + assert result.A_btl__db == pytest.approx(expected[0], abs=ABSTOL__DB) + assert result.E__dBuVm == pytest.approx(expected[1], abs=ABSTOL__DB) + assert result.P_rx__dbm == pytest.approx(expected[2], abs=ABSTOL__DB) + assert result.method == int(expected[3]) + else: + with pytest.raises(RuntimeError): + LFMF.LFMF(*inputs) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..b23066e --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,23 @@ +import csv +from pathlib import Path + +# Test data is expected to exist in tests/data +TEST_DATA_DIR = Path(__file__).parent / "data" +ABSTOL__DB = 0.1 # Absolute tolerance, in dB, to ensure outputs match expected value + +# Check if test data directory exists and is not empty +if not TEST_DATA_DIR.exists() or not any(TEST_DATA_DIR.iterdir()): + _test_data_checked = True + raise RuntimeError( + f"Test data is not available in {TEST_DATA_DIR}.\n Try running " + + "`git submodule init` and `git submodule update` to clone the test data submodule." + ) + + +def read_csv_test_data(filename: str): + with open(TEST_DATA_DIR / filename) as f: + reader = csv.reader(f) + next(reader) # Skip header row + for row in reader: + # yields (*inputs, rtn, *outputs) + yield tuple(map(float, row[:-5])), int(row[-5]), tuple(map(float, row[-4:]))