Metadata-Version: 2.4
Name: antenna_simulation_driver
Version: 0.2.0
Summary: Software to run antenna simulations, for parameter studies and optimizations.
Author-email: "Dr. Andreas Krüger, DJ3EI" <dj3ei@famsik.de>
Project-URL: Homepage, https://gitlab.com/4ham/antenna_simulation_driver
Project-URL: Repository, https://gitlab.com/4ham/antenna_simulation_driver
Project-URL: Issues, https://gitlab.com/4ham/antenna_simulation_driver/-/issues
Keywords: ham,amateur radio
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Education
Classifier: Topic :: Communications :: Ham Radio
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: numpy>=2.4.2
Dynamic: license-file

# Antenna simulation driver and optimizer

## What?  [<span id="what">🔗</span>](#what)

This is a way of running the NEC2 antenna simulation
[nec2++](https://github.com/tmolteno/necpp).  You need to install that
yourself. See to it that the [pull request
82](https://github.com/tmolteno/necpp/pull/82) is on board, which is
true for versions 1.7.6 and 2.0.0 and later; otherwiese, this won't
work. Finally, see to it that the `nec2++` executable can be called,
that is, is reachable via `PATH`.

## What not? [<span id="what_not">🔗</span>](#what_not)

There is a [Python binding](https://pypi.org/project/PyNEC/) 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?  [<span id="why">🔗</span>](#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](https://docs.scipy.org/doc/scipy/reference/optimize.html#global-optimization)
  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 [showcases](#showcases).

## How?  [<span id="how">🔗</span>](#how)

### Overview  [<span id="overview">🔗</span>](#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  [<span id="nec2_input">🔗</span>](#nec2_input)

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](https://www.nec2.org/).  In particular, the [NEC-2
manual](https://www.nec2.org/other/nec2prt3.pdf) (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  [<span id="current_limits">🔗</span>](#current_limits)

`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 is to be triggered by an `RP` card.  I typically
  use `"RP 0 37 144 1003 0.0 0.0 2.5 2.5 0.0 0.0\n"` unless I want
  more detail in far field radiation.  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\n"`, at the expense of
  considerably slower execution.  Typically, both 0° and 90° are
  wanted as `theta` values, so this may need one value more than one
  might think. Similarly, both 0° and 360° are wanted for `phi`.

### Summary  [<span id="summary">🔗</span>](#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  [<span id="limited_precision">🔗</span>](#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  [<span id="output">🔗</span>](#output)

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  [<span id="output_frequency_and_wavelength">🔗</span>](#output_frequency_and_wavelength)

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  [<span id="ouput_antenna_input_parameters">🔗</span>](#ouput_antenna_input_parameters)

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  [<span id="output_segment_current">🔗</span>](#output_segment_current)

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  [<span id="output_power_budget">🔗</span>](#output_power_budget)

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  [<span id="output_radiation_pattern">🔗</span>](#output_radiation_pattern)

The `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.

```
@dataclass
class RadiationPattern:
    rays: list[RadiationPatternRay]
```

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 values 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 dB, whereas the expected (correct) value was 2.15
dB.

#### AverageGain  [<span id="output_average_gain">🔗</span>](#output_average_gain)

To actually know which part of your precious TX power does not heat
the wires or the ground, but is actually radiated (to what direction
whatever), 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 the antenna had now loss, 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  [<span id="output_raw_output">🔗</span>](#output_raw_output)

Finally, you can provide an optional second argument `True` to
`run_nec2pp` (default value `False`, 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  [<span id="error_handling">🔗</span>](#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 is.

## SWR convenient function  [<span id="swr_function">🔗</span>](#swr_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  [<span id="showcase">🔗</span>](#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](https://dj3ei.famsik.de/blog/antennas/antenna_simulation_driver_showcase).

