Skip to main content

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", where tag_nr and seg_nr specify which segment is to be driven. These indexes are 1-based, not 0-based.
  • Calculation itself needs to be triggered by an RP card. 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 as theta values, 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 for phi. 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)

Source distribution for antenna-simulation-driver 0.3.0
File Size Uploaded
antenna_simulation_driver-0.3.0.tar.gz 18.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for antenna-simulation-driver 0.3.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page