FhSim  3.1.0
Marine systems simulation
Loading...
Searching...
No Matches
0006 — TrawlDoorAddedFoil interpolates the foil polar with the wrong array length
ID 0006
Class BUG
Severity 2
Status blocked
Models Trawl/TrawlDoorAddedFoil
Found 2026-09-15 audit (a7d080d)
Decision needed Confirm the count-versus-last-index convention of Interp1D and land the corrected length together with FISH-0004 and the first foil test.

Evidence

src/trawl/TrawlDoorAddedFoil.cpp:15-26 builds three arrays of length m_numFoilHydroPoints + 2, writing the user's table into [1 .. n] and putting clamping sentinels at [0] and [n+1]:

m_foilHydro_Aoa = new double[m_numFoilHydroPoints + 2];
...
creator->GetDoubleParamArray("FoilHydro_Aoa", m_foilHydro_Aoa + 1, m_numFoilHydroPoints);
...
m_foilHydro_Aoa[0] = -FLT_MAX;
m_foilHydro_CL[0] = 0;
m_foilHydro_CD[0] = 0;
m_foilHydro_Aoa[m_numFoilHydroPoints + 1] = FLT_MAX;
m_foilHydro_CL[m_numFoilHydroPoints + 1] = 0;
m_foilHydro_CD[m_numFoilHydroPoints + 1] = 0;

src/trawl/TrawlDoorAddedFoil.cpp:75-76 then interpolates with the unpadded count:

double foilCL = sfh::math::Interp1D(foilAoa, m_foilHydro_Aoa, m_foilHydro_CL, m_numFoilHydroPoints);
double foilCD = sfh::math::Interp1D(foilAoa, m_foilHydro_Aoa, m_foilHydro_CD, m_numFoilHydroPoints);

The padded arrays hold n + 2 samples, so Interp1D sees only the first n: the lower sentinel plus the first n - 1 user points. The last user point and the upper sentinel are never visible to it.

Effect

For any foil whose polar is tabulated (once FISH-0004 makes the table live at all): the last row of FoilHydro_Aoa/CL/CD is silently ignored, so a user who tabulates up to stall loses the stall point; and there is no upper clamp, so an angle of attack above the second-to-last tabulated angle is extrapolated or saturated against whatever Interp1D does past its last sample, instead of being clamped to zero lift by the +FLT_MAX sentinel the constructor installed. Lift on the wrong side of stall is precisely where a control study needs the clamp.

Possible fix

Pass the padded length at src/trawl/TrawlDoorAddedFoil.cpp:75-76:

const int numPaddedPoints = m_numFoilHydroPoints + 2;
double foilCL = sfh::math::Interp1D(foilAoa, m_foilHydro_Aoa, m_foilHydro_CL, numPaddedPoints);
double foilCD = sfh::math::Interp1D(foilAoa, m_foilHydro_Aoa, m_foilHydro_CD, numPaddedPoints);

Confirm sfh::math::Interp1D's last parameter is a sample count and not a last-index before committing; if it is a last-index the correct argument is m_numFoilHydroPoints + 1. That is a signature lookup in the fhsim package, not a judgement call.

No open semantics: the intent is unambiguous from the sentinels — they exist to clamp out-of-range angles, so the interpolator must see them. Blocked on FISH-0004 only: until the zeroing at TrawlDoorAddedFoil.cpp:93-94 is removed, neither foilCL nor foilCD reaches the simulation, so this fix is unobservable and untestable.

Test that would prove it

Depends on FISH-0001 and FISH-0004. In tests/in/SimObj-TrawlDoorAddedFoil-Stall/, tabulate a three-point polar whose last point is the only non-zero CL and command an angle of attack that lands on it: before the fix the lift is zero (the point is invisible to the interpolator), after it non-zero. A second case commands an angle well beyond the table and asserts the lift is zero, proving the upper sentinel is in play.

Risk

Confined to Trawl/TrawlDoorAddedFoil, which no shipped input file uses. It changes foil forces for tabulated polars, so it belongs in the same release as FISH-0004.

Why not solved (fix pass 2026-09-15)

Re-confirmed at HEAD: Interp1D(..., m_numFoilHydroPoints) is called on arrays allocated with n+2 entries. The fix is a single constant, but it changes the foil forces and is unobservable until FISH-0004 lands, and the count-versus-last-index convention of Interp1D was not verified during the fix pass, so even the direction of the correction is not certain. Blocked on FISH-0004; land the two together with the first foil test.