Antenna simulation driver and optimizer
What? 🔗
This is a way of running the NEC2 antenna simulation
nec2++. You need to install that
yourself. See to it that the pull request
82 is on board, which is
true for versions 1.7.6 and 2.0.0 and later; otherwise, this won't
work. Finally, see to it that the nec2++ executable can be called,
that is, is reachable via PATH.
What not? 🔗
There is a Python binding for
nec2++. I tried to get that to work, but failed. If that stuff
works for you, you probably don't need this.
So this is a pedestrian approach: Run nec2++ in a separate
process and communicate with that process via stdin/stdout.
Why? 🔗
- Parameter studies. Terrible things happen if you make your antenna shorter and shorter, or lower and lower, or whatever - but what exactly are these terrible things?
- Optimization. Put this project and an appropriate scipy optimization algorithm into a tumbler and stir well. Voila: An optimal antenna! And it is you who gets to say what is optimal.
Both are demonstrated in the showcase.
How? 🔗
Overview 🔗
The run_nec2pp method of the antenna_simulation_driver package is
your entry point. You need to feed it with a string that contains the
content you would usually put into a .nec file and feed to nec2++
from the nec2pp . That run_nec2pp method will call nec2++ and
parse the output into a Python friendly data structure. More on that
return value later.
NEC2 input data 🔗
The problem with the input data: That format for the input string is
neither documented here nor in the context of the nec2pp project.
The NEC software traces its origin to the 1970s, when punched (paper)
cards were commonplace. The original input format for NEC2 software
was defined in terms of such puched cards. Old printed documentation
of the day has been OCR'ed, manually polished, and is now available at
www.nec2.org. In particular, the NEC-2
manual (converted from paper
September 1996) is a good reference.
Today's nec2 implementations still read that stuff, even though they are no longer as picky about column numbers as the old code used to be.
Limits of what this Python package currently reads 🔗
run_nec2pp presently assumes a certain output format. To achieve
that:
- Run the analysis for one frequency only. I tend to use something
like
f"FR 0 1 0 0 {qrg*1e-6:.6f} 0.00\n"(where qrg is in Hz). - Run the analysis driven with a single voltage source. I use
f"EX 0 {tag_nr} {seg_nr} 0 1.0\n", wheretag_nrandseg_nrspecify which segment is to be driven. These indexes are 1-based, not 0-based. - Calculation itself needs to be triggered by an
RPcard. I might use something like"RP 0 37 145 1003 0.0 0.0 2.5 2.5 0.0 0.0"unless I want more detail in far field radiation, in which case I will increase the 37 and 145 and decrease the two 2.5 numbers accordingly. A lot of detail can be had with"RP 0 181 721 1003 0.0 0.0 0.5 0.5 0.0 0.0", at the expense of considerably slower execution. Typically, both 0° and 90° are wanted asthetavalues, so the first integer may need to be 1 higher than one might think. The same goes for the second integer, as both 0° and 360° are wanted forphi. Be warned that in NEC2 (and also in the output of this software, see below), theta is measured as the angle of the radiation ray from straight up. So DXers are mostly interested in theta values close to 90°.
Summary 🔗
So you put your cards you want nec2++ to process all into one Python
string, and used an RP card to actually start the simulation process
somewhere in there. Run method run_nec2pp of package
antenna_simulation_driver with that string as its only argument.
Limited precision 🔗
On the path from nec2++ to Python, all numbers are printed as
strings with limited precision and then read in. So results as
presented by this software inherit that limited precision.
The output data structure 🔗
The run_nec2pp will run the simulation for you and give you a
Nec2ppOutput object back (defined in package
nec2pp_output_parser). That structure is a big bucket containing
information about the simulation run's result:
@dataclass
class Nec2ppOutput:
frequency_and_wavelength: FrequencyAndWavelength
input_and_impedance: AntennaInputParameters
currents: list[SegmentCurrent]
power_budget: PowerBudget
radiation_pattern: RadiationPattern
average_gain: AverageGain
raw_output: Optional[str]
The following sections describe this in more detail.
FrequencyAndWavelength 🔗
Currently, you have to call run_nec2pp once for each frequency, so
you probably know which frequency you called it with. But that data
is available in case you want to make sure:
@dataclass
class FrequencyAndWavelength:
"""Frequency in Hz, wavelength in m."""
frequency: float
wavelength: float
AntennaInputParameters 🔗
A summary of the antenna's input. The impedance member I use
frequently:
@dataclass
class AntennaInputParameters:
tag: int
seg: int
voltage: complex
current: complex
impedance: complex
admittance: complex
power: float
SegmentCurrent 🔗
If you want to study current distribution on your segments, that information is available here:
@dataclass
class SegmentCurrent:
"""A segment and the current that flows through it."""
seg: int
tag: int
seg_center: SegmentCenter
seg_length: float
current: complex
current_magnitude: float
current_phase: float
Here, SegmentCenter is simply coordinates, but in wavelengths,
not in meter:
@dataclass
class SegmentCenter:
"""Values are in wavelengths, not in meter."""
x: float
y: float
z: float
PowerBudget 🔗
The PowerBudget data is much less useful than one would initially
think:
@dataclass
class PowerBudget:
"""All powers in W, efficiency as a number between 0 and 1."""
input_power: float
radiated_power: float
structure_loss: float
network_loss: float
efficiency: float
The efficiency is almost always very close to 1.0. It includes
ohmic wire loss, but not ground loss. See AverageGain below.
RadiationPattern 🔗
The class RadiationPattern gives you many instances of what I call a
RadiationPatternRay. There is one such ray for every direction you
asked to be calculated via your RP card.
A RadiationPattern object radiation_pattern has a read-only property
radiation_patter.rays that returns a list of RadiationPatternRays.
You can also ask for the one radiation pattern ray that's closest to given phi and theta values (both in degrees):
def closest_ray(self, phi: float, theta: float) -> RadiationPatternRay:
If your phi or theta value is not within the limits you asked via
your RP part (typically from theta = 0 for straight up to theta = 90
for horizon, and phi between 0 and 360 incl.), you'll deservedly earn
and will be given a ValueError.
Here is what the individual ray gives you:
@dataclass
class RadiationPatternRay:
theta: float # ray's vertical angle in degrees from z-axis (straight up)
phi: float # ray's horizontal angle in degrees, measured between x-axis and projection of ray to xy-plane
power_gain_v_db: float
power_gain_h_db: float
power_gain_total: float
polarization_axial_ratio: float
polarization_axial_tilt: float
polarization_sense: PolarizationSense
e_theta_magnitude: float
e_theta_phase_degrees: float
e_phi_magnitude: float
e_phi_phase_degrees: float
In Nec2, theta is the angle, in degrees, measured from straight up,
or, equivalently, measured from the z-axis of the coordinate system.
So theta is 90 if the ray points to the horizon. Secondly, phi is
the angle from the x-axis, counterclockwise, of the projection of the
ray onto the x-y-plane. If you think the x-axis points East and the
y-axis North, then phy values are as follows: 0° is East, 90° is
North, 180° is West and 270° is South.
Of the other members of RadiationPatternRay, I find
power_gain_total the most useful. These are dBi values
that can be used to draw the usual gain diagrams.
However, these numbers are not super precise. I somewhere
found the remark that 0.3 dB deviation is to be expected.
A fairly thin ideal dipole in free space came up with
a maximum gain of 2.06 dBi, whereas the expected
(correct) value was 2.15 dBi.
AverageGain 🔗
To actually know which part of your precious TX power does not heat
the wires or the ground, but is actually radiated (to whatever direction),
you need to look at AverageGain, more precisely,
the average_power_gain member.
@dataclass
class AverageGain:
average_power_gain: float
solid_angle_used_div_by_pi: float
If your antenna lives in free space, the average power gain and your antenna's efficiency are the same number.
If your antenna is employed above ideal or real ground, the average
power gain would become 2 if no loss was involved, as your
antenna's task is now reduced to only illuminate the upper half of
space. Correspondingly, your efficiency now becomes
average_power_gain / 2. The efficiency number thus calculated takes
both wire and ground losses into consideration.
raw_output 🔗
Finally, you can provide an optional second argument True to
run_nec2pp (default value False; this can also be supplied
as a named argument capture_output). If this is True,
then the raw_output member of Nec2ppOutput will contain
the (stdout) output of nec2++ as one big string.
This can be useful for debugging.
Error handling 🔗
If nec2++ fails, run_nec2pp raises CalledProcessError. As
run_nec2pp does not touch where nec2++'s stderr goes to, you
will probably want to read that to find out what the problem was.
SWR convenient function 🔗
Besides the power horse run_nec2pp, the package
antenna_simulation_driver also contains the convenience function
swr that calculates just that. Its signature is:
def swr(z: complex, z0: complex = 50) -> float:
Showcase 🔗
There is a showcase of how to actually use this stuff to do parameter studies and to rediscover the ZS6KBW multiband antenna. This showcase is publicized via my personal block at https://dj3ei.famsik.de/blog/antennas/antenna_simulation_driver_showcase.
Metadata
Release files for antenna-simulation-driver 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| antenna_simulation_driver-0.3.0.tar.gz | 18.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| antenna_simulation_driver-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.5 kB
Release files / antenna_simulation_driver-0.3.0.tar.gz
| Download URL | antenna_simulation_driver-0.3.0.tar.gz |
|---|---|
| Size | 18.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4da2e53e542c481e302743d93958e8235a541253216b4891746ff878bbad8244
|
|
BLAKE2b-256 checksum How to use checksums |
f31cc7f8ef666ddc7628643904c6e10463755ef36964375d49308f28842c13b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|
Release files / antenna_simulation_driver-0.3.0-py3-none-any.whl
| Download URL | antenna_simulation_driver-0.3.0-py3-none-any.whl |
|---|---|
| Size | 13.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
506b2177931763f2ca7b9e3a6626304aba8ea5e7ba2f2651ef98ab7b77d017e0
|
|
BLAKE2b-256 checksum How to use checksums |
20b5088d129c3a41119832d311c89083c810b7f242d594827baa360c29c41986
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.5
|