diff --git a/.github/workflows/build-necpp-wheels.yml b/.github/workflows/build-necpp-wheels.yml new file mode 100644 index 0000000..b1255a2 --- /dev/null +++ b/.github/workflows/build-necpp-wheels.yml @@ -0,0 +1,126 @@ +name: Build and upload necpp to PyPI + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + release: + types: + - published + +jobs: + build_wheels: + name: Build necpp wheels on ${{ matrix.os }} + runs-on: ${{ matrix.runs-on }} + permissions: + contents: read + strategy: + matrix: + include: + - os: linux + runs-on: ubuntu-latest + - os: macos-intel + runs-on: macos-15 + - os: macos-arm + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install build dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y swig autoconf automake libtool + + - name: Install build dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install swig autoconf automake libtool + + - name: Configure necpp_src + shell: bash + run: | + cd necpp_src + make -f Makefile.git + ./configure + + - name: Setup package directory and generate SWIG wrapper + shell: bash + run: | + cd necpp + ln -s ../necpp_src . + swig -I../necpp_src/src/ -python necpp.i + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + env: + CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*" + CIBW_SKIP: "*-musllinux_*" + CIBW_BUILD_VERBOSITY: 1 + with: + package-dir: necpp + output-dir: wheelhouse + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-necpp-${{ matrix.os }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build necpp source distribution + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y swig autoconf automake libtool + + - name: Configure necpp_src + run: | + cd necpp_src + make -f Makefile.git + ./configure + + - name: Setup package directory and generate SWIG wrapper + run: | + cd necpp + ln -s ../necpp_src . + swig -I../necpp_src/src/ -python necpp.i + + - name: Build sdist + run: | + cd necpp + pipx run build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: cibw-sdist-necpp + path: necpp/dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v4 + with: + # unpacks all CIBW artifacts into dist/ + pattern: cibw-* + path: dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/build-pynec-wheels.yml b/.github/workflows/build-pynec-wheels.yml new file mode 100644 index 0000000..bb89d90 --- /dev/null +++ b/.github/workflows/build-pynec-wheels.yml @@ -0,0 +1,126 @@ +name: Build and upload PyNEC to PyPI + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + release: + types: + - published + +jobs: + build_wheels: + name: Build PyNEC wheels on ${{ matrix.os }} + runs-on: ${{ matrix.runs-on }} + permissions: + contents: read + strategy: + matrix: + include: + - os: linux + runs-on: ubuntu-latest + - os: macos-intel + runs-on: macos-15 + - os: macos-arm + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install build dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y swig autoconf automake libtool + + - name: Install build dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install swig autoconf automake libtool + + - name: Configure necpp_src + shell: bash + run: | + cd necpp_src + make -f Makefile.git + ./configure + + - name: Setup package directory and generate SWIG wrapper + shell: bash + run: | + cd PyNEC + ln -s ../necpp_src . + swig -Wall -c++ -python PyNEC.i + + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.5 + env: + CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*" + CIBW_SKIP: "*-musllinux_*" + CIBW_BUILD_VERBOSITY: 1 + with: + package-dir: PyNEC + output-dir: wheelhouse + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-pynec-${{ matrix.os }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build PyNEC source distribution + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y swig autoconf automake libtool + + - name: Configure necpp_src + run: | + cd necpp_src + make -f Makefile.git + ./configure + + - name: Setup package directory and generate SWIG wrapper + run: | + cd PyNEC + ln -s ../necpp_src . + swig -Wall -c++ -python PyNEC.i + + - name: Build sdist + run: | + cd PyNEC + pipx run build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: cibw-sdist-pynec + path: PyNEC/dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v4 + with: + # unpacks all CIBW artifacts into dist/ + pattern: cibw-* + path: dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitmodules b/.gitmodules index 08e0e53..affaabc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "necpp_src"] path = necpp_src - url = git@github.com:tmolteno/necpp + url = https://github.com/tmolteno/necpp.git \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dd1eb58 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +PLAT=manylinux2_x86_28 +DOCKER_IMAGE=quay.io/pypa/manylinux2014_x86_64 +PRE_CMD= +docker-wheels: + docker run --rm -e PLAT=${PLAT} -v `pwd`:/io ${DOCKER_IMAGE} ${PRE_CMD} /io/build_wheels.sh + auditwheel repair /output/mylibrary*whl -w /output + +install: + docker pull ${DOCKER_IMAGE} diff --git a/PyNEC/.gitignore b/PyNEC/.gitignore index a713962..fbe747b 100644 --- a/PyNEC/.gitignore +++ b/PyNEC/.gitignore @@ -1,2 +1,6 @@ build/** *.pyc +PyNEC.py +*.cxx +*.png +necpp_src diff --git a/PyNEC/CHANGES.md b/PyNEC/CHANGES.md new file mode 100644 index 0000000..b22db73 --- /dev/null +++ b/PyNEC/CHANGES.md @@ -0,0 +1,4 @@ +### Version 1.7.3.6: + +* Update with a requested fix by user slawkory in context_clean.py +* Also fix an intger division bug introduced with the shift to python3 in logperiodic_opt.py diff --git a/PyNEC/INSTALL.md b/PyNEC/INSTALL.md new file mode 100644 index 0000000..06059a3 --- /dev/null +++ b/PyNEC/INSTALL.md @@ -0,0 +1,17 @@ +## Building from Source + + aptitude install swig3.0 + git submodule init + git submodule update --remote + cd PyNEC + ./build.sh + sudo python setup.py install + + +## Uploading the package to pypi + +Source & Binary Distribution + python3 setup.py sdist + python3 setup.py bdist_wheel + + python3 setup.py upload diff --git a/PyNEC/LICENCE.txt b/PyNEC/LICENCE.txt new file mode 100644 index 0000000..d6a9326 --- /dev/null +++ b/PyNEC/LICENCE.txt @@ -0,0 +1,340 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/PyNEC/MANIFEST.in b/PyNEC/MANIFEST.in index 4092210..181a486 100644 --- a/PyNEC/MANIFEST.in +++ b/PyNEC/MANIFEST.in @@ -4,4 +4,5 @@ include necpp_src/src/*.h include necpp_src/config.h -include necpp_src/example/test.py \ No newline at end of file +include example/*.py + diff --git a/PyNEC/Makefile b/PyNEC/Makefile new file mode 100644 index 0000000..8b45947 --- /dev/null +++ b/PyNEC/Makefile @@ -0,0 +1,19 @@ +build: + python3 setup.py sdist + #python3 setup.py bdist_wheel + +clean: + rm -rf build + rm -rf dist + +test-upload: + python3 -m pip install --user --upgrade twine + + python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* + +upload: + python3 -m twine upload dist/* + +test-install: + python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps PyNEC --upgrade + diff --git a/PyNEC/README.md b/PyNEC/README.md index 4449822..1829c1d 100644 --- a/PyNEC/README.md +++ b/PyNEC/README.md @@ -1,7 +1,6 @@ # Python NEC2++ Module -This module wraps the C++ API for antenna simulation of nec2++. It is easier to work with, and more powerful -than the C-style API wrapper. +This module wraps the C++ API for antenna simulation of nec2++. It is easier to work with, and more powerful than the C-style API wrapper. Works with Python 2.7 and 3+. ## Usage @@ -53,17 +52,48 @@ Here is an example that plots a radiation pattern. plt.savefig('RadiationPattern.png') plt.show() -## Install +## Manual Build & install - git clone https://github.com/tmolteno/python-necpp.git - cd python-necpp - git submodule init - git submodule update --remote - cd PyNEC - ./build.sh - sudo python setup.py install +Requirements + +* [Pandoc](https://pandoc.org/installing.html) +* [Swig](http://www.swig.org/download.html) +* For Windows: [C/C++ compilers](https://wiki.python.org/moin/WindowsCompilers). +* Git bash (for running build.sh script) +* Latest python packages: pip, setuptools, numpy, wheel, numpy. Run: +`$ pip install --upgrade pip setuptools wheel numpy` + +*Note: Download and extract swigwin.zip and add the path to swig.exe to environment.* + +Then do following: + + $ git clone --recursive https://github.com/tmolteno/python-necpp.git + $ cd python-necpp + $ cd PyNEC + $ ./build.sh + $ python setup.py bdist_wheel (For generating wheel, requires wheel package) + $ sudo python setup.py install + +*Note: 'sudo' is not required in windows.* + +## Install from PyPI + + $ sudo pip install pynec + +*Note: 'sudo' is not required in windows.* ## Testing - python example/test_rp.py +Requirements + +* python package: matplotlib + + $ python example/test_rp.py + +The example directory contains the following additional examples (that are inspired by excercises from a course on antennas): + +* logperiodic_opt.py is an example on how to combine PyNECPP with scipy.optimize to use a genetic algorithm to **optimize an antenna for multiple frequency bands** at the same time (which I thin is not possible in 4nec2). The resulting gains and VSWR are plotted over the frequency range of interest. This requires scipy >= 0.15.0 due to the usage of scipy.optimize.differential_evolution. +* monopole_realistic_ground_plane.py plots the vertical gain pattern of a monopole antenna. Its dimensions are optimized with a local search, and the path through the search space is visualized with a heat map. +* dipole.py does a very simple optimization of a dipole, and plots the VSWR over a given frequency range for different system impedances to file. + diff --git a/PyNEC/build.sh b/PyNEC/build.sh index 9712711..4785138 100755 --- a/PyNEC/build.sh +++ b/PyNEC/build.sh @@ -4,16 +4,15 @@ # # Author. Tim Molteno. # +# FIrst have to do git submodule init git submodule update --remote -# -pushd ../necpp_src +ln -s ../necpp_src . +DIR=`pwd` +cd ../necpp_src make -f Makefile.git ./configure --without-lapack -popd - -# Generate a README.txt from README.md -pandoc -o README.txt README.md +cd ${DIR} # Build PyNEC swig -Wall -v -c++ -python PyNEC.i -python setup.py build \ No newline at end of file +python3 setup.py build diff --git a/example/.gitignore b/PyNEC/example/.gitignore similarity index 100% rename from example/.gitignore rename to PyNEC/example/.gitignore diff --git a/PyNEC/example/README.md b/PyNEC/example/README.md new file mode 100644 index 0000000..0b242c8 --- /dev/null +++ b/PyNEC/example/README.md @@ -0,0 +1,15 @@ +# PyNEC examples + +This folder contains some examples showing the use of PyNEC antenna simulation module + +## Optimizing a Monopole + +The file monopole.py simulates a monopole antenna, printing out the impedance. To optimize this + + python3 monopole.py + +To optimize the monopole design to achieve a particular impedance, try + + python3 optimized.py --basinhopping --target-impedance=110 + +This code uses standard scipy optimizers to find the best solution. diff --git a/PyNEC/example/antenna_util.py b/PyNEC/example/antenna_util.py new file mode 100644 index 0000000..5f5567f --- /dev/null +++ b/PyNEC/example/antenna_util.py @@ -0,0 +1,31 @@ +# +# Some antenna utility functions +# +import numpy as np + +def reflection_coefficient(z, z0): + return np.abs((z - z0) / (z + z0)) + +def vswr(z, z0): + Gamma = reflection_coefficient(z, z0) + return float((1 + Gamma) / (1 - Gamma)) + +def mismatch(z, z0): + Gamma = reflection_coefficient(z, z0) + return 1 - Gamma**2 + +# Source: https://gist.github.com/temporaer/6755266 +# 'matplotlib log-polar plots seem to be quite buggy at the time of writing.' Yes, indeed, sadly... +def plot_logpolar(ax, theta, r_, bullseye=None, **kwargs): + min10 = np.log10(np.min(r_)) + max10 = np.log10(np.max(r_)) + if bullseye is None: + bullseye = min10 - np.log10(0.5 * np.min(r_)) + r = np.log10(r_) - min10 + bullseye + ax.plot(theta, r, **kwargs) + l = np.arange(np.floor(min10), max10) + ax.set_rticks(l - min10 + bullseye) + # ax.set_yticklabels(["1e%d" % x for x in l]) + ax.set_yticklabels(["%d" % x for x in l]) + ax.set_rlim(0, max10 - min10 + bullseye) + return ax diff --git a/PyNEC/example/context_clean.py b/PyNEC/example/context_clean.py new file mode 100644 index 0000000..4f4a3d3 --- /dev/null +++ b/PyNEC/example/context_clean.py @@ -0,0 +1,184 @@ +# Note: explicit zeroes are blanks. All other values should be specified symbolically. + +# Currently these contain only the subset of cards that I needed + +class Range(object): + def __init__(self, start, stop, count=None, delta=None): + self.start = start + self.stop = stop + if count is not None: + self.count = count + self.delta = (stop - start) / count + else: + self.count = (stop_ - start) / delta + self.delta = delta + +# Setting do_debug to True will dump all the cards generated with context_clean, so you can verify the output more easily in a text editor (and debug that file manually) +do_debug = False + +def debug(card, *args): + if do_debug: + stringified = " , ".join([str(a) for a in args]) + print("%s %s" % (card, stringified)) + +class context_clean(object): + def __init__(self, context): + self.context = context + + def remove_all_loads(self): + ld_short_all_loads = -1 + self.context.ld_card(ld_short_all_loads, 0, 0, 0, 0, 0, 0) + + def set_wire_conductivity(self, conductivity, wire_tag=None): + """ The conductivity is specified in mhos/meter. Currently all segments of a wire are set. If wire_tag is None, all wire_tags are set (i.e., a tag of 0 is used). """ + if wire_tag is None: + wire_tag = 0 + + debug("LD", 5, wire_tag, 0, 0, conductivity, 0, 0) + self.context.ld_card(5, wire_tag, 0, 0, conductivity, 0, 0) + + def set_all_wires_conductivity(self, conductivity): + self.set_wire_conductivity(conductivity) + + # TODO: multiplicative + def set_frequencies_linear(self, start_frequency, stop_frequency, count=None, step_size=None): + """ If start_frequency does not equal stop_frequency, either count or step should be specified. The other parameter will be automatically deduced """ + + if start_frequency == stop_frequency: + step_size = 0 + count = 1 + else: + # TODO: add some asserts + if count is not None: + step_size = (stop_frequency - start_frequency) / count + else: + count = (stop_frequency - start_frequency) / step_size + + # TODO, what if we don't have nice divisibility here + count = int(count) + + ifrq_linear_step = 0 + debug("FR", ifrq_linear_step, count, start_frequency, step_size, 0, 0, 0) + self.context.fr_card(ifrq_linear_step, count, start_frequency, step_size) + + def set_frequency(self, frequency): + self.set_frequencies_linear(frequency, frequency) + + def clear_ground(self): + gn_nullify_ground = -1 + self.context.gn_card(gn_nullify_ground, 0, 0, 0, 0, 0, 0, 0) + + # TODO: I could probably make a ground class, would probably be cleaner to group some of the options and different functions there (like combining ground screen etc) + + # TODO: gn card is iffy, check! + def set_finite_ground(self, ground_dielectric, ground_conductivity): + gn_finite_ground = 0 + no_ground_screen = 0 + + self.context.gn_card(gn_finite_ground, no_ground_screen, ground_dielectric, ground_conductivity, 0, 0, 0, 0) + + def set_perfect_ground(self): + gn_perfectly_conducting = 1 + no_ground_screen = 0 + + debug("GN", gn_perfectly_conducting, no_ground_screen, 0, 0, 0, 0, 0, 0) + self.context.gn_card(gn_perfectly_conducting, no_ground_screen, 0, 0, 0, 0, 0, 0) + + + # TODO: i1 = 5 is also a voltage excitation + def voltage_excitation(self, wire_tag, segment_nr, voltage): + ex_voltage_excitation = 0 + no_action = 0 # TODO configurable + option_i3i4 = 10*no_action + no_action + + debug("EX", ex_voltage_excitation, wire_tag, segment_nr, option_i3i4, voltage.real, voltage.imag, 0, 0, 0, 0) + self.context.ex_card(ex_voltage_excitation, wire_tag, segment_nr, option_i3i4, voltage.real, voltage.imag, 0, 0, 0, 0) + + def get_geometry(self): + #return geometry_clean(self.context.get_geometry()) # TODO + return self.context.get_geometry() + + def set_extended_thin_wire_kernel(self, enable): + if enable: + debug ("EK", 0) + self.context.set_extended_thin_wire_kernel(True) + else: + debug ("EK", -1) + self.context.set_extended_thin_wire_kernel(False) + + def geometry_complete(self, ground_plane, current_expansion=True): + no_ground_plane = 0 + ground_plane_current_expansion = 1 + ground_plane_no_current_expansion = -1 + if not ground_plane: + debug("GE", no_ground_plane) + self.context.geometry_complete(no_ground_plane) + else: + if current_expansion: + debug("GE", ground_plane_current_expansion) + self.context.geometry_complete(ground_plane_current_expansion) + else: + debug("GE", ground_plane_no_current_expansion) + self.context.geometry_complete(ground_plane_no_current_expansion) + + output_major_minor = 0 + output_vertical_horizontal = 1 + + normalization_none = 0 + normalization_major = 1 + normalization_minor = 2 + normalization_vertical = 3 + normalization_horizontal = 4 + normalization_totalgain = 5 + + power_gain = 0 + directive_gain = 1 + + average_none = 0 + average_gain = 1 + average_todo = 2 + + # TODO: this should be different for surface_wave_mode (1), because then thetas = z + def radiation_pattern(self, thetas, phis, output_mode=output_vertical_horizontal, normalization=normalization_none, gain=power_gain, average=average_todo): + """ thetas and phis should be Range(-like) objects """ + normal_mode = 0 # TODO other modes + + # the rp_card already has XNDA as separate arguments + radial_distance = 0 # TODO + gnornamize_maximum = 0 # TODO + + xnda = average + 10*gain+100*normalization+1000*output_mode + + debug("RP", normal_mode, thetas.count, phis.count, xnda, thetas.start, phis.start, thetas.delta, phis.delta, radial_distance, gnornamize_maximum) + self.context.rp_card(normal_mode, thetas.count, phis.count, output_mode, normalization, gain, average, thetas.start, phis.start, thetas.delta, phis.delta, radial_distance, gnornamize_maximum) + + # TODO: shunt admittances, length of transmission line if not straight-line distance + def transmission_line(self, src, dst, impedance, crossed_line=False, length=None, shunt_admittance_src=0, shunt_admittance_dst=0): + """ src and dst are (tag_nr, segment_nr) pairs """ + if crossed_line: + impedance *= -1 + if length is None: + length = 0 + shunt_admittance_src = complex(shunt_admittance_src) + shunt_admittance_dst = complex(shunt_admittance_dst) + + debug("TL", src[0], src[1], dst[0], dst[1], impedance, length, shunt_admittance_src.real, shunt_admittance_src.imag, shunt_admittance_dst.real, shunt_admittance_dst.imag) + self.context.tl_card(src[0], src[1], dst[0], dst[1], impedance, length, shunt_admittance_src.real, shunt_admittance_src.imag, shunt_admittance_dst.real, shunt_admittance_dst.imag) + + # Some simple wrappers for context... + # TODO: this should be simpler, can't this be auto-generated, or implicitly defined? The best solution is of course to do this in the C++ code, + # and then the wrappers are immediately correct and nice + def xq_card(self, *args): + return self.context.xq_card(*args) + def get_input_parameters(self, *args): + return self.context.get_input_parameters(*args) + +class geometry_clean(object): + def __init__(self, geometry): + self.geometry = geometry + + def wire(self, tag_id, nr_segments, src, dst, radius, length_ratio=1.0, radius_ratio=1.0): + """ radius is in meter. length_ratio can be set to have non-uniform segment lengths, radius_ratio can be used for tapered wires """ + debug("GW", tag_id, nr_segments, src[0], src[1], src[2], dst[0], dst[1], dst[2], radius) # TODO + + self.geometry.wire(tag_id, nr_segments, src[0], src[1], src[2], dst[0], dst[1], dst[2], radius, length_ratio, radius_ratio) diff --git a/PyNEC/example/dipole.py b/PyNEC/example/dipole.py new file mode 100644 index 0000000..780c713 --- /dev/null +++ b/PyNEC/example/dipole.py @@ -0,0 +1,144 @@ +import numpy as np +import scipy.optimize +import pylab as plt + + +from PyNEC import * +from antenna_util import * + +from context_clean import * + +import math + +def geometry(freq_mhz, length, nr_segments): + wire_radius = 0.01e-3 # 0.01 mm + + nec = context_clean(nec_context()) + nec.set_extended_thin_wire_kernel(False) + + geo = geometry_clean(nec.get_geometry()) + + center = np.array([0,0,0]) + half = np.array([length/2, 0, 0]) + + pt1 = center - half + pt2 = center + half + + wire_tag = 1 + geo.wire(tag_id=wire_tag, nr_segments=nr_segments, src=pt1, dst=pt2, radius=wire_radius) + + nec.geometry_complete(ground_plane=False) + + nec.set_frequency(freq_mhz) + + # Voltage excitation in the center of the antenna + nec.voltage_excitation(wire_tag=wire_tag, segment_nr=int(nr_segments/2), voltage=1.0) + + return nec + +def impedance(freq_mhz, length, nr_segments): + nec = geometry(freq_mhz, length, nr_segments) + nec.xq_card(0) + + index = 0 + return nec.get_input_parameters(index).get_impedance() + +def create_optimization_target(freq_mhz, nr_segments): + def target(length): + return abs(impedance(freq_mhz, length[0], nr_segments).imag) + return target + +# It's probably possible that the antenna matches in multiple regions, pick the one around the center frequency... +def matched_range_around(nec, count, center_freq, system_impedance): + # TODO: this is not ideal + min_dist = float('inf') + min_idx = None + target_hz = center_freq * 1000000 + + for idx in range(0, count): + dist = abs(nec.get_input_parameters(idx).get_frequency() - target_hz) + if dist < min_dist: + min_dist = dist + min_idx = idx + + idx = min_idx + matched_min_freq = None + while idx >= 0: + ipt = nec.get_input_parameters(idx) + z = ipt.get_impedance() + if vswr(z, system_impedance) > 2: + break + matched_min_freq = ipt.get_frequency() / 1000000 + idx -= 1 + + idx = min_idx + matched_max_freq = None + while idx < count: + ipt = nec.get_input_parameters(idx) + z = ipt.get_impedance() + if vswr(z, system_impedance) > 2: + break + matched_max_freq = ipt.get_frequency() / 1000000 + idx += 1 + + return (matched_min_freq, matched_max_freq) + +if (__name__ == '__main__'): + design_freq_mhz = 2450 + wavelength = 299792e3/(design_freq_mhz*1000000) + + initial_length = wavelength / 2 # TODO + + print("Wavelength is %0.4fm, initial length is %0.4fm" % (wavelength, initial_length)) + + nr_segments = 101 # int(math.ceil(50*initial_length/wavelength)) + + z = impedance(design_freq_mhz, initial_length, nr_segments) + + print("Initial impedance: (%6.1f,%+6.1fI) Ohms" % (z.real, z.imag)) + + target = create_optimization_target(design_freq_mhz, nr_segments) + optimized_result = scipy.optimize.minimize(target, np.array([initial_length])) + optimized_length = optimized_result.x[0] + + z = impedance(design_freq_mhz, optimized_length, nr_segments) + + print("Optimized length %6.6f m, which gives an impedance of: (%6.4f,%+6.4fI) Ohms" % (optimized_length, z.real, z.imag)) + print("VSWR @ 75 Ohm is %6.6f" % vswr(z, 75)) + + for system_impedance in [75, 50, 300]: + nec = geometry(design_freq_mhz, optimized_length, nr_segments) + + count = 300 + nec.set_frequencies_linear(2300, 2600, count=count) + nec.xq_card(0) # Execute simulation + + # TODO: add get_n_items to nec_antenna_input and co, so we can automatically deduce count etc. Much cleaner + + rng = matched_range_around(nec, count, design_freq_mhz, system_impedance) + if rng[0] is None or rng[1] is None: + print("VSWR is nowhere <= 2 @ %i Ohm!" % system_impedance) + else: + bandwidth = 100.0 * (rng[1] - rng[0]) / design_freq_mhz + print("The fractional bandwidth @ %i Ohm is %2.2f%% - %i MHz (%i Mhz to %i MHz)" % (system_impedance, bandwidth, (rng[1] - rng[0]), rng[0], rng[1])) + + freqs = [] + vswrs = [] + for idx in range(0, count): + ipt = nec.get_input_parameters(idx) + z = ipt.get_impedance() + + freqs.append(ipt.get_frequency() / 1000000) + vswrs.append(vswr(z, system_impedance)) + + # print "%i MHz, Z = %6.6f @ %i" % (ipt.get_frequency(), vswr(z, system_impedance), system_impedance) + + plt.figure() + plt.plot(freqs, vswrs) + plt.title("VSWR of a %.2f mm long dipole for a %i Ohm system" % (optimized_length * 1000.0, system_impedance)) + plt.xlabel("Frequency (MHz)") + plt.ylabel("VSWR") + plt.grid(True) + filename = "vswr_%i_MHz.pdf" % system_impedance + print("Saving plot to file: %s" % filename) + plt.savefig(filename) diff --git a/example/impedance_plot.py b/PyNEC/example/impedance_plot.py similarity index 100% rename from example/impedance_plot.py rename to PyNEC/example/impedance_plot.py diff --git a/PyNEC/example/logperiodic_opt.py b/PyNEC/example/logperiodic_opt.py new file mode 100644 index 0000000..0a4ecd5 --- /dev/null +++ b/PyNEC/example/logperiodic_opt.py @@ -0,0 +1,244 @@ +import numpy as np +import scipy.optimize +import matplotlib.pyplot as plt +import matplotlib as mpl + +from PyNEC import * +from antenna_util import * + +from context_clean import * + +import math + +""" Optimize and plot the gains/VSWR of a logperiodic antenna (6 brass elements, 75 Ohm transmission lines) for both the 2.4GHz as the 5.8GHz ISM bands. + Inspired by an excercise for a course, hence the weird constraints. """ + +brass_conductivity = 15600000 # mhos + +tl_impedance = 75.0 + +def geometry_logperiodic(l_1, x_1, tau): + """ + x_1 is the distance from the origin to the largest (farthest away) dipole, which has a length of l_1. + The spacing is as follows: l_{i+1}/l_i = tau = x_{i+1}/x_i + """ + wire_radius = 0.00025 # 0.25 mm + + # alpha = np.arctan( (l_1/2.0) / x_1 ) + + nec = context_clean(nec_context()) + nec.set_extended_thin_wire_kernel(True) + + geo = geometry_clean(nec.get_geometry()) + + # Dipoles should be oriented in the Z direction; they should be placed on the (positive) X axis + + x_i = x_1 + l_i = x_1 + + # As ususal, note that nec tags start at 1, and we typically index from 0! + dipole_center_segs = {} # Maps from NEC wire id! + + dipoles_count = 5 + + for dipole_tag in range(1, dipoles_count + 1): + nr_segments = int(math.ceil(50*l_i/wavelength)) # TODO this might vary when sweeping even! + #print nr_segments + + dipole_center_segs[dipole_tag] = nr_segments // 2 + 1 + + center = np.array([x_i, 0, 0]) + half_height = np.array([0 , 0, l_i/2.0]) + top = center + half_height + bottom = center - half_height + + geo.wire(tag_id=dipole_tag, nr_segments=nr_segments, src=bottom, dst=top, radius=wire_radius) + + x_i = tau * x_i + l_i = tau * l_i + + # Everything is in brass + nec.set_wire_conductivity(brass_conductivity) + + nec.geometry_complete(ground_plane=False) + + # The 6th tag is the smallest tag is the source element + for dipole in range(0, dipoles_count - 1): + src_tag = int(1 + dipole) # NEC indexing + src_seg = dipole_center_segs[src_tag] + + dst_tag = src_tag + 1 + dst_seg = dipole_center_segs[dst_tag] + + nec.transmission_line((src_tag, src_seg), (dst_tag, dst_seg), tl_impedance, crossed_line=True) + + smallest_dipole_tag = dipoles_count # Again, start at 1 + + nec.voltage_excitation(wire_tag=smallest_dipole_tag, segment_nr=dipole_center_segs[smallest_dipole_tag], voltage=1.0) + + return nec + +start = 2300 +stop = 5900 +count = stop - start + + +def get_gain_swr_range(l_1, x_1, tau, start=start, stop=stop, step=10): + gains_db = [] + frequencies = [] + vswrs = [] + for freq in range(start, stop + 1, step): + nec = geometry_logperiodic(l_1, x_1, tau) + nec.set_frequency(freq) # TODO: ensure that we don't need to re-generate this! + nec.radiation_pattern(thetas=Range(90, 90, count=1), phis=Range(180,180,count=1)) + + rp = nec.context.get_radiation_pattern(0) + ipt = nec.get_input_parameters(0) + z = ipt.get_impedance() + + # Gains are in decibels + gains_db.append(rp.get_gain()[0]) + vswrs.append(vswr(z, system_impedance)) + frequencies.append(ipt.get_frequency()) + + return frequencies, gains_db, vswrs + +def create_optimization_target(): + def target(args): + l_1, x_1, tau = args + if l_1 <= 0 or x_1 <= 0 or tau <= 0: + return float('inf') + + try: + result = 0 + + vswr_score = 0 + gains_score = 0 + + for range_low, range_high in [ (2400, 2500), (5725, 5875) ]: + freqs, gains, vswrs = get_gain_swr_range(l_1, x_1, tau, start=range_low, stop=range_high) + + for gain in gains: + gains_score += gain + for vswr in vswrs: + if vswr >= 1.8: + vswr = np.exp(vswr) # a penalty :) + vswr_score += vswr + + # VSWR should minimal in both bands, gains maximal: + result = vswr_score - gains_score + + except: + print("Caught exception") + return float('inf') + + print(result) + + return result + return target + + +def simulate_and_get_impedance(nec): + nec.set_frequency(design_freq_mhz) + + nec.xq_card(0) + + index = 0 + return nec.get_input_parameters(index).get_impedance() + +system_impedance = 50 # This makes it a bit harder to optimize, given the 75 Ohm TLs, which is good for this excercise of course... + +# (2.4 GHz to 2.5 GHz) and the 5.8 GHz ISM band (5.725 GHz to 5.875 GHz) + +design_freq_mhz = 2450 # The center of the first range +wavelength = 299792e3/(design_freq_mhz*1000000) + +majorLocator = mpl.ticker.MultipleLocator(10) +majorFormatter = mpl.ticker.FormatStrFormatter('%d') +minorLocator = mpl.ticker.MultipleLocator(1) +minorFormatter = mpl.ticker.FormatStrFormatter('%d') + +def draw_frequencie_ranges(ax): + ax.axvline(x=2400, color='red', linewidth=1) + ax.axvline(x=2500, color='red', linewidth=1) + ax.axvline(x=5725, color='red', linewidth=1) + ax.axvline(x=5875, color='red', linewidth=1) + +def show_report(l1, x1, tau): + nec = geometry_logperiodic(l1, x1, tau) + + z = simulate_and_get_impedance(nec) + + print("Initial impedance: (%6.1f,%+6.1fI) Ohms" % (z.real, z.imag)) + print("VSWR @ 50 Ohm is %6.6f" % vswr(z, 50)) + + nec = geometry_logperiodic(l1, x1, tau) + + freqs, gains, vswrs = get_gain_swr_range(l1, x1, tau, step=5) + + freqs = np.array(freqs) / 1000000 # In MHz + + ax = plt.subplot(111) + ax.plot(freqs, gains) + draw_frequencie_ranges(ax) + + ax.set_title("Gains of a 5-element log-periodic antenna") + ax.set_xlabel("Frequency (MHz)") + ax.set_ylabel("Gain") + + ax.yaxis.set_major_locator(majorLocator) + ax.yaxis.set_major_formatter(majorFormatter) + + ax.yaxis.set_minor_locator(minorLocator) + ax.yaxis.set_minor_formatter(minorFormatter) + + ax.yaxis.grid(b=True, which='minor', color='0.75', linestyle='-') + + plt.show() + + ax = plt.subplot(111) + ax.plot(freqs, vswrs) + draw_frequencie_ranges(ax) + + ax.set_yscale("log") + ax.set_title("VSWR of a 6-element log-periodic antenna @ 50 Ohm impedance") + ax.set_xlabel("Frequency (MHz)") + ax.set_ylabel("VSWR") + + ax.yaxis.set_major_locator(majorLocator) + ax.yaxis.set_major_formatter(majorFormatter) + ax.yaxis.set_minor_locator(minorLocator) + ax.yaxis.set_minor_formatter(minorFormatter) + + ax.yaxis.grid(b=True, which='minor', color='0.75', linestyle='-') + plt.show() + + +if (__name__ == '__main__'): + initial_l1 = wavelength / 2 + initial_x1 = wavelength / 2 + initial_tau = 0.8 + + print("Wavelength is %0.4fm, initial length is %0.4fm" % (wavelength, initial_l1)) + + print("Unoptimized antenna...") + show_report(initial_l1, initial_x1, initial_tau) + + print("Optimizing antenna...") + target = create_optimization_target() + + # Optimize local minimum only with gradient desce + #optimized_result = scipy.optimize.minimize(target, np.array([initial_l1, initial_x1, initial_tau]), method='Nelder-Mead') + + # Use differential evolution: + minimizer_kwargs = dict(method='Nelder-Mead') + bounds = [ (0.01, 0.2), (0.01, 0.2), (0.7, 0.9) ] + optimized_result = scipy.optimize.differential_evolution(target, bounds, seed=42, disp=True, popsize=20) + + # Basin hopping isn't so good, but could also have been an option: + #optimized_result = scipy.optimize.basinhopping(target, np.array([initial_l1, initial_x1, initial_tau]), minimizer_kwargs=minimizer_kwargs, niter=5, stepsize=0.015, T=2.0, disp=True) + + print("Optimized antenna...") + optimized_l1, optimized_x1, optimized_tau = optimized_result.x[0], optimized_result.x[1], optimized_result.x[2] + show_report(optimized_l1, optimized_x1, optimized_tau) + diff --git a/PyNEC/example/monopole.py b/PyNEC/example/monopole.py new file mode 100644 index 0000000..cc0eb4b --- /dev/null +++ b/PyNEC/example/monopole.py @@ -0,0 +1,48 @@ +# +# Simple vertical monopole antenna simulation using python-necpp +# pip install PyNEC +# +from PyNEC import * + +from context_clean import * + +import math + +def geometry(freq, base, length): + conductivity = 1.45e6 # Stainless steel + ground_conductivity = 0.002 + ground_dielectric = 10 + + wavelength = 3e8/(1e6*freq) + n_seg = int(math.ceil(50*length/wavelength)) + + nec = context_clean(nec_context()) + + geo = nec.get_geometry() + geo.wire(1, n_seg, 0, 0, base, 0, 0, base+length, 0.002, 1.0, 1.0) + nec.geometry_complete(1) + + nec.set_all_wires_conductivity(conductivity) + + nec.set_finite_ground(ground_dielectric, ground_conductivity) + nec.set_frequency(freq) + + # Voltage excitation one third of the way along the wire + nec.voltage_excitation(wire_tag=1, segment_nr=int(n_seg/3), voltage=1.0) + + return nec + +def impedance(freq, base, length): + nec = geometry(freq, base, length) + nec.xq_card(0) # Execute simulation + + index = 0 + + ipt = nec.get_input_parameters(index) + z = ipt.get_impedance() + + return z + +if (__name__ == '__main__'): + z = impedance(freq = 134.5, base = 0.5, length = 4.0) + print("Impedance at base=%0.2f, length=%0.2f : (%6.1f,%+6.1fI) Ohms" % (0.5, 4.0, z.real, z.imag)) diff --git a/PyNEC/example/monopole_realistic_ground_plane.py b/PyNEC/example/monopole_realistic_ground_plane.py new file mode 100644 index 0000000..8c18713 --- /dev/null +++ b/PyNEC/example/monopole_realistic_ground_plane.py @@ -0,0 +1,227 @@ +import numpy as np +import scipy.optimize +import matplotlib.pyplot as plt +import matplotlib as mpl + + +from PyNEC import * +from antenna_util import * + +from context_clean import * + +import math + +brass_conductivity = 15600000 # mhos + +""" Optimize and plot the gains/VSWR of a simple monopole antenna, that has some brass wires added that act as ground. Visualises the path in the search space explored + by the minimization algorithm. + Inspired by an excercise for a course on antennas. The constraints are thus not really ideal I think... """ + +# TODO: this probably could also be done by the GN/GD cards, but am I allowed? +def add_ground_screen(geo, start_tag, length): + # The ground is modeled 6 radial wires (brass, radius 1 mm), equally spaced by 2pi/6, and their initial length is d = 2 cm + + src = np.array([0, 0, 0]) + radius = 0.001 + + for i in range(0, 6): + angle = i * (2*np.pi/6.0) + dst = np.array([length * np.cos(angle), length * np.sin(angle), 0]) + # TODO: nr_segments (actually, more TODO: if nr_segments>=9, the geometry is invalid with this wire radius!) + geo.wire(tag_id=start_tag+i, nr_segments=5, src=src, dst=dst, radius=radius) # TODO: nr_segments + +def geometry_monopole_ground(freq_mhz, monopole_length, ground_wire_length, nr_segments): + wire_radius = 0.0005 # 0.5 mm + + nec = context_clean(nec_context()) + nec.set_extended_thin_wire_kernel(True) + + geo = geometry_clean(nec.get_geometry()) + + bottom = np.array([0, 0, 0]) + top = np.array([0, 0, monopole_length]) + + wire_tag = 1 + geo.wire(tag_id=wire_tag, nr_segments=nr_segments, src=bottom, dst=top, radius=wire_radius) + + add_ground_screen(geo, start_tag=2, length=ground_wire_length) + + # Everything is in brass + nec.set_wire_conductivity(brass_conductivity) + + nec.geometry_complete(ground_plane=False) # We added our own 'ground plane' + + nec.voltage_excitation(wire_tag=wire_tag, segment_nr=1, voltage=1.0) + + return nec + +def simulate_and_get_impedance(nec): + nec.set_frequency(design_freq_mhz) + + nec.xq_card(0) + + index = 0 + return nec.get_input_parameters(index).get_impedance() + +# TODO: perhaps the length <= 0 can be added through additional constraints to minimize? + +design_freq_mhz = 2595 +lte_high_band = [2500, 2690] +system_impedance = 50 + +sampled_monopole_lenths = [] +sampled_ground_wire_lenths = [] +sampled_results = [] + +def create_optimization_target(freq_mhz, nr_segments): + def target(args): + monopole_length, ground_wire_length = args[0], args[1] + if monopole_length <= 0 or ground_wire_length <= 0: + return float('inf') + + result = 0 + # VSWR should be < 2 in the lte_high_band + # So let's just sum (for now, TODO) the surplus for all the VSWRs that exceed it: + low = lte_high_band[0] + high = lte_high_band[1] + count = high-low + + try: + nec = geometry_monopole_ground(design_freq_mhz, monopole_length, ground_wire_length, nr_segments) + nec.set_frequencies_linear(low, high, count=count) + + nec.xq_card(0) # Execute simulation + except: + print("Caught exception") + return float('inf') + + for idx in range(0, count): + ipt = nec.get_input_parameters(idx) + z = ipt.get_impedance() + s = vswr(z, system_impedance) + if s > 2: + result += s - 2 + + # And then maximize the gain in the horizontal plane (theta = pi/2 (90)) + # TODO: might this be possible in one step? + gains_db = [] + for freq in range(low, high+1): + nec = geometry_monopole_ground(design_freq_mhz, monopole_length, ground_wire_length, nr_segments) + + nec.set_frequency(freq) + nec.radiation_pattern(thetas=Range(-90, -90, count=1), phis=Range(90,90,count=1)) + rp = nec.context.get_radiation_pattern(0) + + gains_db.append(rp.get_gain()[0][0]) + + #print gains_db + result -= np.exp(max(gains_db)) # maximize gain, hence the minus + + global sampled_monopole_lenths, sampled_ground_wire_lenths, sampled_results + sampled_monopole_lenths.append(monopole_length) + sampled_ground_wire_lenths.append(ground_wire_length) + sampled_results.append(result) + + print(result) + + return result + return target + +if (__name__ == '__main__'): + + wavelength = 299792e3/(design_freq_mhz*1000000) + + initial_length = wavelength / 4 # quarter-wavelength monopole + + print("Wavelength is %0.4fm, initial length is %0.4fm" % (wavelength, initial_length)) + + nr_segments = 15 # int(math.ceil(50*initial_length/wavelength)) + #print nr_segments + + ground_wire_length = 0.02 + z = simulate_and_get_impedance(geometry_monopole_ground(design_freq_mhz, initial_length, ground_wire_length, nr_segments)) + + print("Initial impedance: (%6.1f,%+6.1fI) Ohms" % (z.real, z.imag)) + print("VSWR @ 50 Ohm is %6.6f" % vswr(z, 50)) + + target = create_optimization_target(design_freq_mhz, nr_segments) + optimized_result = scipy.optimize.minimize(target, np.array([initial_length, ground_wire_length]), method='Nelder-Mead') + + optimized_length, optimized_ground_wire_length = optimized_result.x[0], optimized_result.x[1] + + geo_opt = geometry_monopole_ground(design_freq_mhz, optimized_length, optimized_ground_wire_length, nr_segments) + z = simulate_and_get_impedance(geo_opt) + + print("Optimized length %6.6f m and ground screen radials of length %6.6f m, which gives an impedance of: (%6.4f,%+6.4fI) Ohms" % (optimized_length, optimized_ground_wire_length, z.real, z.imag)) + print("Mismatch @ 50 Ohm is %6.6f" % mismatch(z, 50)) + print("VSWR @ 50 Ohm is %6.6f" % vswr(z, 50)) + + geo_opt = geometry_monopole_ground(design_freq_mhz, optimized_length, optimized_ground_wire_length, nr_segments) + geo_opt.set_frequency(design_freq_mhz) + geo_opt.radiation_pattern(thetas=Range(-90, 90, count=180), phis=Range(0,0,count=1)) + + #get the radiation_pattern + rp = geo_opt.context.get_radiation_pattern(0) + + # Gains are in decibels + gains_db = rp.get_gain()[:,0] # Is an array of theta,phi -> gain. In this case we only have one phi + thetas = rp.get_theta_angles() * 3.1415 / 180.0 + phis = rp.get_phi_angles() * 3.1415 / 180.0 + + max_idx = gains_db.argmax() + max_gain = gains_db[max_idx] + max_theta = thetas[max_idx] + #print gains_db + print("Maximal gain is %2.2f dBi, at an angle of %2.2f" % (max_gain, max_theta * 180.0 / np.pi)) + + # Plot stuff + + ax = plt.subplot(111, polar=True) + + ax.plot(thetas, gains_db, color='r', linewidth=3) + ax.set_xticks(np.pi/180. * np.linspace(180, -180, 8, endpoint=False)) + ax.set_theta_zero_location("N") + ax.set_rlim((-24.0, 9.0)) # TODO: automate. TODO: 4nec2 cheats and makes the lowest points (-999) the same as the lowest non-999 point :) + ax.set_rticks(np.linspace(-24, 9, 10, endpoint=False)) + ax.grid(True) + + ax.set_title("Gain pattern in the vertical plane", va='bottom') + plt.show() + + + low = lte_high_band[0] + high = lte_high_band[1] + count = high-low + + # Reset the geometry, so that there is no spurious FR card left. (TODO this should really be not necessary) + geo_opt = geometry_monopole_ground(design_freq_mhz, optimized_length, optimized_ground_wire_length, nr_segments) + geo_opt.set_frequencies_linear(low, high, count=count) + geo_opt.xq_card(0) # Execute simulation + + freqs = [] + vswrs = [] + for idx in range(0, count): + ipt = geo_opt.get_input_parameters(idx) + z = ipt.get_impedance() + + freqs.append(ipt.get_frequency() / 1000000) + vswrs.append(vswr(z, system_impedance)) + + ax = plt.subplot(111) + ax.plot(freqs, vswrs) + ax.set_xlabel("Frequency (MHz)") + ax.set_ylabel("VSWR") + ax.grid(True) + plt.show() + + ax = plt.subplot(111) + #print sampled_monopole_lenths + #print sampled_ground_wire_lenths + sampled_results = np.log(4 + np.array(sampled_results)) + #print sampled_results + norm = mpl.colors.Normalize(vmin=min(sampled_results), vmax=max(sampled_results)) + plt.scatter(sampled_monopole_lenths, sampled_ground_wire_lenths, c=sampled_results, cmap=mpl.cm.cool, norm=norm, edgecolors='None', alpha=0.75) + ax.set_title("Optimization path") + plt.colorbar() + plt.show() + diff --git a/PyNEC/example/optimized.py b/PyNEC/example/optimized.py new file mode 100644 index 0000000..3d8d33f --- /dev/null +++ b/PyNEC/example/optimized.py @@ -0,0 +1,61 @@ +# +# Automatically tune antenna +# +import argparse +import scipy.optimize +import numpy as np + +import monopole +from antenna_util import reflection_coefficient + +# A function that will be minimized when the impedance is 50 Ohms +# We convert the height and antenna length to positive +# numbers using exp. because otherwise the antenna will lie +# below ground and cause an error in simulation. +def target(x): + global freq, target_impedance + base_height = np.exp(x[0]) # Make it positive + length = np.exp(x[1]) # Make it positive + if (length > 10.0): + return 100 + try: + z = monopole.impedance(freq, base_height, length) + return reflection_coefficient(z, z0=target_impedance) + except RuntimeError as re: + return 100 + +def print_result(x, f, accepted): + log_base, log_length = x + base_height = np.exp(log_base) + length = np.exp(log_length) + + if accepted: + print("Optimium base_height=%fm, h=%fm, impedance=%s Ohms" % \ + (base_height, length, monopole.impedance(freq, base_height, length))) + else: + print("Local_minimum=%fm, h=%fm, impedance=%s Ohms" % \ + (base_height, length, monopole.impedance(freq, base_height, length))) + +if __name__=="__main__": + parser = argparse.ArgumentParser(description='Optimize a monopole antenna.') + parser.add_argument('--target-impedance', type=float, default=50.0, help='Target for the optimized impedance') + parser.add_argument('--basinhopping', action="store_true", help='Use basinhopping') + args = parser.parse_args() + + # Starting value + freq = 134.5 + x0 = [-2.0, 1.0] + target_impedance = args.target_impedance + + # Carry out the minimization + + if args.basinhopping: + result = scipy.optimize.basinhopping(target, x0, disp=True, T=1.0, niter_success=10) + else: + result = scipy.optimize.minimize(target, x0, method='Nelder-Mead') + + print("") + print("***********************************************************************") + print("* OPTIMIZATION COMPLETED *") + print("***********************************************************************") + print_result(result.x, None, True) diff --git a/PyNEC/example/radiation_pattern.py b/PyNEC/example/radiation_pattern.py new file mode 100644 index 0000000..39a3084 --- /dev/null +++ b/PyNEC/example/radiation_pattern.py @@ -0,0 +1,53 @@ +# +# Simple vertical monopole antenna simulation using python-necpp +# pip install necpp +# +from PyNEC import * + +import math + +def geometry(freq, base, length): + conductivity = 1.45e6 # Stainless steel + ground_conductivity = 0.002 + ground_dielectric = 10 + + wavelength = 3e8/(1e6*freq) + n_seg = int(math.ceil(50*length/wavelength)) + + #nec = context_clean(nec_context()) + nec = nec_context() + + geo = nec.get_geometry() + geo.wire(1, n_seg, 0, 0, base, 0, 0, base+length, 0.002, 1.0, 1.0) + nec.geometry_complete(1) + + nec.ld_card(5, 0, 0, 0, conductivity, 0.0, 0.0) + nec.gn_card(0, 0, ground_dielectric, ground_conductivity, 0, 0, 0, 0) + nec.fr_card(0, 1, freq, 0) + + # Voltage excitation one third of the way along the wire + nec.ex_card(0, 0, int(n_seg/3), 0, 1.0, 0, 0, 0, 0, 0) + + return nec + +nec = geometry(freq=123.4, base=0.5, length=4.0) +nec.rp_card(calc_mode=0, n_theta=30, n_phi=30, output_format=0, normalization=0, D=0, A=0, theta0=0, delta_theta=10, phi0=0, delta_phi=5, radial_distance=0, gain_norm=0) +nec.xq_card(0) # Execute simulation + +ipt = nec.get_input_parameters(0) +z = ipt.get_impedance() +print(("Impedance is {}".format(z))) + +rpt = nec.get_radiation_pattern(0) + +complex_e_field = rpt.get_e_theta() +e = complex_e_field.reshape((30,30)) + +print((complex_e_field.size)) + +for t in range(30): + for p in range(30): + pass + print(e[t, p]) + +print(dir(rpt)) diff --git a/PyNEC/example/test_ne_nh.py b/PyNEC/example/test_ne_nh.py index 4a13d21..2b08584 100644 --- a/PyNEC/example/test_ne_nh.py +++ b/PyNEC/example/test_ne_nh.py @@ -53,4 +53,4 @@ ne = context.get_near_field_pattern(0) nh = context.get_near_field_pattern(1) -print ne \ No newline at end of file +print(ne) \ No newline at end of file diff --git a/PyNEC/interface_files/c_geometry.i b/PyNEC/interface_files/c_geometry.i index 2008f7b..8c2a0b2 100644 --- a/PyNEC/interface_files/c_geometry.i +++ b/PyNEC/interface_files/c_geometry.i @@ -255,18 +255,13 @@ public: \param ax3 The x_coordinate of corner 3. \param ay3 The y_coordinate of corner 3. \param az3 The z_coordinate of corner 3. - - \param ax4 The x_coordinate of corner 4. - \param ay4 The x_coordinate of corner 4. - \param az4 The x_coordinate of corner 4. */ void multiple_patch( int nx, int ny, nec_float ax1, nec_float ay1, nec_float az1, nec_float ax2, nec_float ay2, nec_float az2, - nec_float ax3, nec_float ay3, nec_float az3, - nec_float ax4, nec_float ay4, nec_float az4 ) + nec_float ax3, nec_float ay3, nec_float az3 ) { - return self->patch( nx, ny, ax1, ay1, az1, ax2, ay2, az2, ax3, ay3, az3, ax4, ay4, az4 ); + return self->patch( nx, ny, ax1, ay1, az1, ax2, ay2, az2, ax3, ay3, az3, 0, 0, 0 ); } } diff --git a/PyNEC/interface_files/nec_context.i b/PyNEC/interface_files/nec_context.i index bd1fb23..a010769 100644 --- a/PyNEC/interface_files/nec_context.i +++ b/PyNEC/interface_files/nec_context.i @@ -12,7 +12,39 @@ public: c_geometry* get_geometry(); + /*! \brief Get the maximum gain in dB. + This function requires a previous rp_card() method to have been called (with gain normalization requested) + + \return The maximum gain in dB or -999.0 if no radiation pattern had been previously requested. + */ + double get_gain(int freq_index, int theta_index, int phi_index); + + double get_gain_max(int freq_index); + double get_gain_min(int freq_index); + double get_gain_mean(int freq_index); + double get_gain_sd(int freq_index); + + /********************** RHCP ********************************/ + double get_gain_rhcp_max(int freq_index); + double get_gain_rhcp_min(int freq_index); + double get_gain_rhcp_mean(int freq_index); + double get_gain_rhcp_sd(int freq_index); + + /********************** LHCP ********************************/ + double get_gain_lhcp_max(int freq_index); + double get_gain_lhcp_min(int freq_index); + double get_gain_lhcp_mean(int freq_index); + double get_gain_lhcp_sd(int freq_index); + + /****************** IMPEDANCE CHARACTERISTICS *********************/ + + /*! \brief Impedance: Real Part */ + double get_impedance_real(int freq_index); + /*! \brief Impedance: Imaginary Part */ + double get_impedance_imag(int freq_index); + + /*! Get the result antenna_input_parameters specified by index \param index The index of the requested result. diff --git a/PyNEC/pyproject.toml b/PyNEC/pyproject.toml new file mode 100644 index 0000000..4973c84 --- /dev/null +++ b/PyNEC/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel", "numpy"] +build-backend = "setuptools.build_meta" diff --git a/PyNEC/setup.py b/PyNEC/setup.py index f11ea4d..bc33761 100644 --- a/PyNEC/setup.py +++ b/PyNEC/setup.py @@ -6,63 +6,79 @@ Author Tim Molteno. tim@molteno.net """ -from distutils.core import setup, Extension import distutils.sysconfig -from glob import glob import os -import numpy as np +from glob import glob +import numpy as np +import setuptools # Remove silly flags from the compilation to avoid warnings. -cfg_vars = distutils.sysconfig.get_config_vars() -for key, value in cfg_vars.items(): - if type(value) == str: - cfg_vars[key] = value.replace("-Wstrict-prototypes", "") +# cfg_vars = distutils.sysconfig.get_config_vars() +# for key, value in cfg_vars.items(): +# if type(value) == str: +# cfg_vars[key] = value.replace("-Wstrict-prototypes", "") -# Generate a list of the sources. +# Generate a list of the sources. nec_sources = [] -nec_sources.extend([fn for fn in glob('../necpp_src/src/*.cpp') - if not os.path.basename(fn).endswith('_tb.cpp') - if not os.path.basename(fn).startswith('net_solve.cpp') - if not os.path.basename(fn).startswith('nec2cpp.cpp') - if not os.path.basename(fn).startswith('necDiff.cpp')]) +nec_sources.extend( + [ + fn + for fn in glob("necpp_src/src/*.cpp") + if not os.path.basename(fn).endswith("_tb.cpp") + if not os.path.basename(fn).startswith("net_solve.cpp") + if not os.path.basename(fn).startswith("nec2cpp.cpp") + if not os.path.basename(fn).startswith("necDiff.cpp") + ] +) nec_sources.extend(glob("PyNEC_wrap.cxx")) nec_headers = [] -nec_headers.extend(glob("../necpp_src/src/*.h")) -nec_headers.extend(glob("../necpp_src/config.h")) +nec_headers.extend(glob("necpp_src/src/*.h")) +nec_headers.extend(glob("necpp_src/config.h")) # At the moment, the config.h file is needed, and this should be generated from the ./configure # command in the parent directory. Use ./configure --without-lapack to avoid dependance on LAPACK # -necpp_module = Extension('_PyNEC', +necpp_module = setuptools.Extension( + "_PyNEC", sources=nec_sources, - include_dirs=[np.get_include(), '../necpp_src/src', '../necpp_src/'], - extra_compile_args = ['-fPIC'], - extra_link_args = ['-lstdc++'], + include_dirs=[np.get_include(), "necpp_src/src", "necpp_src/", "necpp_src/win32/"], + extra_compile_args=["-fPIC"], + extra_link_args=["-lstdc++"], depends=nec_headers, - define_macros=[('BUILD_PYTHON', '1'), ('NPY_NO_DEPRECATED_API','NPY_1_7_API_VERSION')] - ) - + define_macros=[ + ("BUILD_PYTHON", "1"), + ("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION"), + ], +) +with open("README.md", "r") as fh: + long_description = fh.read() -setup (name = 'PyNEC', - version = '1.7.0.3', - author = "Tim Molteno", - author_email = "tim@physics.otago.ac.nz", - url = "http://github.com/tmolteno/necpp", - keywords = "nec2 nec2++ antenna electromagnetism radio", - description = "Python Antenna Simulation Module (nec2++) object-oriented interface", - data_files=[('examples', ['example/test_rp.py'])], - ext_modules = [necpp_module], - requires = ['numpy'], - py_modules = ["PyNEC"], - license='GPLv2', - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Topic :: Scientific/Engineering", - "Topic :: Communications :: Ham Radio", - "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", - "Intended Audience :: Science/Research"] - ) +setuptools.setup( + name="PyNEC", + version="1.7.4", + author="Tim Molteno", + author_email="tim@physics.otago.ac.nz", + url="http://github.com/tmolteno/python-necpp", + keywords="nec2 nec2++ antenna electromagnetism radio", + description="Python Antenna Simulation Module (nec2++) object-oriented interface", + long_description=long_description, + long_description_content_type="text/markdown", + include_package_data=True, + data_files=[("examples", ["example/test_rp.py"])], + ext_modules=[necpp_module], + requires=["numpy"], + py_modules=["PyNEC"], + license="GPLv2", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Topic :: Scientific/Engineering", + "Topic :: Communications :: Ham Radio", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Programming Language :: Python :: 3", + "Intended Audience :: Science/Research", + ], +) diff --git a/PyNEC/tests/Makefile b/PyNEC/tests/Makefile index b67626b..a3792fa 100644 --- a/PyNEC/tests/Makefile +++ b/PyNEC/tests/Makefile @@ -1,2 +1,2 @@ test: - python -m unittest discover + python3 -m unittest discover diff --git a/PyNEC/tests/test_get_gain.py b/PyNEC/tests/test_get_gain.py index dd1a4a1..054e940 100644 --- a/PyNEC/tests/test_get_gain.py +++ b/PyNEC/tests/test_get_gain.py @@ -1,4 +1,4 @@ -from PyNEC import * +import PyNEC import unittest @@ -22,15 +22,18 @@ def test_example4(self): RP 0 10 4 1001 0. 0. 10. 30. EN ''' - nec = nec_create() - nec.sp_card(0, 0.1, 0.05, 0.05, 0.0, 0.0, 0.01) - nec.sp_card(0, .05, .1, .05, 0.0, 90.0, 0.01) - nec.gx_card(0, 110) - nec.sp_card(0, 0.0, 0.0, 0.1, 90.0, 0.0, 0.04) + nec= PyNEC.nec_context() + + geo = nec.get_geometry() + + geo.sp_card(0, 0.1, 0.05, 0.05, 0.0, 0.0, 0.01) + geo.sp_card(0, .05, .1, .05, 0.0, 90.0, 0.01) + geo.gx_card(0, 110) + geo.sp_card(0, 0.0, 0.0, 0.1, 90.0, 0.0, 0.04) - nec.wire(1, 4, 0., 0.0, 0.1, 0.0, 0.0, 0.3, .001, 1.0, 1.0) - nec.wire(2, 2, 0., 0.0, 0.3, 0.15, 0.0, 0.3, .001, 1.0, 1.0) - nec.wire(3, 2, 0., 0.0, 0.3, -.15, 0.0, 0.3, .001, 1.0, 1.0) + geo.wire(1, 4, 0., 0.0, 0.1, 0.0, 0.0, 0.3, .001, 1.0, 1.0) + geo.wire(2, 2, 0., 0.0, 0.3, 0.15, 0.0, 0.3, .001, 1.0, 1.0) + geo.wire(3, 2, 0., 0.0, 0.3, -.15, 0.0, 0.3, .001, 1.0, 1.0) nec.geometry_complete(1) nec.gn_card(1, 0, 0, 0, 0, 0, 0, 0) @@ -38,18 +41,16 @@ def test_example4(self): nec.ex_card(0, 1, 1, 0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0) nec.rp_card(0,10,4,1,0,0,1,0.0,0.0,10.0,30.0, 0, 0) - self.assertAlmostEqual(nec_gain_max(nec,0),5.076,3) + self.assertAlmostEqual(nec.get_gain_max(0),5.076,3) gmax = -999.0 for theta_index in range(0,10): for phi_index in range(0,4): - g = nec_gain(nec,0,theta_index, phi_index) + g = nec.get_gain(0,theta_index, phi_index) gmax = max(g, gmax) - self.assertAlmostEqual(gmax, nec_gain_max(nec,0), 5 ) - - nec_delete(nec) + self.assertAlmostEqual(gmax, nec.get_gain_max(0), 5 ) if __name__ == '__main__': diff --git a/README.md b/README.md index 1c997e4..9e20312 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,12 @@ # python-necpp: Antenna simulation in python -This module allows you to do antenna simulations in Python using the nec2++ antenna -simulation package. This is a wrapper using SWIG of the C interface, so the syntax -is quite simple. Have a look at the file necpp_src/example/test.py, for an example of how this -library can be used. +This repository contains two wrappers for the [http://github.com/tmolteno/necpp nec2++] antenna simulation package: -Tim Molteno. tim@physics.otago.ac.nz +* necpp/ contains a wrapper using SWIG of the C interface (Python module name: necpp). +* PyNEC/ contains a wrapper of the C++ interfaces (Python module name: PyNEC). The example/ directory furthermore contains some nicer, more readable Python wrappers that make toying around with NEC a less painful experience. -## NEWS +Both are based on Tim Molteno (tim@physics.otago.ac.nz)'s code with major cleanup by Bart Coppens. -* Version 1.7.0 includes support for getting elements of radiation patterns. At the moment - this is just through the function nec_get_gain(). -* Version 1.7.0.3 includes nec_medium_parameters(). You could simulate an antenna in seawater! +## TODOs - -## Install - -As of version 1.6.1.2 swig is no longer required for installation. Simply use PIP as -follows: - - pip install necpp - -## Documentation - -Try help(necpp) to list the available functions. The functions available are documented in the C-style API of nec2++. -This is [available here](http://tmolteno.github.io/necpp/libnecpp_8h.html) - -## Using - -The following code calculates the impedance of a simple vertical monopole antenna -over a perfect ground. - - import necpp - - def handle_nec(result): - if (result != 0): - print necpp.nec_error_message() - - def impedance(frequency, z0, height): - - nec = necpp.nec_create() - handle_nec(necpp.nec_wire(nec, 1, 17, 0, 0, z0, 0, 0, z0+height, 0.1, 1, 1)) - handle_nec(necpp.nec_geometry_complete(nec, 1, 0)) - handle_nec(necpp.nec_gn_card(nec, 1, 0, 0, 0, 0, 0, 0, 0)) - handle_nec(necpp.nec_fr_card(nec, 0, 1, frequency, 0)) - handle_nec(necpp.nec_ex_card(nec, 0, 0, 1, 0, 1.0, 0, 0, 0, 0, 0)) - handle_nec(necpp.nec_rp_card(nec, 0, 90, 1, 0,5,0,0, 0, 90, 1, 0, 0, 0)) - result_index = 0 - - z = complex(necpp.nec_impedance_real(nec,result_index), - necpp.nec_impedance_imag(nec,result_index)) - - necpp.nec_delete(nec) - return z - - if (__name__ == 'main'): - z = impedance(frequency = 34.5, z0 = 0.5, height = 4.0) - print "Impedance \t(%6.1f,%+6.1fI) Ohms" % (z.real, z.imag) - -## More Information - -Have a look at [http://github.com/tmolteno/necpp] for more information on using nec2++. +The cleaner API should really be **ported to C++**, so the clean wrappers get automatically generated, and C++ can use the same cleaner interface. But for now, I'm happy with the Python wrapper :) diff --git a/build_wheels.sh b/build_wheels.sh new file mode 100755 index 0000000..d19092b --- /dev/null +++ b/build_wheels.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e -x + +# Install a system package required by our library +# yum install -y atlas-devel + +# Compile wheels +for PYBIN in /opt/python/*/bin; do + "${PYBIN}/pip" install -r /io/dev-requirements.txt + "${PYBIN}/pip" wheel -e /io/PyNEC/ -w wheelhouse/ +done + +# Bundle external shared libraries into the wheels +for whl in wheelhouse/*.whl; do + auditwheel repair "$whl" --plat $PLAT -w /io/wheelhouse/ +done + +# Install packages and test +for PYBIN in /opt/python/*/bin/; do + "${PYBIN}/pip" install PyNEC --no-index -f /io/wheelhouse + (cd "$HOME"; "${PYBIN}/nosetests" pymanylinuxdemo) +done diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1 @@ +numpy diff --git a/necpp/.gitignore b/necpp/.gitignore new file mode 100644 index 0000000..d66c9ee --- /dev/null +++ b/necpp/.gitignore @@ -0,0 +1,3 @@ +necpp_wrap.c +necpp_src +necpp.py diff --git a/INSTALL.md b/necpp/INSTALL.md similarity index 86% rename from INSTALL.md rename to necpp/INSTALL.md index 18e1ba9..499cc1d 100644 --- a/INSTALL.md +++ b/necpp/INSTALL.md @@ -4,7 +4,7 @@ PyPI module for nec2++ This module allows you to do antenna simulations in Python using the nec2++ antenna simulation package. This is a wrapper using SWIG of the C interface, so the syntax is quite simple. Have a look at the file test.py, for an example of how this -library can be used. +library can be used. Other examples are in the 'examples' directory. ### Author @@ -30,6 +30,8 @@ To update the submodule to the latest necpp ### Converting from MarkDown sudo aptitude install pandoc swig + + pandoc --to=rst README.md > README.txt ### Testing @@ -43,4 +45,4 @@ This will run SWIG a source distribution tarball http://peterdowns.com/posts/first-time-with-pypi.html - python setup.py sdist upload -r pypitest \ No newline at end of file + python setup.py sdist upload -r pypitest diff --git a/necpp/LICENCE.txt b/necpp/LICENCE.txt new file mode 100644 index 0000000..d6a9326 --- /dev/null +++ b/necpp/LICENCE.txt @@ -0,0 +1,340 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + diff --git a/MANIFEST.in b/necpp/MANIFEST.in similarity index 57% rename from MANIFEST.in rename to necpp/MANIFEST.in index 4092210..bb7bd4c 100644 --- a/MANIFEST.in +++ b/necpp/MANIFEST.in @@ -1,7 +1,7 @@ include LICENCE.txt include necpp_src/src/*.h - include necpp_src/config.h -include necpp_src/example/test.py \ No newline at end of file +include necpp_src/example/test.py +include example/*.py diff --git a/necpp/Makefile b/necpp/Makefile new file mode 100644 index 0000000..9f1f3e5 --- /dev/null +++ b/necpp/Makefile @@ -0,0 +1,20 @@ +build: + sh build.sh + python3 setup.py sdist + #python3 setup.py bdist_wheel + +clean: + rm -rf necpp_src + rm -rf build + rm -rf dist + +test-upload: + python3 -m pip install --user --upgrade twine + + python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* + +upload: + python3 -m twine upload dist/* + +test-install: + python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps necpp --upgrade diff --git a/necpp/README.md b/necpp/README.md new file mode 100644 index 0000000..28ad3c9 --- /dev/null +++ b/necpp/README.md @@ -0,0 +1,67 @@ +# python-necpp: Antenna simulation in python + +This module allows you to do antenna simulations in Python using the nec2++ antenna +simulation package. + +This is a wrapper using SWIG of the C interface, so the syntax +is quite simple. Have a look at the file necpp_src/example/test.py, for an example of how this +library can be used. + +Tim Molteno. tim@physics.otago.ac.nz + +## NEWS + +* Version 1.7.3 Includes Python3 support. Also some bug fixes and updating nec++ to the + latest version. +* Version 1.7.0.3 includes nec_medium_parameters(). You could simulate an antenna in seawater! +* Version 1.7.0 includes support for getting elements of radiation patterns. At the moment + this is just through the function nec_get_gain(). + + +## Install + +As of version 1.6.1.2 swig is no longer required for installation. Simply use PIP as +follows: + + pip install necpp + +## Documentation + +Try help(necpp) to list the available functions. The functions available are documented in the C-style API of nec2++. +This is [available here](http://tmolteno.github.io/necpp/libnecpp_8h.html) + +## Using + +The following code calculates the impedance of a simple vertical monopole antenna +over a perfect ground. + + import necpp + + def handle_nec(result): + if (result != 0): + print necpp.nec_error_message() + + def impedance(frequency, z0, height): + + nec = necpp.nec_create() + handle_nec(necpp.nec_wire(nec, 1, 17, 0, 0, z0, 0, 0, z0+height, 0.1, 1, 1)) + handle_nec(necpp.nec_geometry_complete(nec, 1, 0)) + handle_nec(necpp.nec_gn_card(nec, 1, 0, 0, 0, 0, 0, 0, 0)) + handle_nec(necpp.nec_fr_card(nec, 0, 1, frequency, 0)) + handle_nec(necpp.nec_ex_card(nec, 0, 0, 1, 0, 1.0, 0, 0, 0, 0, 0)) + handle_nec(necpp.nec_rp_card(nec, 0, 90, 1, 0,5,0,0, 0, 90, 1, 0, 0, 0)) + result_index = 0 + + z = complex(necpp.nec_impedance_real(nec,result_index), + necpp.nec_impedance_imag(nec,result_index)) + + necpp.nec_delete(nec) + return z + + if (__name__ == 'main'): + z = impedance(frequency = 34.5, z0 = 0.5, height = 4.0) + print "Impedance \t(%6.1f,%+6.1fI) Ohms" % (z.real, z.imag) + +## More Information + +Have a look at [http://github.com/tmolteno/necpp] for more information on using nec2++. diff --git a/build.sh b/necpp/build.sh similarity index 54% rename from build.sh rename to necpp/build.sh index 7052360..a99d63b 100755 --- a/build.sh +++ b/necpp/build.sh @@ -1,12 +1,13 @@ #!/bin/bash # Script to build the nec2++ python module. git submodule update --remote -pushd necpp_src +ln -s ../necpp_src . +DIR=`pwd` +cd necpp_src make -f Makefile.git ./configure --without-lapack -popd -pandoc -o README.txt README.md -PYTHON=python -swig -v -Inecpp_src/src/ -python necpp.i -python setup.py build +cd ${DIR} +PYTHON=python3 +swig3.0 -v -Inecpp_src/src/ -python necpp.i +python3 setup.py build #sudo python setup.py install diff --git a/necpp/example/.gitignore b/necpp/example/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/necpp/example/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/example/Makefile b/necpp/example/Makefile similarity index 100% rename from example/Makefile rename to necpp/example/Makefile diff --git a/example/README.md b/necpp/example/README.md similarity index 100% rename from example/README.md rename to necpp/example/README.md diff --git a/example/antenna_util.py b/necpp/example/antenna_util.py similarity index 100% rename from example/antenna_util.py rename to necpp/example/antenna_util.py diff --git a/necpp/example/different_material.py b/necpp/example/different_material.py new file mode 100644 index 0000000..92b87c1 --- /dev/null +++ b/necpp/example/different_material.py @@ -0,0 +1,50 @@ +from necpp import * +import math + +def handle_nec(result): + if (result != 0): + print(nec_error_message()) + +def geometry(freq, base, length): + + conductivity = 1.45e6 # Stainless steel + ground_conductivity = 0.002 + ground_dielectric = 10 + + wavelength = 3e8/(1e6*freq) + n_seg = int(math.ceil(50*length/wavelength)) + nec = nec_create() + + ''' + \brief Set the prameters of the medium (permittivity and permeability) + + \param permittivity The electric permittivity of the medium (in farads per meter) + \param permeability The magnetic permeability of the medium (in henries per meter) + + \remark From these parameters a speed of light is chosen. + ''' + permittivity = 8.8e-12 # Farads per meter + permeability = 4*math.pi*1e-7 + handle_nec(nec_medium_parameters(nec, 2.0*permittivity, permeability)) + + handle_nec(nec_wire(nec, 1, n_seg, 0, 0, base, 0, 0, base+length, 0.002, 1.0, 1.0)) + handle_nec(nec_geometry_complete(nec, 1)) + handle_nec(nec_ld_card(nec, 5, 0, 0, 0, conductivity, 0.0, 0.0)) + handle_nec(nec_gn_card(nec, 0, 0, ground_dielectric, ground_conductivity, 0, 0, 0, 0)) + handle_nec(nec_fr_card(nec, 0, 1, freq, 0)) + # Voltage excitation one third of the way along the wire + handle_nec(nec_ex_card(nec, 0, 0, int(n_seg/3), 0, 1.0, 0, 0, 0, 0, 0)) + + return nec + +def impedance(freq, base, length): + nec = geometry(freq, base, length) + handle_nec(nec_xq_card(nec, 0)) # Execute simulation + index = 0 + z = complex(nec_impedance_real(nec,index), nec_impedance_imag(nec,index)) + nec_delete(nec) + return z + +if (__name__ == '__main__'): + z = impedance(freq = 134.5, base = 0.5, length = 4.0) + print("Impedance at base=%0.2f, length=%0.2f : (%6.1f,%+6.1fI) Ohms" % (0.5, 4.0, z.real, z.imag)) diff --git a/necpp/example/impedance_plot.py b/necpp/example/impedance_plot.py new file mode 100644 index 0000000..a882ce4 --- /dev/null +++ b/necpp/example/impedance_plot.py @@ -0,0 +1,25 @@ +# +# Plot of reflection coefficient vs antenna length for a fixed base height. +# +import monopole +import numpy as np +import pylab as plt +from antenna_util import reflection_coefficient + +lengths = np.linspace(0.2, 5.0, 270) +reflections = [] +z0 = 50 + +for l in lengths: + freq = 134.5 + z = monopole.impedance(freq, base=0.5, length=l) + reflections.append(reflection_coefficient(z, z0)) + +plt.plot(lengths, reflections) +plt.xlabel("Antenna length (m)") +plt.ylabel("Reflection coefficient") +plt.title("Reflection coefficient vs length (base_height=0.5m)") +plt.grid(True) +plt.show() +plt.savefig("reflection_coefficient.png") + diff --git a/example/monopole.py b/necpp/example/monopole.py similarity index 84% rename from example/monopole.py rename to necpp/example/monopole.py index 725cb4d..4ccf0a8 100644 --- a/example/monopole.py +++ b/necpp/example/monopole.py @@ -7,7 +7,7 @@ def handle_nec(result): if (result != 0): - print nec_error_message() + print(nec_error_message()) def geometry(freq, base, length): @@ -24,7 +24,7 @@ def geometry(freq, base, length): handle_nec(nec_gn_card(nec, 0, 0, ground_dielectric, ground_conductivity, 0, 0, 0, 0)) handle_nec(nec_fr_card(nec, 0, 1, freq, 0)) # Voltage excitation one third of the way along the wire - handle_nec(nec_ex_card(nec, 0, 0, n_seg/3, 0, 1.0, 0, 0, 0, 0, 0)) + handle_nec(nec_ex_card(nec, 0, 0, int(n_seg/3), 0, 1.0, 0, 0, 0, 0, 0)) return nec @@ -38,4 +38,4 @@ def impedance(freq, base, length): if (__name__ == '__main__'): z = impedance(freq = 134.5, base = 0.5, length = 4.0) - print "Impedance at base=%0.2f, length=%0.2f : (%6.1f,%+6.1fI) Ohms" % (0.5, 4.0, z.real, z.imag) + print("Impedance at base=%0.2f, length=%0.2f : (%6.1f,%+6.1fI) Ohms" % (0.5, 4.0, z.real, z.imag)) diff --git a/example/optimized.py b/necpp/example/optimized.py similarity index 85% rename from example/optimized.py rename to necpp/example/optimized.py index 96317eb..2d1778d 100644 --- a/example/optimized.py +++ b/necpp/example/optimized.py @@ -27,5 +27,4 @@ def target(x): base_height = np.exp(log_base) length = np.exp(log_length) -print "Optimium base_height=%fm, h=%fm, impedance=%s Ohms" % \ - (base_height, length, monopole.impedance(freq, base_height, length)) +print("Optimium base_height={}m, h={}m, impedance={} Ohms".format(base_height, length, monopole.impedance(freq, base_height, length))) diff --git a/necpp.i b/necpp/necpp.i similarity index 100% rename from necpp.i rename to necpp/necpp.i diff --git a/necpp/pyproject.toml b/necpp/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/necpp/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/necpp/setup.cfg similarity index 100% rename from setup.cfg rename to necpp/setup.cfg diff --git a/necpp/setup.py b/necpp/setup.py new file mode 100644 index 0000000..896ed98 --- /dev/null +++ b/necpp/setup.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +""" +setup.py file for necpp Python module. +""" + +import os +from glob import glob + +from setuptools import Extension, setup + +nec_sources = [] +nec_sources.extend( + [ + fn + for fn in glob("necpp_src/src/*.cpp") + if not os.path.basename(fn).endswith("_tb.cpp") + if not os.path.basename(fn).startswith("net_solve.cpp") + if not os.path.basename(fn).startswith("nec2cpp.cpp") + if not os.path.basename(fn).startswith("necDiff.cpp") + ] +) +nec_sources.extend(glob("necpp_wrap.c")) + +nec_headers = [] +nec_headers.extend(glob("necpp_src/src/*.h")) +nec_headers.extend(glob("necpp_src/config.h")) + + +# At the moment, the config.h file is needed, and this should be generated from the ./configure +# command in the parent directory. Use ./configure --without-lapack to avoid dependance on LAPACK +# +necpp_module = Extension( + "_necpp", + sources=nec_sources, + include_dirs=["necpp_src/src/", "necpp_src/"], + depends=nec_headers, + define_macros=[("BUILD_PYTHON", "1")], +) + +with open("README.md") as f: + readme = f.read() + +setup( + name="necpp", + version="1.7.4", + author="Tim Molteno", + author_email="tim@physics.otago.ac.nz", + url="http://github.com/tmolteno/necpp", + keywords="nec2 nec2++ antenna electromagnetism radio", + description="Python Antenna Simulation Module (nec2++) C-style interface", + long_description=readme, + long_description_content_type="text/markdown", + include_package_data=True, + data_files=[("examples", ["necpp_src/example/test.py"])], + ext_modules=[necpp_module], + py_modules=["necpp"], + license="GPLv2", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Topic :: Scientific/Engineering", + "Topic :: Communications :: Ham Radio", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Intended Audience :: Science/Research", + ], +) diff --git a/necpp_src b/necpp_src index 56f10d2..049c556 160000 --- a/necpp_src +++ b/necpp_src @@ -1 +1 @@ -Subproject commit 56f10d2bfe56e1ef7bbfda81de8fe421a3be4b83 +Subproject commit 049c556cb10769410b760dcea7ef341a3e08f78b diff --git a/setup.py b/setup.py deleted file mode 100644 index b307b47..0000000 --- a/setup.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python - -""" -setup.py file for necpp Python module -""" - -from distutils.core import setup, Extension -from glob import glob -import os - -nec_sources = [] -nec_sources.extend([fn for fn in glob('necpp_src/src/*.cpp') - if not os.path.basename(fn).endswith('_tb.cpp') - if not os.path.basename(fn).startswith('net_solve.cpp') - if not os.path.basename(fn).startswith('nec2cpp.cpp') - if not os.path.basename(fn).startswith('necDiff.cpp')]) -nec_sources.extend(glob("necpp_wrap.c")) - -nec_headers = [] -nec_headers.extend(glob("necpp_src/src/*.h")) -nec_headers.extend(glob("necpp_src/config.h")) - - -# At the moment, the config.h file is needed, and this should be generated from the ./configure -# command in the parent directory. Use ./configure --without-lapack to avoid dependance on LAPACK -# -necpp_module = Extension('_necpp', - sources=nec_sources, - include_dirs=['necpp_src/src/', 'necpp_src/'], - depends=nec_headers, - define_macros=[('BUILD_PYTHON', '1')] - ) - - -setup (name = 'necpp', - version = '1.7.0.3', - author = "Tim Molteno", - author_email = "tim@physics.otago.ac.nz", - url = "http://github.com/tmolteno/necpp", - keywords = "nec2 nec2++ antenna electromagnetism radio", - description = "Python Antenna Simulation Module (nec2++) C-style interface", - data_files=[('examples', ['necpp_src/example/test.py'])], - ext_modules = [necpp_module], - py_modules = ["necpp"], - license='GPLv2', - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Topic :: Scientific/Engineering", - "Topic :: Communications :: Ham Radio", - "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", - "Intended Audience :: Science/Research"] - )