diff --git a/.zenodo.json b/.zenodo.json index e4a5acc..833748a 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -14,7 +14,7 @@ "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.", + "description": "This code repository contains a Python wrapper for the NTIA/ITS implementation of the Low Frequency / Medium Frequency (LF/MF) Propagation Model. This Python package wraps the NTIA/ITS C++ implementation.", "keywords": [ "propagation", "communications", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4c5d11..a1d9b41 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,18 +51,16 @@ 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). +PropLib C++ repositories make use of Git submodules to reference certain development +dependencies, e.g. GoogleTest. Depending on the CMake preset or options used, submodules +may be required to successfully build and/or test the software. When cloning a repository, +submodules are not additionally cloned by default. Use the following commands to initialize +and clone any submodules in a repository: + +```cmd +git submodule init +git submodule update +``` ### Contributing on GitHub @@ -130,35 +128,27 @@ repository. For details about wrapper repositories, refer to their own README fi ```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 + test-data/ # Git submodule containing test data files shared with wrappers + ... # Other 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" + .h # Library interface header file goes here, e.g. "ITM.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. +CMakePresets.json # Presets for CMake, e.g. "release64", "debug32", etc. ... ``` @@ -179,7 +169,6 @@ The following CMake options are used for top-level project configuration: | `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 @@ -202,21 +191,21 @@ generating the Doxygen documentation site. 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 +# Configure and compile in 64-bit release configuration +cmake --preset release64 +cmake --build --preset release64 -# Use the release configuration but don't build Doxygen docs -cmake --preset release -DBUILD_DOCS=OFF -cmake --build --preset release +# Use the 64-bit release configuration but don't build Doxygen docs +cmake --preset release64 -DBUILD_DOCS=OFF +cmake --build --preset release64 -# Configure and compile in debug configuration -cmake --preset debug -cmake --build --preset debug +# Configure and compile in 32-bit debug configuration +cmake --preset debug32 +cmake --build --preset debug32 -# Use the release configuration but don't run driver tests -cmake --preset release -DRUN_DRIVER_TESTS=OFF -cmake --build --preset release +# Use the 64-bit release configuration but don't run driver tests +cmake --preset release64 -DRUN_DRIVER_TESTS=OFF +cmake --build --preset release64 ``` ### Supported Platforms and Build Options @@ -243,7 +232,7 @@ example showing the expected documentation formats. Except for inline documentat 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 */ +constexpr double = PI 3.1415; ///< Inline format, e.g. for constants /******************************************************************************* * This is a brief description of the function. @@ -273,8 +262,36 @@ 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. +MATLAB® wrappers are implemented as toolboxes which interface with the shared library +compiled from C++ source code. The project structure is informed by the best practices +provided by MathWorks® in their [`toolboxdesign` repository](https://github.com/mathworks/toolboxdesign). +Here is an example of how a function may be documented in a MATLAB wrapper. Note the +documentation with code, where input and output arguments are provided for autocompletion. + +```matlab +function y = DoubleTheInput(x) +% DoubleTheInput - produces an output which is twice its input. +% +% Syntax: +% y = DoubleTheInput(x) +% +% Input Arguments: +% x (double) - A number which needs doubling +% +% Output Arguments: +% y (double) - The result, 2*x +% +% Description: +% Functions more complex than this one may warrant an additional, +% longer description. +arguments (Input) + x double +end +arguments (Output) + y double +end +... +``` ### Python Wrappers @@ -302,9 +319,9 @@ def double_the_input(x: float) -> float: return 2 * x ``` -### C#/.NET Wrappers +### .NET Wrappers -In C#/.NET, documentation comments are written in +PropLib .NET wrappers are written in C# and 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 diff --git a/GitHubRepoPublicReleaseApproval.md b/GitHubRepoPublicReleaseApproval.md index f2af819..a5d6176 100644 --- a/GitHubRepoPublicReleaseApproval.md +++ b/GitHubRepoPublicReleaseApproval.md @@ -1,8 +1,8 @@ # GitHub Repository Public Release Approval -**Project Name:** NTIA/OSM Research and Development +**Project Name:** NTIA/OSM Research and Development - Propagation Library -**Software Name:** Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python Wrapper +**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: @@ -18,14 +18,14 @@ mark next to each attests that the criterion has been met. * [x] The repository includes the appropriate `LICENSE.md` file 2. [x] 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. [x] The README.md file has passed editorial review from the ITS Publications Office. +3. [x] The README.md file has passed editorial review by the ITS Publications Office. 4. [x] 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. [x] 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 +your version of this Markdown document to that branch, and then create a pull request +for that branch. The following must log in to GitHub and approve that pull request before the pull request can be merged and this repo made public: * Project Lead: William Kozma, Jr. diff --git a/README.md b/README.md index 3e1e8f3..3910973 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python® Wrapper # +# Low Frequency / Medium Frequency (LF/MF) Propagation Model, Python® Wrapper # [![NTIA/ITS PropLib][proplib-badge]][proplib-link] [![PyPI Release][pypi-release-badge]][pypi-release-link] diff --git a/src/ITS/Propagation/LFMF/LFMF.py b/src/ITS/Propagation/LFMF/LFMF.py index def17a9..50dbaa0 100644 --- a/src/ITS/Propagation/LFMF/LFMF.py +++ b/src/ITS/Propagation/LFMF/LFMF.py @@ -4,16 +4,33 @@ from .proplib_loader import PropLibCDLL -class Result(Structure): +class Polarization(IntEnum): + Horizontal = 0 + Vertical = 1 + + +class SolutionMethod(IntEnum): + FlatEarthCurveCorrection = 0 + ResidueSeries = 1 + + +class c_LFMFResult(Structure): # C Struct for library outputs _fields_ = [ - ("A_btl__db", c_double), - ("E__dBuVm", c_double), - ("P_rx__dbm", c_double), - ("method", c_int), + ("A_btl__db", c_double), # Basic transmission loss, in dB + ("E__dBuVm", c_double), # Electic field strength, in db(uV/m) + ("P_rx__dbm", c_double), # Received power, in dBm + ("method", c_int), # Solution method used ] +class LFMFResult(Structure): + A_btl__db: float = None # Basic transmission loss, in dB + E__dBuVm: float = None # Electic field strength, in db(uV/m) + P_rx__dbm: float = None # Received power, in dBm + method: SolutionMethod = None # Solution method used + + # Load the shared library lib = PropLibCDLL("LFMF-1.1") @@ -29,15 +46,10 @@ class Result(Structure): c_double, c_double, c_int, - POINTER(Result), + POINTER(c_LFMFResult), ) -class Polarization(IntEnum): - Horizontal = 0 - Vertical = 1 - - def LFMF( h_tx__meter: float, h_rx__meter: float, @@ -48,7 +60,7 @@ def LFMF( epsilon: float, sigma: float, pol: Polarization, -) -> Result: +) -> LFMFResult: """ Compute the Low Frequency / Medium Frequency (LF/MF) propagation prediction @@ -67,7 +79,7 @@ def LFMF( :return: In Result class. """ - result = Result() + result = c_LFMFResult() lib.err_check( lib.LFMF( c_double(h_tx__meter), @@ -78,9 +90,19 @@ def LFMF( c_double(d__km), c_double(epsilon), c_double(sigma), - c_int(int(pol)), + c_int(pol), byref(result), ) ) + return __convertResultStruct(result) + + +def __convertResultStruct(c_result): + result = LFMFResult + result.A_btl__db = c_result.A_btl__db + result.E__dBuVm = c_result.E__dBuVm + result.P_rx__dbm = c_result.P_rx__dbm + result.method = SolutionMethod(c_result.method) + return result diff --git a/src/ITS/Propagation/LFMF/__init__.py b/src/ITS/Propagation/LFMF/__init__.py index 46ddf3f..a0bde28 100644 --- a/src/ITS/Propagation/LFMF/__init__.py +++ b/src/ITS/Propagation/LFMF/__init__.py @@ -2,4 +2,9 @@ # and Z is the version of this Python wrapper __version__ = "1.1.0" -from .LFMF import LFMF, Polarization, Result +from .LFMF import ( + LFMF, + Polarization, + SolutionMethod, + LFMFResult +) diff --git a/tests/data b/tests/data index d3cc4d6..c593360 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit d3cc4d6efff973b1a4fac4dbf3074439aa3246ba +Subproject commit c593360c1c9f1cde463d98a3bf89447903d95861 diff --git a/tests/test_lfmf.py b/tests/test_lfmf.py index db2d577..c420f49 100644 --- a/tests/test_lfmf.py +++ b/tests/test_lfmf.py @@ -10,12 +10,17 @@ 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]) + if rtn != 40: + pol = LFMF.Polarization(int(inputs[-1])) + if rtn == 0: + result = LFMF.LFMF(*inputs[:-1], pol) + 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 == LFMF.SolutionMethod(int(expected[3])) + else: + with pytest.raises(RuntimeError): + LFMF.LFMF(*inputs[:-1], pol) else: with pytest.raises(RuntimeError): - LFMF.LFMF(*inputs) + LFMF.LFMF(*inputs[:-1], int(inputs[-1])) diff --git a/tests/test_utils.py b/tests/test_utils.py index a4b7633..1699a2e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -13,6 +13,7 @@ ) +# Read CSV into dictionary and convert to specified data type def read_csv_test_data(filename: str): with open(TEST_DATA_DIR / filename) as f: reader = csv.reader(f)