Skip to content

nec2diff treats any two negative values as equal (missing fabs in BaseInput.h diff()) #138

Open

Description

@Bruiser59

Summary

nec2diff treats any two negative values as equal, so it reports Difference = 0 for real changes in negative reactance, negative dB gains, and negative voltage/current components. A related edge case gives inf when a value only changes sign, and the program always exits with status 0, so scripts can't detect a difference.

Found at commit 7605c05 (v2.3.4), building with CMake on macOS (arm64, AppleClang 21) and reproduced with GCC 11.4 on Ubuntu 22.04.

Cause

src/BaseInput.h, real-valued diff():

nec_float diff(nec_float a, nec_float b)
{
	if (a == b)
		return 0;
	if ((a < 1e-8) && (b < 1e-8))   // <-- true for ANY two negative numbers
		return 0;
	...

The intent appears to be "both values are effectively zero", but without fabs() every pair of negative numbers takes this early return. The complex overload just below uses abs(a) < 1e-8, so it isn't affected.

A second issue in the same function: the result is |a − b| / |a + b|. When b == −a (a pure sign change, e.g. X = −179 Ω vs. +179 Ω), a + b == 0 and the division gives inf.

Steps to reproduce

Deck short_dipole.nec (a short, capacitive 20 m dipole in free space):

CM Short (capacitive) 20 m dipole in free space
CE
GW 1 21 0 -4.5 0 0 4.5 0 0.001
GE 0
EX 0 1 11 0 1 0
FR 0 1 0 0 14.2 0
RP 0 1 3 1000 90 0 0 45
EN
nec2++ -i short_dipole.nec -o a.out

Feedpoint impedance is 49.889 − j179.39 Ω. To isolate the comparison logic, make copies of a.out with one value changed in each:

# 1. Reactance -179.39 -> -1.00 ohm (both negative)
sed 's/4.9889E+01 -1.7939E+02/4.9889E+01 -1.0000E+00/' a.out > b_reactance.out
# 2. Pattern gain at phi=45 deg: -1.71 -> -9.71 dBi (both negative)
sed 's/   -1.71    -1.71/   -9.71    -9.71/' a.out > b_gain.out
# 3. Control: resistance 49.889 -> 59.889 ohm (positive)
sed 's/4.9889E+01 -1.7939E+02/5.9889E+01 -1.7939E+02/' a.out > c_resistance.out
# 4. Reactance sign flip: -179.39 -> +179.39 ohm
sed 's/4.9889E+01 -1.7939E+02/4.9889E+01  1.7939E+02/' a.out > d_signflip.out

for f in b_reactance b_gain c_resistance d_signflip; do nec2diff a.out $f.out; echo "exit=$?"; done

Actual results

Case Change nec2diff result Exit
1 X: −179.39 → −1.00 Ω AntennaInput Difference = 0 0
2 Gain: −1.71 → −9.71 dBi RadiationInput Difference = 0 0
3 (control) R: 49.889 → 59.889 Ω AntennaInput Difference = 0.0910929, "Input Parameters Different" 0
4 X: −179.39 → +179.39 Ω AntennaInput Difference = inf 0

Output for case 1, a 178 Ω reactance change reported as identical:

NecDiff 0.12
File1: a.out
File2: b_reactance.out
Impedance : 49.889 -179.39
Impedance : 49.889 -1
AntennaInput Difference = 0
PowerBudget Difference = 0
Radiation pattern: 3 lines
Radiation pattern: 3 lines
RadiationInput Difference = 0

Expected results

  • Cases 1 and 2 report a non-zero difference, as the positive-value control (case 3) does.
  • Case 4 reports a large, finite difference.
  • The exit status is non-zero when any section differs, so nec2diff can be used in scripts and test harnesses.

Impact

Most of a radiation pattern is negative dBi, and capacitive feedpoints have negative reactance, so a large share of the values nec2diff reads are never actually compared. Anything relying on it as a regression check (including the project's own test harness, if it uses nec2diff) can pass when results have changed.

Suggested fix

Minimal change (fixes cases 1 and 2):

	if ((std::fabs(a) < 1e-8) && (std::fabs(b) < 1e-8))
		return 0;

Also avoiding the inf in case 4 (normalising by the larger magnitude instead of |a + b|):

nec_float diff(nec_float a, nec_float b)
{
	if (a == b)
		return 0;
	nec_float scale = std::max(std::fabs(a), std::fabs(b));
	if (scale < 1e-8)
		return 0;
	nec_float ret = std::fabs(a - b) / scale;
	if (ret > 1e-2)
		cout << "    diff(" << a << "," << b << ") = " << ret << endl;
	return ret;
}

With this version applied locally, comparing a.out with itself still gives 0, and the four cases give 0.994 (1), 0.824 (2), 0.167 (3) and 2 (4).

Note this changes the scale of the metric slightly (|a−b|/max(|a|,|b|) instead of |a−b|/|a+b|), so the 1e-4 and 1e-2 thresholds may need a look.

For the exit status, main() in src/necDiff.cpp could track whether any equalto() check failed and return 1.

Minor, related

main() checks argc < 2 but continues anyway, and two file names are needed (argc < 3). Running nec2diff with zero or one argument prints the usage line and then aborts:

terminate called after throwing an instance of 'std::logic_error'
  what():  basic_string::_M_construct null not valid

Returning after printing usage when argc < 3 would fix it.

Thanks for maintaining nec2++!

Activity

  1. added a commit that references this issue on Nov 9, 2026
  2. tmolteno commented on Nov 9, 2026

    @tmolteno
    Owner

    Thank you for the excellent report — the analysis was exactly right, and all four problems are now fixed on master (PR #140):

    • The zero test in diff() now uses magnitudes (std::fabs), so negative values are compared properly. Both the real and complex overloads normalize by the larger magnitude (|a−b| / max(|a|,|b|)), which also removes the inf on pure sign flips.
    • nec2diff now exits 1 when any section differs and 0 when the files are identical, so it can gate scripts and test harnesses.
    • argc < 3 prints the usage line and returns 1 instead of aborting with std::logic_error.

    Your four cases now report exactly the values from your suggested fix: 0.994 (reactance), 0.824 (gain), 0.167 (positive control) and a finite 2.0 (sign flip). Regression fixtures and an exit-code test are committed (tests/data/issue138/, ctest necpp_nec2diff_issue138), testharness/data/short_dipole.nec is added as the fixture deck, and the nec2diff(1) man page now documents the exit status.

    One note on your caveat about the metric scale: we kept |a−b|/max(|a|,|b|) as you suggested. It is roughly twice the old |a−b|/|a+b| for near-equal values, so the summed 1e-4 equalto() threshold now binds about twice as tightly — a few last-digit discrepancies between nec2++ and the nec2c reference engine that the old metric silently hid are now visible in the harness .diff output. That is the comparison working as intended, but flagging it since it changes what the threshold means.

    This will be in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions