From 1ad2a127bee09b07d4318be1a884ba49eac8f50d Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Mon, 16 Jun 2025 23:54:08 -0500 Subject: [PATCH 01/10] Add error handling to Builder --- CMakeLists.txt | 2 +- include/lob/lob.hpp | 104 +++++++-- source/constants.hpp | 1 + source/lob_builder.cpp | 139 ++++++++++-- source/lob_solve.cpp | 8 +- source/solve_step.cpp | 4 +- test/source/lob_builder_test.cpp | 364 +++++++++++++++++++++++++++++++ test/source/lob_cwaj_test.cpp | 13 ++ 8 files changed, 581 insertions(+), 54 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bb3db28..40f381c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ include(cmake/prelude.cmake) project( lob - VERSION 0.6.0 + VERSION 0.6.5 DESCRIPTION "an exterior balistics calculation library" HOMEPAGE_URL "https://github.com/joelbenway/lob" LANGUAGES CXX) diff --git a/include/lob/lob.hpp b/include/lob/lob.hpp index a48ba0a..a3caa8b 100644 --- a/include/lob/lob.hpp +++ b/include/lob/lob.hpp @@ -24,12 +24,12 @@ LOB_EXPORT const char* Version(); * @brief Enumerates the supported drag functions. */ enum class LOB_EXPORT DragFunctionT : uint8_t { - kG1, /// @brief G1 drag function - kG2, /// @brief G2 drag function - kG5, /// @brief G5 drag function - kG6, /// @brief G6 drag function - kG7, /// @brief G7 drag function - kG8 /// @brief G8 drag function + kG1 = 1U, /// @brief G1 drag function + kG2 = 2U, /// @brief G2 drag function + kG5 = 5U, /// @brief G5 drag function + kG6 = 6U, /// @brief G6 drag function + kG7 = 7U, /// @brief G7 drag function + kG8 = 8U /// @brief G8 drag function }; /** @@ -46,20 +46,45 @@ enum class LOB_EXPORT AtmosphereReferenceT : uint8_t { * wind direction. */ enum class LOB_EXPORT ClockAngleT : uint8_t { - kIII = 0U, /// @brief three o'clock - kII, /// @brief two o'clock - kI, /// @brief one o'clock - kXII, /// @brief twelve o'clock - kXI, /// @brief eleven o'clock - kX, /// @brief ten o'clock - kIX, /// @brief nine o'clock - kVIII, /// @brief eight o'clock - kVII, /// @brief seven o'clock - kVI, /// @brief six o'clock - kV, /// @brief five o'clock - kIV /// @brief four o'clock + kI = 1U, /// @brief one o'clock + kII, /// @brief two o'clock + kIII, /// @brief three o'clock + kIV, /// @brief four o'clock + kV, /// @brief five o'clock + kVI, /// @brief six o'clock + kVII, /// @brief seven o'clock + kVIII, /// @brief eight o'clock + kIX, /// @brief nine o'clock + kX, /// @brief ten o'clock + kXI, /// @brief eleven o'clock + kXII /// @brief twelve o'clock }; // enum class ClockAngleT +enum class LOB_EXPORT ErrorT : uint8_t { + kNone, + kAirPressure, + kAltitude, + kAzimuth, + kBallisticCoefficient, + kBaseDiameter, + kDiameter, + kInitialVelocity, + kLatitude, + kLength, + kMachDragTable, + kMass, + kMaximumTime, + kMeplatDiameter, + kNoseLength, + kOgiveRtR, + kRangeAngle, + kTailLength, + kWindHeading, + kZeroAngle, + kZeroDistance, + kNotFormed +}; // enum class ErrorT + /// @brief Not-a-Number for floating-point values. template constexpr T NaN() { @@ -95,12 +120,13 @@ struct LOB_EXPORT Input { double cos_l_cos_a{NaN()}; /// @brief 2Ωcos(latitude)cos(azimuth) } corilolis; double zero_angle{NaN()}; /// @brief Angle between sight and trajectory. - double stability_factor{NaN()}; /// @brief Miller stability factor. - double aerodynamic_jump{NaN()}; /// @brief Aerodynamic jump effect in Moa. - double spindrift_factor{NaN()}; /// @brief Spin drift factor. - uint16_t minimum_speed{0}; /// @brief Minimum speed for solver. - double max_time{NaN()}; /// @brief Max time of flight for solver. - uint16_t step_size{0}; /// @brief Step size for solver. + double stability_factor{NaN()}; /// @brief Miller stability factor. + double aerodynamic_jump{NaN()}; /// @brief Aerodynamic jump effect in Moa. + double spindrift_factor{NaN()}; /// @brief Spin drift factor. + uint16_t minimum_speed{0}; /// @brief Minimum speed for solver. + double max_time{NaN()}; /// @brief Max time of flight for solver. + uint16_t step_size{0}; /// @brief Step size for solver. + ErrorT error{ErrorT::kNotFormed}; /// @brief Builder error field. }; // struct Input class Impl; @@ -126,18 +152,21 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& BallisticCoefficientPsi(double value); + /** * @brief Sets the atmosphere reference associated with ballistic coefficient. * @param type The atmosphere reference type. * @return A reference to the Builder object. */ Builder& BCAtmosphere(AtmosphereReferenceT type); + /** * @brief Sets the drag function associated with ballistic coefficient. * @param type The drag function type. * @return A reference to the Builder object. */ Builder& BCDragFunction(DragFunctionT type); + /** * @brief Sets the projectile diameter (caliber) in inches. * @param value The diameter in inches. @@ -213,12 +242,14 @@ class LOB_EXPORT Builder { const std::array& drags) { return MachVsDragTable(machs.data(), drags.data(), N); } + /** * @brief Sets the projectile mass in grains. * @param value The mass in grains. * @return A reference to the Builder object. */ Builder& MassGrains(double value); + /** * @brief Sets the initial velocity of the projectile in feet per second. * @param value The initial velocity in fps. @@ -231,6 +262,7 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& OpticHeightInches(double value); + /** * @brief Sets the twist rate of the barrel in inches per turn. * @note Used to calculate adjustments for spin drift and aerodynamic jump. @@ -238,6 +270,7 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& TwistInchesPerTurn(double value); + /** * @brief Sets the angle between the sight and launch angle used to achieve * zero. @@ -247,12 +280,14 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& ZeroAngleMOA(double value); + /** * @brief Sets the zero distance in yards. * @param value The zero distance in yards. * @return A reference to the Builder object. */ Builder& ZeroDistanceYds(double value); + /** * @brief Sets the zero impact height in inches. * @note This would be used if zeroing three inches high at 100 yards for @@ -261,18 +296,21 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& ZeroImpactHeightInches(double value); + /** * @brief Sets the altitude of the firing site in feet. * @param value The altitude in feet. * @return A reference to the Builder object. */ Builder& AltitudeOfFiringSiteFt(double value); + /** * @brief Sets the air pressure in inches of mercury (inHg). * @param value The air pressure in inHg. * @return A reference to the Builder object. */ Builder& AirPressureInHg(double value); + /** * @brief Sets the altitude of the location where air pressure was taken in * feet. @@ -283,12 +321,14 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& AltitudeOfBarometerFt(double value); + /** * @brief Sets the temperature in degrees Fahrenheit. * @param value The temperature in degrees F. * @return A reference to the Builder object. */ Builder& TemperatureDegF(double value); + /** * @brief Sets the altitude of the location where temperature was taken in * feet. @@ -299,12 +339,14 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& AltitudeOfThermometerFt(double value); + /** * @brief Sets the relative humidity at the firing site in percent. * @param value The relative humidity in percent. * @return A reference to the Builder object. */ Builder& RelativeHumidityPercent(double value); + /** * @brief Sets the wind heading using a clock angle. * @note Twelve O'Clock is pure tailwind, Six O'Clock is a pure headwind. @@ -312,6 +354,7 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& WindHeading(ClockAngleT value); + /** * @brief Sets the wind heading in degrees. * @note 0 is pure tailwind, 180 is pure headwind. @@ -319,18 +362,21 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& WindHeadingDeg(double value); + /** * @brief Sets the wind speed in feet per second. * @param value The wind speed in fps. * @return A reference to the Builder object. */ Builder& WindSpeedFps(double value); + /** * @brief Sets the wind speed in miles per hour. * @param value The wind speed in mph. * @return A reference to the Builder object. */ Builder& WindSpeedMph(double value); + /** * @brief Sets the azimuth (bearing) of the target in degrees. * @note Used for making coriolis effect corrections. @@ -338,6 +384,7 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& AzimuthDeg(double value); + /** * @brief Sets the latitude of the firing location in degrees. * @note Used for making coriolis effect corrections. @@ -345,12 +392,14 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& LatitudeDeg(double value); + /** * @brief Sets the range angle (inclination) to the target in degrees. * @param value The range angle in degrees. * @return A reference to the Builder object. */ Builder& RangeAngleDeg(double value); + /** * @brief Sets the minimum speed threshold for the solver. * @param value The minimum speed in feet per second (fps) at which the solver @@ -381,6 +430,13 @@ class LOB_EXPORT Builder { * @return A reference to the Builder object. */ Builder& StepSize(uint16_t value); + + /** + * @brief Resets the builder state by creating a fresh Impl object. + * @return A reference to the Builder object. + */ + Builder& Reset(); + /** * @brief Builds the `Input` object with the configured parameters. * @return The constructed `Input` object. diff --git a/source/constants.hpp b/source/constants.hpp index 8339e1b..d47df7c 100644 --- a/source/constants.hpp +++ b/source/constants.hpp @@ -17,6 +17,7 @@ constexpr double kIsaSeaLevelAirDensityLbsPerCuFt = 0.0764742; constexpr double kIsaSeaLevelSpeedOfSoundFps = 1116.45; constexpr double kIsaLapseDegFPerFt = 0.00356616; constexpr double kIsaTropopauseAltitudeFt = 36'090.0; +constexpr double kIsaStratosphereAltitudeFt = 65'617.0; constexpr double kIsaMinimumTempDegF = -69.7; constexpr double kArmyToIcaoBcConversionFactor = 0.982; constexpr double kAngularVelocityOfEarthRadPerSec = 7.292115E-5; diff --git a/source/lob_builder.cpp b/source/lob_builder.cpp index c707382..9380e66 100644 --- a/source/lob_builder.cpp +++ b/source/lob_builder.cpp @@ -100,36 +100,67 @@ Builder& Builder::operator=(Builder&& rhs) noexcept { } Builder& Builder::AltitudeOfFiringSiteFt(double value) { + const bool kIsValid = (-kIsaStratosphereAltitudeFt < value) && + (value < kIsaStratosphereAltitudeFt); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kAltitude; + } pimpl_->altitude_ft = FeetT(value); return *this; } Builder& Builder::AltitudeOfBarometerFt(double value) { + const bool kIsValid = (-kIsaStratosphereAltitudeFt < value) && + (value < kIsaStratosphereAltitudeFt); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kAltitude; + } pimpl_->altitude_of_barometer_ft = FeetT(value); return *this; } Builder& Builder::AltitudeOfThermometerFt(double value) { + const bool kIsValid = (-kIsaStratosphereAltitudeFt < value) && + (value < kIsaStratosphereAltitudeFt); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kAltitude; + } pimpl_->altitude_of_thermometer_ft = FeetT(value); return *this; } Builder& Builder::AzimuthDeg(double value) { + const bool kIsValid = (-kDegreesPerTurn < value) && (value < kDegreesPerTurn); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kAzimuth; + } pimpl_->azimuth_rad = DegreesT(value); return *this; } Builder& Builder::BallisticCoefficientPsi(double value) { + const bool kIsValid = (0.0 < value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kBallisticCoefficient; + } pimpl_->ballistic_coefficient_psi = PmsiT(value); return *this; } Builder& Builder::AirPressureInHg(double value) { + const bool kIsValid = (0.0 < value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kAirPressure; + } pimpl_->air_pressure_in_hg = InHgT(value); return *this; } Builder& Builder::BaseDiameterInch(double value) { + const bool kIsValid = (0.0 <= value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kBaseDiameter; + } pimpl_->base_diameter_in = InchT(value); return *this; } @@ -173,30 +204,45 @@ Builder& Builder::BCDragFunction(DragFunctionT type) { } Builder& Builder::DiameterInch(double value) { + const bool kIsValid = (0.0 < value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kDiameter; + } pimpl_->diameter_in = InchT(value); return *this; } Builder& Builder::InitialVelocityFps(uint16_t value) { + const bool kIsValid = (0 != value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kInitialVelocity; + } pimpl_->build.velocity = FpsT(value).U16(); return *this; } Builder& Builder::LatitudeDeg(double value) { + const bool kIsValid = (-90.0 <= value && value <= 90.0); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kLatitude; + } pimpl_->latitude_rad = DegreesT(value); return *this; } Builder& Builder::LengthInch(double value) { + const bool kIsValid = (0 < value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kLength; + } pimpl_->length_in = InchT(value); return *this; } Builder& Builder::MachVsDragTable(const float* pmachs, const float* pdrags, size_t size) { - assert(pmachs != nullptr); - assert(pdrags != nullptr); - if (pmachs == nullptr || pdrags == nullptr) { + if (pmachs == nullptr || pdrags == nullptr || size == 0) { + pimpl_->build.error = ErrorT::kMachDragTable; return *this; } for (size_t i = 0; i < kTableSize; i++) { @@ -211,16 +257,28 @@ Builder& Builder::MachVsDragTable(const float* pmachs, const float* pdrags, } Builder& Builder::MassGrains(double value) { + const bool kIsValid = (0.0 < value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kMass; + } pimpl_->build.mass = LbsT(GrainT(value)).Value(); return *this; } Builder& Builder::MaximumTime(double value) { + const bool kIsValid = (0.0 < value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kMaximumTime; + } pimpl_->build.max_time = value; return *this; } Builder& Builder::MeplatDiameterInch(double value) { + const bool kIsValid = (0.0 <= value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kMeplatDiameter; + } pimpl_->meplat_diameter_in = InchT(value); return *this; } @@ -236,11 +294,19 @@ Builder& Builder::MinimumSpeed(uint16_t value) { } Builder& Builder::NoseLengthInch(double value) { + const bool kIsValid = (0.0 <= value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kNoseLength; + } pimpl_->nose_length_in = InchT(value); return *this; } Builder& Builder::OgiveRtR(double value) { + const bool kIsValid = (0.0 <= value && value <= 1.0); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kOgiveRtR; + } pimpl_->ogive_rtr = value; return *this; } @@ -251,11 +317,19 @@ Builder& Builder::OpticHeightInches(double value) { } Builder& Builder::RelativeHumidityPercent(double value) { + const bool kIsValid = (0.0 < value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kNoseLength; + } pimpl_->relative_humidity_percent = value; return *this; } Builder& Builder::RangeAngleDeg(double value) { + const bool kIsValid = (-90.0 < value && value < 90.0); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kRangeAngle; + } pimpl_->range_angle_rad = RadiansT(DegreesT(value)); return *this; } @@ -266,6 +340,10 @@ Builder& Builder::StepSize(uint16_t value) { } Builder& Builder::TailLengthInch(double value) { + const bool kIsValid = (0.0 <= value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kTailLength; + } pimpl_->tail_length_in = InchT(value); return *this; } @@ -281,35 +359,32 @@ Builder& Builder::TwistInchesPerTurn(double value) { } Builder& Builder::WindHeading(ClockAngleT value) { - constexpr DegreesT kDegreesPerClockNumber = DegreesT(kDegreesPerTurn) / 12; - pimpl_->wind_heading_rad = - DegreesT(kDegreesPerClockNumber * static_cast(value)); + const DegreesT kDegreesPerClockNumber = DegreesT(kDegreesPerTurn) / 12; + const DegreesT kPosition(3 - static_cast(value)); + if (kPosition.Value() > 0) { + pimpl_->wind_heading_rad = kDegreesPerClockNumber * kPosition; + } else { + pimpl_->wind_heading_rad = + kDegreesPerClockNumber * kPosition + kDegreesPerTurn; + } return *this; } Builder& Builder::WindHeadingDeg(double value) { const DegreesT kFullTurn(kDegreesPerTurn); const DegreesT kQuarterTurn(kFullTurn / 4); - DegreesT angle(value); - - while (angle > DegreesT(0)) { - angle -= kFullTurn; - } - - while (angle < kFullTurn * -1.0) { - angle += kFullTurn; + const bool kIsValid = (kFullTurn * -1 < angle && angle < kFullTurn); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kWindHeading; } angle = angle * -1 + kQuarterTurn; - while (angle >= kFullTurn) { - angle -= kFullTurn; + if (angle < DegreesT(0)) { + angle += kFullTurn; } - assert(angle >= DegreesT(0)); - assert(angle < kFullTurn); - pimpl_->wind_heading_rad = angle; return *this; @@ -326,11 +401,19 @@ Builder& Builder::WindSpeedMph(double value) { } Builder& Builder::ZeroAngleMOA(double value) { + const bool kIsInvalid = (-45.0 > value || value > 45.0); + if (kIsInvalid) { + pimpl_->build.error = ErrorT::kZeroAngle; + } pimpl_->build.zero_angle = MoaT(value).Value(); return *this; } Builder& Builder::ZeroDistanceYds(double value) { + const bool kIsValid = (0.0 < value); + if (!kIsValid) { + pimpl_->build.error = ErrorT::kZeroDistance; + } pimpl_->zero_distance_ft = YardT(value); return *this; } @@ -340,14 +423,20 @@ Builder& Builder::ZeroImpactHeightInches(double value) { return *this; } +Builder& Builder::Reset() { + pimpl_->~Impl(); + pimpl_ = new (buffer_.data()) Impl(); + return *this; +} + namespace { bool ValidateBuild(const Impl& impl) { - const bool kBCisOk = !std::isnan(impl.ballistic_coefficient_psi); + const bool kErrorIsInExpectedState = impl.build.error == ErrorT::kNotFormed; + const bool kBCisOk = !impl.ballistic_coefficient_psi.IsNaN(); const bool kVelocityIsOk = impl.build.velocity > 0; const bool kZeroIsOk = - !std::isnan(impl.zero_distance_ft) || !std::isnan(impl.build.zero_angle); - - return (kBCisOk && kVelocityIsOk && kZeroIsOk); + !impl.zero_distance_ft.IsNaN() || !std::isnan(impl.build.zero_angle); + return kErrorIsInExpectedState && kBCisOk && kVelocityIsOk && kZeroIsOk; } void BuildEnvironment(Impl* pimpl) { @@ -690,6 +779,9 @@ void BuildOptions(Impl* pimpl) { } // namespace Input Builder::Build() { + if (pimpl_->build.error == ErrorT::kNone) { + pimpl_->build.error = ErrorT::kNotFormed; + } if (ValidateBuild(*pimpl_)) { // This order matters BuildEnvironment(pimpl_); @@ -705,6 +797,7 @@ Input Builder::Build() { BuildCoriolis(pimpl_); BuildZeroAngle(pimpl_); BuildOptions(pimpl_); + pimpl_->build.error = ErrorT::kNone; } return pimpl_->build; } diff --git a/source/lob_solve.cpp b/source/lob_solve.cpp index 28c154b..858b9f2 100644 --- a/source/lob_solve.cpp +++ b/source/lob_solve.cpp @@ -68,8 +68,8 @@ size_t Solve(const Input& in, const uint32_t* pranges, Output* pouts, assert(pranges != nullptr); assert(pouts != nullptr); assert(size > 0); - if (std::isnan(in.table_coefficient) || pranges == nullptr || - pouts == nullptr || size == 0) { + if (in.error != ErrorT::kNone || pranges == nullptr || pouts == nullptr || + size == 0) { return 0; } const FpsT kMinimumSpeed(in.minimum_speed); @@ -86,10 +86,10 @@ size_t Solve(const Input& in, const uint32_t* pranges, Output* pouts, const TrajectoryStateT kS = s; const SecT kT = t; - if (in.step_size > 0) { + if (in.step_size != 0) { SolveStep(&s, &t, in, SecT(UsecT(in.step_size))); } else { - SolveStep(&s, &t, in, FeetT(1)); + SolveStep(&s, &t, in); } if (s.P().X() >= FeetT(pranges[index]) && kS.P().X() < s.P().X()) { diff --git a/source/solve_step.cpp b/source/solve_step.cpp index 3d33882..eb634fa 100644 --- a/source/solve_step.cpp +++ b/source/solve_step.cpp @@ -19,7 +19,7 @@ namespace lob { void SolveStep(TrajectoryStateT* ps, SecT* pt, const Input& input, SecT step) { assert(ps != nullptr); assert(pt != nullptr); - assert(step.Value() > 0 && "step is not valid"); + assert(step.Value() > 0.0 && "step is not valid"); const CartesianT kWind(FpsT(input.wind.x), FpsT(0.0), FpsT(input.wind.z)); @@ -57,7 +57,7 @@ void SolveStep(TrajectoryStateT* ps, SecT* pt, const Input& input, SecT step) { void SolveStep(TrajectoryStateT* ps, SecT* pt, const Input& input, FeetT step) { assert(ps != nullptr); assert(pt != nullptr); - assert(step.Value() > 0 && "step is not valid"); + assert(step.Value() > 0.0 && "step is not valid"); const SecT kDt = AreEqual(ps->V().X().Value(), 0.0) ? SecT(UsecT(100)) : SecT(ps->V().X().Inverse().Value() * step.Value()); diff --git a/test/source/lob_builder_test.cpp b/test/source/lob_builder_test.cpp index 8e141a0..25f4289 100644 --- a/test/source/lob_builder_test.cpp +++ b/test/source/lob_builder_test.cpp @@ -347,6 +347,370 @@ TEST_F(BuilderTestFixture, JackOConnorZero) { EXPECT_NEAR(kJack.zero_angle, kExpectedZeroAngle, kError); } +TEST_F(BuilderTestFixture, ResetWorks) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .Reset() + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kNotFormed); +} + +TEST_F(BuilderTestFixture, AirPressureError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .AirPressureInHg(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kAirPressure); +} + +TEST_F(BuilderTestFixture, FiringSiteAltitudeError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .AltitudeOfFiringSiteFt(lob::kIsaStratosphereAltitudeFt + 1) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kAltitude); +} + +TEST_F(BuilderTestFixture, BarometerAltitudeError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .AltitudeOfBarometerFt(lob::kIsaStratosphereAltitudeFt + 1) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kAltitude); +} + +TEST_F(BuilderTestFixture, ThermometerAltitudeError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .AltitudeOfThermometerFt(lob::kIsaStratosphereAltitudeFt + 1) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kAltitude); +} + +TEST_F(BuilderTestFixture, AzimuthError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .AzimuthDeg(lob::kDegreesPerTurn + 1) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kAzimuth); +} + +TEST_F(BuilderTestFixture, BallisticCoefficientError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(-kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kBallisticCoefficient); +} + +TEST_F(BuilderTestFixture, BaseDiameterError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .BaseDiameterInch(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kBaseDiameter); +} + +TEST_F(BuilderTestFixture, DiameterError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .DiameterInch(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kDiameter); +} + +TEST_F(BuilderTestFixture, InitialVelocity) { + const double kSierraGameKingBC = 0.436; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(0) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kInitialVelocity); +} + +TEST_F(BuilderTestFixture, LatitudeError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .LatitudeDeg(91.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kLatitude); +} + +TEST_F(BuilderTestFixture, LengthError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .LengthInch(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kLength); +} + +TEST_F(BuilderTestFixture, MachVsDragTableError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .MachVsDragTable(nullptr, nullptr, 0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kMachDragTable); +} + +TEST_F(BuilderTestFixture, MassError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .MassGrains(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kMass); +} + +TEST_F(BuilderTestFixture, MaximumTimeError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .MaximumTime(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kMaximumTime); +} + +TEST_F(BuilderTestFixture, MeplatDiameterError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .MeplatDiameterInch(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kMeplatDiameter); +} + +TEST_F(BuilderTestFixture, NoseLengthError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .NoseLengthInch(-1) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kNoseLength); +} + +TEST_F(BuilderTestFixture, OgiveRtRError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .OgiveRtR(-1) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kOgiveRtR); +} + +TEST_F(BuilderTestFixture, RangeAngleError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .RangeAngleDeg(90) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kRangeAngle); +} + +TEST_F(BuilderTestFixture, TailLengthError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .TailLengthInch(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kTailLength); +} + +TEST_F(BuilderTestFixture, WindHeadingError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .WindHeadingDeg(lob::kDegreesPerTurn + 1) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kWindHeading); +} + +TEST_F(BuilderTestFixture, ZeroAngleError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .ZeroAngleMOA(46.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kZeroAngle); +} + +TEST_F(BuilderTestFixture, ZeroDistanceError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(-kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kZeroDistance); +} + TEST_F(BuilderTestFixture, RangeAngleDeg) { const double kBc = 0.400; const uint16_t kVelocity = 3000U; diff --git a/test/source/lob_cwaj_test.cpp b/test/source/lob_cwaj_test.cpp index 9aefffb..f382549 100644 --- a/test/source/lob_cwaj_test.cpp +++ b/test/source/lob_cwaj_test.cpp @@ -426,6 +426,19 @@ TEST_P(CWAJParameterizedFixture, Boatright) { .InitialVelocityFps(kShot.velocity) .TwistInchesPerTurn(kShot.twist) .Build(); + if (kA.error != lob::ErrorT::kNone) { + std::cout << kShot.diameter << "\n" + << kShot.length << "\n" + << kShot.mass << "\n" + << kShot.nose_length << "\n" + << kShot.tail_length << "\n" + << kShot.base_diameter << "\n" + << kShot.meplat_diameter << "\n" + << kShot.ogive_rtr << "\n" + << kShot.g1_bc << "\n" + << kShot.velocity << "\n" + << kShot.twist << "\n"; + } const double kError = std::abs(kShot.boatright) * 0.10; EXPECT_NEAR(kA.aerodynamic_jump, kShot.boatright, kError); } From c4b1dacab8c8823c78a64ab83f3cafa2e04aea5b Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:50:13 -0500 Subject: [PATCH 02/10] Fix flake build, update tests --- CMakeLists.txt | 6 +++--- CMakePresets.json | 2 +- HACKING.md | 4 ++-- cmake/variables.cmake | 2 +- flake.nix | 10 ++++------ include/lob/lob.hpp | 3 ++- source/lob_builder.cpp | 4 ++-- test/source/lob_builder_test.cpp | 16 ++++++++++++++++ test/source/lob_cwaj_test.cpp | 14 +------------- 9 files changed, 32 insertions(+), 29 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 40f381c..ba4faf6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,7 +76,7 @@ endif() # ---- Examples ---- if(PROJECT_IS_TOP_LEVEL) - option(BUILD_EXAMPLES "Build examples tree." "${lob_DEVELOPER_MODE}") + option(BUILD_EXAMPLES "Build examples tree." "${LOB_DEVELOPER_MODE}") if(BUILD_EXAMPLES) add_subdirectory(example) endif() @@ -85,7 +85,7 @@ endif() # ---- Benchmarks ---- if(PROJECT_IS_TOP_LEVEL) - option(BUILD_BENCHMARKS "Build benchmarks tree." "${lob_DEVELOPER_MODE}") + option(BUILD_BENCHMARKS "Build benchmarks tree." "${LOB_DEVELOPER_MODE}") if(BUILD_BENCHMARKS) add_subdirectory(benchmark) endif() @@ -93,7 +93,7 @@ endif() # ---- Developer mode ---- -if(NOT lob_DEVELOPER_MODE) +if(NOT LOB_DEVELOPER_MODE) return() elseif(NOT PROJECT_IS_TOP_LEVEL) message(AUTHOR_WARNING "Developer mode is intended for developers of lob") diff --git a/CMakePresets.json b/CMakePresets.json index 2054067..d1d108d 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -26,7 +26,7 @@ "hidden": true, "inherits": "cmake-pedantic", "cacheVariables": { - "lob_DEVELOPER_MODE": "ON" + "LOB_DEVELOPER_MODE": "ON" } }, { diff --git a/HACKING.md b/HACKING.md index 914c020..a823390 100644 --- a/HACKING.md +++ b/HACKING.md @@ -10,7 +10,7 @@ A declarative development environment for this project is available via the [Nix ## Developer mode -Build system targets that are only useful for developers of this project are hidden if the `lob_DEVELOPER_MODE` option is disabled. Enabling this option makes tests and other developer targets and options available. Not enabling this option means that you are a consumer of this project and thus you have no need for these targets and options. +Build system targets that are only useful for developers of this project are hidden if the `LOB_DEVELOPER_MODE` option is disabled. Enabling this option makes tests and other developer targets and options available. Not enabling this option means that you are a consumer of this project and thus you have no need for these targets and options. Developer mode is always set to on in CI workflows. @@ -18,7 +18,7 @@ Developer mode is always set to on in CI workflows. This project makes use of [presets][1] to simplify the process of configuring the project. As a developer, you are recommended to always have the [latest CMake version][12] installed to make use of the latest Quality-of-Life additions. -You have a few options to pass `lob_DEVELOPER_MODE` to the configure command, but this project prefers to use presets. +You have a few options to pass `LOB_DEVELOPER_MODE` to the configure command, but this project prefers to use presets. As a developer, you should create a `CMakeUserPresets.json` file at the root of the project: diff --git a/cmake/variables.cmake b/cmake/variables.cmake index 666d37c..6766c2e 100644 --- a/cmake/variables.cmake +++ b/cmake/variables.cmake @@ -9,7 +9,7 @@ # project must be provided unconditionally, so consumers can trivially build and # package the project if(PROJECT_IS_TOP_LEVEL) - option(lob_DEVELOPER_MODE "Enable developer mode" OFF) + option(LOB_DEVELOPER_MODE "Enable developer mode" OFF) option(BUILD_SHARED_LIBS "Build shared libs." OFF) endif() diff --git a/flake.nix b/flake.nix index ee34ee6..c5c424d 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,7 @@ configurePhase = '' cmake -S . -B build \ -D CMAKE_BUILD_TYPE=Release \ - -D lob_DEVELOP_MODE=ON \ + -D LOB_DEVELOPER_MODE=ON \ -D BUILD_EXAMPLES=OFF \ -D BUILD_BENCHMARKS=OFF ''; @@ -99,7 +99,7 @@ else [gdb] ); in { - # stdenv = pkgs.clangStdenv; + stdenv = pkgs.clangStdenv; buildInputs = oldAttrs.buildInputs ++ extraDevPackages; shellHook = let inherit (pkgs) stdenv; @@ -112,8 +112,6 @@ else ""; in '' - cores=$(getconf _NPROCESSORS_ONLN) - json=$(cat <<-EOF { "version": 2, @@ -144,7 +142,7 @@ "name": "dev", "configurePreset": "dev", "configuration": "Debug", - "jobs": "$cores" + "jobs": $NIX_BUILD_CORES } ], "testPresets": [ @@ -156,7 +154,7 @@ "outputOnFailure": true }, "execution": { - "jobs": "$cores", + "jobs": $NIX_BUILD_CORES, "noTestsAction": "error" } } diff --git a/include/lob/lob.hpp b/include/lob/lob.hpp index a3caa8b..8bd7ba4 100644 --- a/include/lob/lob.hpp +++ b/include/lob/lob.hpp @@ -68,6 +68,7 @@ enum class LOB_EXPORT ErrorT : uint8_t { kBallisticCoefficient, kBaseDiameter, kDiameter, + kHumidity, kInitialVelocity, kLatitude, kLength, @@ -435,7 +436,7 @@ class LOB_EXPORT Builder { * @brief Resets the builder state by creating a fresh Impl object. * @return A reference to the Builder object. */ - Builder& Reset(); + Builder& Reset() noexcept; /** * @brief Builds the `Input` object with the configured parameters. diff --git a/source/lob_builder.cpp b/source/lob_builder.cpp index 9380e66..a17f511 100644 --- a/source/lob_builder.cpp +++ b/source/lob_builder.cpp @@ -319,7 +319,7 @@ Builder& Builder::OpticHeightInches(double value) { Builder& Builder::RelativeHumidityPercent(double value) { const bool kIsValid = (0.0 < value); if (!kIsValid) { - pimpl_->build.error = ErrorT::kNoseLength; + pimpl_->build.error = ErrorT::kHumidity; } pimpl_->relative_humidity_percent = value; return *this; @@ -423,7 +423,7 @@ Builder& Builder::ZeroImpactHeightInches(double value) { return *this; } -Builder& Builder::Reset() { +Builder& Builder::Reset() noexcept { pimpl_->~Impl(); pimpl_ = new (buffer_.data()) Impl(); return *this; diff --git a/test/source/lob_builder_test.cpp b/test/source/lob_builder_test.cpp index 25f4289..6f2b036 100644 --- a/test/source/lob_builder_test.cpp +++ b/test/source/lob_builder_test.cpp @@ -490,6 +490,22 @@ TEST_F(BuilderTestFixture, DiameterError) { EXPECT_TRUE(kJack.error == lob::ErrorT::kDiameter); } +TEST_F(BuilderTestFixture, HumidityError) { + const double kSierraGameKingBC = 0.436; + const uint16_t kM70MuzzleVelocity = 3100U; + const double kZeroYardage = 100.0; + const double kZeroHeight = 3.0; + const lob::Input kJack = + puut->BallisticCoefficientPsi(kSierraGameKingBC) + .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + .InitialVelocityFps(kM70MuzzleVelocity) + .ZeroDistanceYds(kZeroYardage) + .ZeroImpactHeightInches(kZeroHeight) + .RelativeHumidityPercent(-1.0) + .Build(); + EXPECT_TRUE(kJack.error == lob::ErrorT::kHumidity); +} + TEST_F(BuilderTestFixture, InitialVelocity) { const double kSierraGameKingBC = 0.436; const double kZeroYardage = 100.0; diff --git a/test/source/lob_cwaj_test.cpp b/test/source/lob_cwaj_test.cpp index f382549..0b4c697 100644 --- a/test/source/lob_cwaj_test.cpp +++ b/test/source/lob_cwaj_test.cpp @@ -426,19 +426,7 @@ TEST_P(CWAJParameterizedFixture, Boatright) { .InitialVelocityFps(kShot.velocity) .TwistInchesPerTurn(kShot.twist) .Build(); - if (kA.error != lob::ErrorT::kNone) { - std::cout << kShot.diameter << "\n" - << kShot.length << "\n" - << kShot.mass << "\n" - << kShot.nose_length << "\n" - << kShot.tail_length << "\n" - << kShot.base_diameter << "\n" - << kShot.meplat_diameter << "\n" - << kShot.ogive_rtr << "\n" - << kShot.g1_bc << "\n" - << kShot.velocity << "\n" - << kShot.twist << "\n"; - } + const double kError = std::abs(kShot.boatright) * 0.10; EXPECT_NEAR(kA.aerodynamic_jump, kShot.boatright, kError); } From 74213d1c11f37dfcb286b1462783a0d2a46e6ee5 Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:01:26 -0500 Subject: [PATCH 03/10] run clang-format --- test/source/lob_cwaj_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/lob_cwaj_test.cpp b/test/source/lob_cwaj_test.cpp index 0b4c697..21a58be 100644 --- a/test/source/lob_cwaj_test.cpp +++ b/test/source/lob_cwaj_test.cpp @@ -426,7 +426,7 @@ TEST_P(CWAJParameterizedFixture, Boatright) { .InitialVelocityFps(kShot.velocity) .TwistInchesPerTurn(kShot.twist) .Build(); - + const double kError = std::abs(kShot.boatright) * 0.10; EXPECT_NEAR(kA.aerodynamic_jump, kShot.boatright, kError); } From b6f04ac2b558f37b24d5f5bec00128e7f85dc15c Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Tue, 17 Jun 2025 13:02:56 -0500 Subject: [PATCH 04/10] bug fix --- flake.nix | 2 -- source/lob_builder.cpp | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index c5c424d..d423417 100644 --- a/flake.nix +++ b/flake.nix @@ -55,7 +55,6 @@ pkgs.mkShell.override { # Override stdenv in order to change compiler: # stdenv = pkgs.clangStdenv; - stdenv = pkgs.gccStdenv; } { packages = with pkgs; [ @@ -99,7 +98,6 @@ else [gdb] ); in { - stdenv = pkgs.clangStdenv; buildInputs = oldAttrs.buildInputs ++ extraDevPackages; shellHook = let inherit (pkgs) stdenv; diff --git a/source/lob_builder.cpp b/source/lob_builder.cpp index a17f511..547f7e3 100644 --- a/source/lob_builder.cpp +++ b/source/lob_builder.cpp @@ -317,7 +317,7 @@ Builder& Builder::OpticHeightInches(double value) { } Builder& Builder::RelativeHumidityPercent(double value) { - const bool kIsValid = (0.0 < value); + const bool kIsValid = (0.0 <= value); if (!kIsValid) { pimpl_->build.error = ErrorT::kHumidity; } From 34274e0d63db314d7551d13390b364ea56c83a04 Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:31:50 -0500 Subject: [PATCH 05/10] update docs --- CONTRIBUTING.md | 22 +++++++++++++++++++--- README.md | 29 ++++++++++++++++++----------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a4026e..980d12c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,24 @@ # Contributing - +## Submitting Bugs + +A helpful bug report is the most valueable contribution a project can receive. Thank you! To ensure that your bug report is in fact helpful be sure to do your due diligence by troubleshooting. Check the issues to ensure your bug hasn't already been submitted. When you do submit, be specific about your version, your environment, compiler, etc. Include all the steps required to reproduce the buggy behavior. + +## Submitting Patches + +Before creating a PR, consider opening an issue to discuss the problem and solution. This will help ensure that your PR is accepted. When you do create a PR, be sure to include a description of the problem and solution. Always include tests if applicable. + +### Commit Guidelines + +* Keep commits small and focused +* Use the present tense ("Add feature" not "Added feature") +* Use the imperative mood ("Move error check to..." not "Moves error check to...") +* Limit the first line to 72 characters or less +* Reference issues and pull requests liberally after the first line + +### Style Guidelines + +Follow the style you see! This project uses a format and style from the Google style guide for C++. The style and formatting is enforced by CI. Consult the [`HACKING.md`](HACKING.md) document for details on how to run the style checks locally as cmake targets. ## Code of Conduct diff --git a/README.md b/README.md index 59e874f..d42921e 100644 --- a/README.md +++ b/README.md @@ -39,27 +39,34 @@ Lob uses a straightforward API featuring data structures and free functions that ### How to use lob ```C++ -#include // for array (c-style arrays are also supported) +#include // for array #include // for size_t #include // for uint32_t #include // for cout #include "lob.hpp" // Prepare an input using the Builder class -const lob::Input kSolverInput = lob::Builder() - .BallisticCoefficientPsi(0.425) - .InitialVelocityFps(2700) - .ZeroDistanceYds(100.0) - .Build(); -const size_t kNumToSolve = 7; // Solve for multiple ranges -const std::array kRanges = {0, 300, 600, 900, 1200, 1500, 1800}; -// Create a place to output solutions +const lob::Input kSolverInput = + lob::Builder() + .BallisticCoefficientPsi(0.425) + .InitialVelocityFps(2700) + .ZeroDistanceYds(100.0) + .Build(); +// Solve for multiple ranges +const size_t kNumToSolve = 7; +const std::array kRanges = + {0, 300, 600, 900, 1200, 1500, 1800}; +// Create an array to hold the outputs +// (c-style arrays are also supported) std::array solver_outputs = {}; // Solve! -const size_t kNumSolved = lob::Solve(kSolverInput, kRanges, solver_outputs); +const size_t kNumSolved = + lob::Solve(kSolverInput, kRanges, solver_outputs); // Do something with outputs for (size_t i = 0; i < kNumSolved; i++) { - std::cout << "Drop at " << solver_outputs.at(i).range << " feet is " << solver_outputs.at(i).elevation << " inches.\n"; + std::cout << "Drop at " << solver_outputs.at(i).range + << " feet is " << solver_outputs.at(i).elevation + << " inches.\n"; } ``` Those few parameters are enough for lob to make a well-formed, if minimal, ballistic solution. By feeding more data, we can get something more practical from the solver. From f7c6d4d60dbe139f5a996933aa158af2cfea7368 Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:40:54 -0500 Subject: [PATCH 06/10] spelling error --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 980d12c..bfb05b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Submitting Bugs -A helpful bug report is the most valueable contribution a project can receive. Thank you! To ensure that your bug report is in fact helpful be sure to do your due diligence by troubleshooting. Check the issues to ensure your bug hasn't already been submitted. When you do submit, be specific about your version, your environment, compiler, etc. Include all the steps required to reproduce the buggy behavior. +A helpful bug report is the most valuable contribution a project can receive. Thank you! To ensure that your bug report is in fact helpful be sure to do your due diligence by troubleshooting. Check the issues to ensure your bug hasn't already been submitted. When you do submit, be specific about your version, your environment, compiler, etc. Include all the steps required to reproduce the buggy behavior. ## Submitting Patches From 5d4f9ff746608407e2d76e24b5f9c61eefa24d66 Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:52:12 -0600 Subject: [PATCH 07/10] change flag emojis to something that works on windows --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d42921e..a64ed9e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The following can be accounted for in lob's solutions: * Gyroscopic spin drift * Aerodynamic jump -In addition to ballistic solutions, lob provides some of the instrumental values it calculates which may be useful including the local speed of sound, stability factor, and the "zero angle" between the line of sight and line of fire. All native units are customary American freedom units :us: but a collection of unit conversion functions are included :hammer_and_wrench: +In addition to ballistic solutions, lob provides some of the instrumental values it calculates which may be useful including the local speed of sound, stability factor, and the "zero angle" between the line of sight and line of fire. All native units are customary American freedom units :statue_of_liberty: but a collection of unit conversion functions are included :hammer_and_wrench: This repo includes a tiny example CLI program, lobber, which demonstrates the library's use. It's actually pretty handy! @@ -136,4 +136,4 @@ See the [CONTRIBUTING](CONTRIBUTING.md) document. See the [COPYING](COPYING) document. -:us: Hey, American company, you'd love to use lob but require a commercial license? [Raise an issue](https://github.com/joelbenway/lob/issues) to get in touch! Lob will help you hit your target! :rocket: +:eagle: Hey, American company, you'd love to use lob but require a commercial license? [Raise an issue](https://github.com/joelbenway/lob/issues) to get in touch! Lob will help you hit your target! :rocket: From 44b2e522f9c5db11ed6d7b742d40a11101b505e6 Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:52:26 -0600 Subject: [PATCH 08/10] fix typos, format --- CODE_OF_CONDUCT.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5724a6f..0498285 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,19 +2,19 @@ Lob is not trying to marshal contributors, a community, or even users but what collaboration exists shall govern itself according to the Scout Oath and Scout Law as championed by the Boy Scouts of America. These and similar doctrines usedby scouting organizations worldwide have a proven history of cultivating growth, strong character, and a fun environment among boys and men alike. These are welcome outcomes for an open source software project. They also have the benefit of already being memorized by lob's author. ## The Scout Oath -On my honor, I will do my bestTo do my duty to God and my country and to obey the Scout Law;To help other people at all times;To keep myself physically strong, mentally awake and morally straight. +On my honor, I will do my best to do my duty to God and my country and to obey the Scout Law; to help other people at all times; to keep myself physically strong, mentally awake and morally straight. ## The Scout Law A Scout is: -* Trustworthy, -* Loyal, -* Helpful, -* Friendly, -* Courteous, -* Kind, -* Obedient, -* Cheerful, -* Thrifty, -* Brave, -* Clean, -* and Reverent. \ No newline at end of file +* Trustworthy +* Loyal +* Helpful +* Friendly +* Courteous +* Kind +* Obedient +* Cheerful +* Thrifty +* Brave +* Clean +* Reverent \ No newline at end of file From 37351c6dd79f1126c7bde5e29a6138abe2c95894 Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:38:46 -0600 Subject: [PATCH 09/10] add builder validate method --- include/lob/lob.hpp | 6 ++++++ source/lob_builder.cpp | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/include/lob/lob.hpp b/include/lob/lob.hpp index 8bd7ba4..827cc4d 100644 --- a/include/lob/lob.hpp +++ b/include/lob/lob.hpp @@ -438,6 +438,12 @@ class LOB_EXPORT Builder { */ Builder& Reset() noexcept; + /** + * @brief Checks if the current builder state is well-formed. + * @return True if state is valid, false otherwise. + */ + bool IsValid() const; + /** * @brief Builds the `Input` object with the configured parameters. * @return The constructed `Input` object. diff --git a/source/lob_builder.cpp b/source/lob_builder.cpp index 547f7e3..51bd57b 100644 --- a/source/lob_builder.cpp +++ b/source/lob_builder.cpp @@ -429,16 +429,18 @@ Builder& Builder::Reset() noexcept { return *this; } -namespace { -bool ValidateBuild(const Impl& impl) { - const bool kErrorIsInExpectedState = impl.build.error == ErrorT::kNotFormed; - const bool kBCisOk = !impl.ballistic_coefficient_psi.IsNaN(); - const bool kVelocityIsOk = impl.build.velocity > 0; - const bool kZeroIsOk = - !impl.zero_distance_ft.IsNaN() || !std::isnan(impl.build.zero_angle); +bool Builder::IsValid() const { + const bool kErrorIsInExpectedState = + pimpl_->build.error == ErrorT::kNotFormed || + pimpl_->build.error == ErrorT::kNone; + const bool kBCisOk = !pimpl_->ballistic_coefficient_psi.IsNaN(); + const bool kVelocityIsOk = pimpl_->build.velocity > 0; + const bool kZeroIsOk = !pimpl_->zero_distance_ft.IsNaN() || + !std::isnan(pimpl_->build.zero_angle); return kErrorIsInExpectedState && kBCisOk && kVelocityIsOk && kZeroIsOk; } +namespace { void BuildEnvironment(Impl* pimpl) { assert(pimpl != nullptr); FeetT altitude_of_firing_site = FeetT(0); @@ -782,7 +784,7 @@ Input Builder::Build() { if (pimpl_->build.error == ErrorT::kNone) { pimpl_->build.error = ErrorT::kNotFormed; } - if (ValidateBuild(*pimpl_)) { + if (IsValid()) { // This order matters BuildEnvironment(pimpl_); BuildTable(pimpl_); From 51b171316aa9cfec3e3fab4a9a728c853e58441a Mon Sep 17 00:00:00 2001 From: Joel Benway <157863269+joelbenway@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:40:23 -0600 Subject: [PATCH 10/10] add full output tests for boatright cwaj and spin drift --- test/source/lob_cwaj_test.cpp | 440 +++++++++++++++++++++++----- test/source/lob_spin_drift_test.cpp | 417 +++++++++++++++++++++----- 2 files changed, 703 insertions(+), 154 deletions(-) diff --git a/test/source/lob_cwaj_test.cpp b/test/source/lob_cwaj_test.cpp index 21a58be..569133d 100644 --- a/test/source/lob_cwaj_test.cpp +++ b/test/source/lob_cwaj_test.cpp @@ -22,16 +22,25 @@ struct LobCWAJTestFixture : public testing::Test { LobCWAJTestFixture() : puut(nullptr) {} + const double kZeroDistance = 300; + const double kBarrelTwist = 11.0; + const double kOgiveLength = 0.748; + const double kTailLength = 0.257; + const double kMeplatDiameter = 0.069; + const double kBaseDiameter = 0.276; + const double kRtR = 0.99; + void SetUp() override { ASSERT_EQ(puut, nullptr); puut = std::make_unique(); ASSERT_NE(puut, nullptr); - const double kTestBC = 0.310; + // Lapua 250gr FMJBT pg 656, Ballistic Performance of Rifle Bullets - Litz + const double kTestBC = 0.308; const lob::DragFunctionT kDragFunction = lob::DragFunctionT::kG7; const double kTestDiameter = 0.338; const double kTestWeight = 250.0; - const double kBulletLength = 1.457; + const double kBulletLength = 1.471; const uint16_t kTestMuzzleVelocity = 3071; const double kTestZeroAngle = 6.53; const double kTestOpticHeight = 2.0; @@ -55,9 +64,8 @@ struct LobCWAJTestFixture : public testing::Test { TEST_F(LobCWAJTestFixture, ZeroAngleSearch) { ASSERT_NE(puut, nullptr); auto input1 = puut->Build(); - const double kZeroRange = 300.0; auto input2 = puut->ZeroAngleMOA(std::numeric_limits::quiet_NaN()) - .ZeroDistanceYds(kZeroRange) + .ZeroDistanceYds(kZeroDistance) .Build(); const double kError = 0.01; EXPECT_NEAR(input1.zero_angle, input2.zero_angle, kError); @@ -86,19 +94,19 @@ TEST_F(LobCWAJTestFixture, SolveWithoutSpin) { 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 0.95, 0.00, 0.049}, - {300, 2914, 4709, 2.93, 0.00, 0.100}, - {600, 2761, 4227, 3.76, 0.00, 0.206}, - {900, 2612, 3783, 0.03, 0.00, 0.318}, - {1200, 2467, 3376, -8.80, 0.00, 0.436}, - {1500, 2327, 3004, -23.35, 0.00, 0.561}, - {1800, 2192, 2665, -44.32, 0.00, 0.694}, - {2100, 2062, 2357, -72.53, 0.00, 0.835}, - {2400, 1936, 2078, -108.94, 0.00, 0.985}, - {2700, 1814, 1824, -154.62, 0.00, 1.145}, - {3000, 1695, 1593, -210.93, 0.00, 1.316}, - {4500, 1159, 745, -717.03, 0.00, 2.388}, - {6000, 952, 503, -1865.85, 0.00, 3.851}}; + {150, 2992, 4963, 0.95, 0.00, 0.049}, + {300, 2913, 4706, 2.93, 0.00, 0.100}, + {600, 2759, 4221, 3.76, 0.00, 0.206}, + {900, 2609, 3775, 0.02, 0.00, 0.318}, + {1200, 2464, 3366, -8.83, 0.00, 0.436}, + {1500, 2323, 2992, -23.42, 0.00, 0.562}, + {1800, 2187, 2652, -44.45, 0.00, 0.695}, + {2100, 2056, 2344, -72.76, 0.00, 0.836}, + {2400, 1929, 2064, -109.32, 0.00, 0.987}, + {2700, 1806, 1810, -155.22, 0.00, 1.148}, + {3000, 1687, 1579, -211.78, 0.00, 1.319}, + {4500, 1150, 734, -721.83, 0.00, 2.397}, + {6000, 949, 500, -1882.00, 0.00, 3.867}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -130,7 +138,6 @@ TEST_F(LobCWAJTestFixture, SolveWithoutSpin) { // NOLINTNEXTLINE(readability-function-cognitive-complexity) TEST_F(LobCWAJTestFixture, LitzRightHandSpinLeftwardWind) { ASSERT_NE(puut, nullptr); - constexpr double kBarrelTwist = 11.0; constexpr double kWind = 15.0; constexpr lob::ClockAngleT kWindHeading = lob::ClockAngleT::kIX; constexpr uint16_t kVelocityError = 1; @@ -138,7 +145,7 @@ TEST_F(LobCWAJTestFixture, LitzRightHandSpinLeftwardWind) { constexpr double kMoaError = 0.1; constexpr double kInchError = 0.5; constexpr double kTimeOfFlightError = 0.01; - constexpr double kAerodynamicJump = 0.660887; + constexpr double kAerodynamicJump = 0.650208; constexpr size_t kSolutionLength = 14; const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) .WindSpeedMph(kWind) @@ -150,19 +157,19 @@ TEST_F(LobCWAJTestFixture, LitzRightHandSpinLeftwardWind) { 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 1.30, -0.15, 0.049}, - {300, 2914, 4709, 3.62, -0.62, 0.100}, - {600, 2761, 4227, 5.15, -2.58, 0.206}, - {900, 2612, 3783, 2.10, -5.99, 0.318}, - {1200, 2467, 3376, -6.04, -10.99, 0.436}, - {1500, 2327, 3004, -19.90, -17.70, 0.561}, - {1800, 2192, 2665, -40.17, -26.27, 0.694}, - {2100, 2062, 2357, -67.69, -36.84, 0.835}, - {2400, 1936, 2078, -103.41, -49.61, 0.985}, - {2700, 1814, 1824, -148.40, -64.76, 1.145}, - {3000, 1695, 1593, -204.01, -82.56, 1.316}, - {4500, 1159, 745, -706.66, -222.33, 2.388}, - {6000, 952, 503, -1852.03, -450.26, 3.851}}; + {150, 2992, 4963, 1.29, -0.15, 0.049}, + {300, 2913, 4706, 3.61, -0.63, 0.100}, + {600, 2759, 4221, 5.12, -2.60, 0.206}, + {900, 2609, 3775, 2.06, -6.05, 0.318}, + {1200, 2464, 3366, -6.11, -11.09, 0.436}, + {1500, 2323, 2992, -20.02, -17.87, 0.562}, + {1800, 2187, 2652, -40.37, -26.52, 0.695}, + {2100, 2056, 2344, -68.00, -37.21, 0.836}, + {2400, 1929, 2064, -103.88, -50.11, 0.987}, + {2700, 1806, 1810, -149.10, -65.44, 1.148}, + {3000, 1687, 1579, -204.97, -83.43, 1.319}, + {4500, 1150, 734, -711.68, -225.13, 2.397}, + {6000, 949, 500, -1868.40, -455.01, 3.867}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -194,7 +201,7 @@ TEST_F(LobCWAJTestFixture, LitzRightHandSpinLeftwardWind) { // NOLINTNEXTLINE(readability-function-cognitive-complexity) TEST_F(LobCWAJTestFixture, LitzLeftHandSpinLeftwardWind) { ASSERT_NE(puut, nullptr); - constexpr double kBarrelTwist = -11.0; + const double kLeftTwist = -1.0 * kBarrelTwist; constexpr double kWind = 15.0; constexpr lob::ClockAngleT kWindHeading = lob::ClockAngleT::kIX; constexpr uint16_t kVelocityError = 1; @@ -202,9 +209,9 @@ TEST_F(LobCWAJTestFixture, LitzLeftHandSpinLeftwardWind) { constexpr double kMoaError = 0.1; constexpr double kInchError = 0.5; constexpr double kTimeOfFlightError = 0.01; - constexpr double kAerodynamicJump = -0.660887; + constexpr double kAerodynamicJump = -0.650208; constexpr size_t kSolutionLength = 14; - const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) + const auto kInput = puut->TwistInchesPerTurn(kLeftTwist) .WindSpeedMph(kWind) .WindHeading(kWindHeading) .Build(); @@ -213,20 +220,20 @@ TEST_F(LobCWAJTestFixture, LitzLeftHandSpinLeftwardWind) { 0, 150, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { - {0, 3071, 5230, -2.00, -0.00, 0.000}, - {150, 2992, 4965, 0.61, -0.19, 0.049}, - {300, 2914, 4709, 2.24, -0.75, 0.100}, - {600, 2761, 4227, 2.38, -3.06, 0.206}, - {900, 2612, 3783, -2.04, -7.05, 0.318}, - {1200, 2467, 3376, -11.57, -12.87, 0.436}, - {1500, 2327, 3004, -26.81, -20.69, 0.561}, - {1800, 2192, 2665, -48.47, -30.67, 0.694}, - {2100, 2062, 2357, -77.37, -43.03, 0.835}, - {2400, 1936, 2078, -114.47, -57.98, 0.985}, - {2700, 1814, 1824, -160.84, -75.79, 1.145}, - {3000, 1695, 1593, -217.84, -96.79, 1.316}, - {4500, 1159, 745, -727.39, -264.63, 2.388}, - {6000, 952, 503, -1879.67, -551.74, 3.851}}; + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 0.61, -0.19, 0.049}, + {300, 2913, 4706, 2.25, -0.75, 0.100}, + {600, 2759, 4221, 2.40, -3.07, 0.206}, + {900, 2609, 3775, -2.03, -7.09, 0.318}, + {1200, 2464, 3366, -11.56, -12.94, 0.436}, + {1500, 2323, 2992, -26.82, -20.81, 0.562}, + {1800, 2187, 2652, -48.54, -30.86, 0.695}, + {2100, 2056, 2344, -77.53, -43.30, 0.836}, + {2400, 1929, 2064, -114.77, -58.36, 0.987}, + {2700, 1806, 1810, -161.35, -76.31, 1.148}, + {3000, 1687, 1579, -218.59, -97.46, 1.319}, + {4500, 1150, 734, -732.11, -266.99, 2.397}, + {6000, 949, 500, -1895.73, -555.46, 3.868}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -258,7 +265,6 @@ TEST_F(LobCWAJTestFixture, LitzLeftHandSpinLeftwardWind) { // NOLINTNEXTLINE(readability-function-cognitive-complexity) TEST_F(LobCWAJTestFixture, LitzRightHandSpinRightwardWind) { ASSERT_NE(puut, nullptr); - constexpr double kBarrelTwist = 11.0; constexpr double kWind = 15.0; constexpr lob::ClockAngleT kWindHeading = lob::ClockAngleT::kIII; constexpr uint16_t kVelocityError = 1; @@ -266,7 +272,7 @@ TEST_F(LobCWAJTestFixture, LitzRightHandSpinRightwardWind) { constexpr double kMoaError = 0.1; constexpr double kInchError = 0.5; constexpr double kTimeOfFlightError = 0.01; - constexpr double kAerodynamicJump = -0.660887; + constexpr double kAerodynamicJump = -0.650208; constexpr size_t kSolutionLength = 14; const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) .WindSpeedMph(kWind) @@ -278,19 +284,19 @@ TEST_F(LobCWAJTestFixture, LitzRightHandSpinRightwardWind) { 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 0.61, 0.19, 0.049}, - {300, 2914, 4709, 2.24, 0.75, 0.100}, - {600, 2761, 4227, 2.38, 3.06, 0.206}, - {900, 2612, 3783, -2.04, 7.05, 0.318}, - {1200, 2467, 3376, -11.57, 12.87, 0.436}, - {1500, 2327, 3004, -26.81, 20.69, 0.561}, - {1800, 2192, 2665, -48.47, 30.67, 0.694}, - {2100, 2062, 2357, -77.37, 43.03, 0.835}, - {2400, 1936, 2078, -114.47, 57.98, 0.985}, - {2700, 1814, 1824, -160.84, 75.79, 1.145}, - {3000, 1695, 1593, -217.84, 96.79, 1.316}, - {4500, 1159, 745, -727.39, 264.63, 2.388}, - {6000, 952, 503, -1879.67, 551.74, 3.851}}; + {150, 2992, 4963, 0.61, 0.19, 0.049}, + {300, 2913, 4706, 2.25, 0.75, 0.100}, + {600, 2759, 4221, 2.40, 3.07, 0.206}, + {900, 2609, 3775, -2.03, 7.09, 0.318}, + {1200, 2464, 3366, -11.56, 12.94, 0.436}, + {1500, 2323, 2992, -26.82, 20.81, 0.562}, + {1800, 2187, 2652, -48.54, 30.86, 0.695}, + {2100, 2056, 2344, -77.53, 43.30, 0.836}, + {2400, 1929, 2064, -114.77, 58.36, 0.987}, + {2700, 1806, 1810, -161.35, 76.31, 1.148}, + {3000, 1687, 1579, -218.59, 97.46, 1.319}, + {4500, 1150, 734, -732.11, 266.99, 2.397}, + {6000, 949, 500, -1895.73, 555.46, 3.868}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -322,7 +328,207 @@ TEST_F(LobCWAJTestFixture, LitzRightHandSpinRightwardWind) { // NOLINTNEXTLINE(readability-function-cognitive-complexity) TEST_F(LobCWAJTestFixture, LitzLeftHandSpinRightwardWind) { ASSERT_NE(puut, nullptr); - constexpr double kBarrelTwist = -11.0; + const double kLeftTwist = -1.0 * kBarrelTwist; + constexpr double kWind = 15.0; + constexpr lob::ClockAngleT kWindHeading = lob::ClockAngleT::kIII; + constexpr uint16_t kVelocityError = 1; + constexpr uint16_t kEnergyError = 5; + constexpr double kMoaError = 0.1; + constexpr double kInchError = 0.5; + constexpr double kTimeOfFlightError = 0.01; + constexpr double kAerodynamicJump = 0.650208; + constexpr size_t kSolutionLength = 14; + const auto kInput = puut->TwistInchesPerTurn(kLeftTwist) + .WindSpeedMph(kWind) + .WindHeading(kWindHeading) + .Build(); + EXPECT_NEAR(kInput.aerodynamic_jump, kAerodynamicJump, kMoaError); + const std::array kRanges = { + 0, 150, 300, 600, 900, 1200, 1500, + 1800, 2100, 2400, 2700, 3000, 4500, 6000}; + const std::vector kExpected = { + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 1.29, 0.15, 0.049}, + {300, 2913, 4706, 3.61, 0.63, 0.100}, + {600, 2759, 4221, 5.12, 2.60, 0.206}, + {900, 2609, 3775, 2.06, 6.05, 0.318}, + {1200, 2464, 3366, -6.11, 11.09, 0.436}, + {1500, 2323, 2992, -20.02, 17.87, 0.562}, + {1800, 2187, 2652, -40.37, 26.52, 0.695}, + {2100, 2056, 2344, -68.00, 37.21, 0.836}, + {2400, 1929, 2064, -103.88, 50.11, 0.987}, + {2700, 1806, 1810, -149.10, 65.44, 1.148}, + {3000, 1687, 1579, -204.97, 83.43, 1.319}, + {4500, 1150, 734, -711.68, 225.13, 2.397}, + {6000, 949, 500, -1868.40, 455.01, 3.867}}; + + std::array solutions = {}; + const size_t kSize = lob::Solve(kInput, kRanges, solutions); + EXPECT_EQ(kSize, kSolutionLength); + for (size_t i = 0; i < kSolutionLength; i++) { + EXPECT_EQ(solutions.at(i).range, kExpected.at(i).range); + EXPECT_NEAR(solutions.at(i).velocity, kExpected.at(i).velocity, + kVelocityError); + EXPECT_NEAR(solutions.at(i).energy, kExpected.at(i).energy, kEnergyError); + const double kSolutionElevationMoa = + lob::InchToMoa(solutions.at(i).elevation, solutions.at(i).range); + const double kExpectedElevationMoa = + lob::InchToMoa(kExpected.at(i).elevation, kExpected.at(i).range); + EXPECT_NEAR(kSolutionElevationMoa, kExpectedElevationMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).elevation, kExpected.at(i).elevation, + kInchError); + const double kSolutionDeflectionMoa = + lob::InchToMoa(solutions.at(i).deflection, solutions.at(i).range); + const double kExpectedDeflectionMoa = + lob::InchToMoa(kExpected.at(i).deflection, kExpected.at(i).range); + EXPECT_NEAR(kSolutionDeflectionMoa, kExpectedDeflectionMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).deflection, kExpected.at(i).deflection, + kInchError); + EXPECT_NEAR(solutions.at(i).time_of_flight, kExpected.at(i).time_of_flight, + kTimeOfFlightError); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(LobCWAJTestFixture, BoatrightRightHandSpinLeftwardWind) { + ASSERT_NE(puut, nullptr); + constexpr double kWind = 15.0; + constexpr lob::ClockAngleT kWindHeading = lob::ClockAngleT::kIX; + constexpr uint16_t kVelocityError = 1; + constexpr uint16_t kEnergyError = 5; + constexpr double kMoaError = 0.1; + constexpr double kInchError = 0.5; + constexpr double kTimeOfFlightError = 0.01; + constexpr double kAerodynamicJump = 1.015683; + constexpr size_t kSolutionLength = 14; + const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) + .WindSpeedMph(kWind) + .WindHeading(kWindHeading) + .NoseLengthInch(kOgiveLength) + .TailLengthInch(kTailLength) + .BaseDiameterInch(kBaseDiameter) + .MeplatDiameterInch(kMeplatDiameter) + .OgiveRtR(kRtR) + .Build(); + EXPECT_NEAR(kInput.aerodynamic_jump, kAerodynamicJump, kMoaError); + const std::array kRanges = { + 0, 150, 300, 600, 900, 1200, 1500, + 1800, 2100, 2400, 2700, 3000, 4500, 6000}; + const std::vector kExpected = { + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 1.48, -0.15, 0.049}, + {300, 2913, 4706, 3.99, -0.63, 0.100}, + {600, 2759, 4221, 5.89, -2.60, 0.206}, + {900, 2609, 3775, 3.21, -6.05, 0.318}, + {1200, 2464, 3366, -4.58, -11.09, 0.436}, + {1500, 2323, 2992, -18.10, -17.87, 0.562}, + {1800, 2187, 2652, -38.07, -26.52, 0.695}, + {2100, 2056, 2344, -65.32, -37.21, 0.836}, + {2400, 1929, 2064, -100.82, -50.11, 0.987}, + {2700, 1806, 1810, -145.65, -65.44, 1.148}, + {3000, 1687, 1579, -201.15, -83.43, 1.319}, + {4500, 1150, 734, -705.94, -225.13, 2.397}, + {6000, 949, 500, -1860.74, -455.01, 3.867}}; + + std::array solutions = {}; + const size_t kSize = lob::Solve(kInput, kRanges, solutions); + EXPECT_EQ(kSize, kSolutionLength); + for (size_t i = 0; i < kSolutionLength; i++) { + EXPECT_EQ(solutions.at(i).range, kExpected.at(i).range); + EXPECT_NEAR(solutions.at(i).velocity, kExpected.at(i).velocity, + kVelocityError); + EXPECT_NEAR(solutions.at(i).energy, kExpected.at(i).energy, kEnergyError); + const double kSolutionElevationMoa = + lob::InchToMoa(solutions.at(i).elevation, solutions.at(i).range); + const double kExpectedElevationMoa = + lob::InchToMoa(kExpected.at(i).elevation, kExpected.at(i).range); + EXPECT_NEAR(kSolutionElevationMoa, kExpectedElevationMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).elevation, kExpected.at(i).elevation, + kInchError); + const double kSolutionDeflectionMoa = + lob::InchToMoa(solutions.at(i).deflection, solutions.at(i).range); + const double kExpectedDeflectionMoa = + lob::InchToMoa(kExpected.at(i).deflection, kExpected.at(i).range); + EXPECT_NEAR(kSolutionDeflectionMoa, kExpectedDeflectionMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).deflection, kExpected.at(i).deflection, + kInchError); + EXPECT_NEAR(solutions.at(i).time_of_flight, kExpected.at(i).time_of_flight, + kTimeOfFlightError); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(LobCWAJTestFixture, BoatrightLeftHandSpinLeftwardWind) { + ASSERT_NE(puut, nullptr); + const double kLeftTwist = -1.0 * kBarrelTwist; + constexpr double kWind = 15.0; + constexpr lob::ClockAngleT kWindHeading = lob::ClockAngleT::kIX; + constexpr uint16_t kVelocityError = 1; + constexpr uint16_t kEnergyError = 5; + constexpr double kMoaError = 0.1; + constexpr double kInchError = 0.5; + constexpr double kTimeOfFlightError = 0.01; + constexpr double kAerodynamicJump = -1.015683; + constexpr size_t kSolutionLength = 14; + const auto kInput = puut->TwistInchesPerTurn(kLeftTwist) + .WindSpeedMph(kWind) + .WindHeading(kWindHeading) + .NoseLengthInch(kOgiveLength) + .TailLengthInch(kTailLength) + .BaseDiameterInch(kBaseDiameter) + .MeplatDiameterInch(kMeplatDiameter) + .OgiveRtR(kRtR) + .Build(); + EXPECT_NEAR(kInput.aerodynamic_jump, kAerodynamicJump, kMoaError); + const std::array kRanges = { + 0, 150, 300, 600, 900, 1200, 1500, + 1800, 2100, 2400, 2700, 3000, 4500, 6000}; + const std::vector kExpected = { + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 0.42, -0.19, 0.049}, + {300, 2913, 4706, 1.87, -0.75, 0.100}, + {600, 2759, 4221, 1.63, -3.07, 0.206}, + {900, 2609, 3775, -3.18, -7.09, 0.318}, + {1200, 2464, 3366, -13.09, -12.94, 0.436}, + {1500, 2323, 2992, -28.74, -20.81, 0.562}, + {1800, 2187, 2652, -50.84, -30.86, 0.695}, + {2100, 2056, 2344, -80.21, -43.30, 0.836}, + {2400, 1929, 2064, -117.84, -58.36, 0.987}, + {2700, 1806, 1810, -164.80, -76.31, 1.148}, + {3000, 1687, 1579, -222.42, -97.46, 1.319}, + {4500, 1150, 734, -737.85, -266.99, 2.397}, + {6000, 949, 500, -1903.39, -555.46, 3.868}}; + + std::array solutions = {}; + const size_t kSize = lob::Solve(kInput, kRanges, solutions); + EXPECT_EQ(kSize, kSolutionLength); + for (size_t i = 0; i < kSolutionLength; i++) { + EXPECT_EQ(solutions.at(i).range, kExpected.at(i).range); + EXPECT_NEAR(solutions.at(i).velocity, kExpected.at(i).velocity, + kVelocityError); + EXPECT_NEAR(solutions.at(i).energy, kExpected.at(i).energy, kEnergyError); + const double kSolutionElevationMoa = + lob::InchToMoa(solutions.at(i).elevation, solutions.at(i).range); + const double kExpectedElevationMoa = + lob::InchToMoa(kExpected.at(i).elevation, kExpected.at(i).range); + EXPECT_NEAR(kSolutionElevationMoa, kExpectedElevationMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).elevation, kExpected.at(i).elevation, + kInchError); + const double kSolutionDeflectionMoa = + lob::InchToMoa(solutions.at(i).deflection, solutions.at(i).range); + const double kExpectedDeflectionMoa = + lob::InchToMoa(kExpected.at(i).deflection, kExpected.at(i).range); + EXPECT_NEAR(kSolutionDeflectionMoa, kExpectedDeflectionMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).deflection, kExpected.at(i).deflection, + kInchError); + EXPECT_NEAR(solutions.at(i).time_of_flight, kExpected.at(i).time_of_flight, + kTimeOfFlightError); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(LobCWAJTestFixture, BoatrightRightHandSpinRightwardWind) { + ASSERT_NE(puut, nullptr); constexpr double kWind = 15.0; constexpr lob::ClockAngleT kWindHeading = lob::ClockAngleT::kIII; constexpr uint16_t kVelocityError = 1; @@ -330,11 +536,16 @@ TEST_F(LobCWAJTestFixture, LitzLeftHandSpinRightwardWind) { constexpr double kMoaError = 0.1; constexpr double kInchError = 0.5; constexpr double kTimeOfFlightError = 0.01; - constexpr double kAerodynamicJump = 0.660887; + constexpr double kAerodynamicJump = -1.015683; constexpr size_t kSolutionLength = 14; const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) .WindSpeedMph(kWind) .WindHeading(kWindHeading) + .NoseLengthInch(kOgiveLength) + .TailLengthInch(kTailLength) + .BaseDiameterInch(kBaseDiameter) + .MeplatDiameterInch(kMeplatDiameter) + .OgiveRtR(kRtR) .Build(); EXPECT_NEAR(kInput.aerodynamic_jump, kAerodynamicJump, kMoaError); const std::array kRanges = { @@ -342,19 +553,88 @@ TEST_F(LobCWAJTestFixture, LitzLeftHandSpinRightwardWind) { 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 1.30, 0.15, 0.049}, - {300, 2914, 4709, 3.62, 0.62, 0.100}, - {600, 2761, 4227, 5.15, 2.58, 0.206}, - {900, 2612, 3783, 2.10, 5.99, 0.318}, - {1200, 2467, 3376, -6.04, 10.99, 0.436}, - {1500, 2327, 3004, -19.90, 17.70, 0.561}, - {1800, 2192, 2665, -40.17, 26.27, 0.694}, - {2100, 2062, 2357, -67.69, 36.84, 0.835}, - {2400, 1936, 2078, -103.41, 49.61, 0.985}, - {2700, 1814, 1824, -148.40, 64.76, 1.145}, - {3000, 1695, 1593, -204.01, 82.56, 1.316}, - {4500, 1159, 745, -706.66, 222.33, 2.388}, - {6000, 952, 503, -1852.03, 450.26, 3.851}}; + {150, 2992, 4963, 0.42, 0.19, 0.049}, + {300, 2913, 4706, 1.87, 0.75, 0.100}, + {600, 2759, 4221, 1.63, 3.07, 0.206}, + {900, 2609, 3775, -3.18, 7.09, 0.318}, + {1200, 2464, 3366, -13.09, 12.94, 0.436}, + {1500, 2323, 2992, -28.74, 20.81, 0.562}, + {1800, 2187, 2652, -50.84, 30.86, 0.695}, + {2100, 2056, 2344, -80.21, 43.30, 0.836}, + {2400, 1929, 2064, -117.84, 58.36, 0.987}, + {2700, 1806, 1810, -164.80, 76.31, 1.148}, + {3000, 1687, 1579, -222.42, 97.46, 1.319}, + {4500, 1150, 734, -737.85, 266.99, 2.397}, + {6000, 949, 500, -1903.39, 555.46, 3.868}}; + + std::array solutions = {}; + const size_t kSize = lob::Solve(kInput, kRanges, solutions); + EXPECT_EQ(kSize, kSolutionLength); + for (size_t i = 0; i < kSolutionLength; i++) { + EXPECT_EQ(solutions.at(i).range, kExpected.at(i).range); + EXPECT_NEAR(solutions.at(i).velocity, kExpected.at(i).velocity, + kVelocityError); + EXPECT_NEAR(solutions.at(i).energy, kExpected.at(i).energy, kEnergyError); + const double kSolutionElevationMoa = + lob::InchToMoa(solutions.at(i).elevation, solutions.at(i).range); + const double kExpectedElevationMoa = + lob::InchToMoa(kExpected.at(i).elevation, kExpected.at(i).range); + EXPECT_NEAR(kSolutionElevationMoa, kExpectedElevationMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).elevation, kExpected.at(i).elevation, + kInchError); + const double kSolutionDeflectionMoa = + lob::InchToMoa(solutions.at(i).deflection, solutions.at(i).range); + const double kExpectedDeflectionMoa = + lob::InchToMoa(kExpected.at(i).deflection, kExpected.at(i).range); + EXPECT_NEAR(kSolutionDeflectionMoa, kExpectedDeflectionMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).deflection, kExpected.at(i).deflection, + kInchError); + EXPECT_NEAR(solutions.at(i).time_of_flight, kExpected.at(i).time_of_flight, + kTimeOfFlightError); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(LobCWAJTestFixture, BoatrightLeftHandSpinRightwardWind) { + ASSERT_NE(puut, nullptr); + const double kLeftTwist = -1.0 * kBarrelTwist; + constexpr double kWind = 15.0; + constexpr lob::ClockAngleT kWindHeading = lob::ClockAngleT::kIII; + constexpr uint16_t kVelocityError = 1; + constexpr uint16_t kEnergyError = 5; + constexpr double kMoaError = 0.1; + constexpr double kInchError = 0.5; + constexpr double kTimeOfFlightError = 0.01; + constexpr double kAerodynamicJump = 1.015683; + constexpr size_t kSolutionLength = 14; + const auto kInput = puut->TwistInchesPerTurn(kLeftTwist) + .WindSpeedMph(kWind) + .WindHeading(kWindHeading) + .NoseLengthInch(kOgiveLength) + .TailLengthInch(kTailLength) + .BaseDiameterInch(kBaseDiameter) + .MeplatDiameterInch(kMeplatDiameter) + .OgiveRtR(kRtR) + .Build(); + EXPECT_NEAR(kInput.aerodynamic_jump, kAerodynamicJump, kMoaError); + const std::array kRanges = { + 0, 150, 300, 600, 900, 1200, 1500, + 1800, 2100, 2400, 2700, 3000, 4500, 6000}; + const std::vector kExpected = { + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 1.48, 0.15, 0.049}, + {300, 2913, 4706, 3.99, 0.63, 0.100}, + {600, 2759, 4221, 5.89, 2.60, 0.206}, + {900, 2609, 3775, 3.21, 6.05, 0.318}, + {1200, 2464, 3366, -4.58, 11.09, 0.436}, + {1500, 2323, 2992, -18.10, 17.87, 0.562}, + {1800, 2187, 2652, -38.07, 26.52, 0.695}, + {2100, 2056, 2344, -65.32, 37.21, 0.836}, + {2400, 1929, 2064, -100.82, 50.11, 0.987}, + {2700, 1806, 1810, -145.65, 65.44, 1.148}, + {3000, 1687, 1579, -201.15, 83.43, 1.319}, + {4500, 1150, 734, -705.94, 225.13, 2.397}, + {6000, 949, 500, -1860.74, 455.01, 3.867}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -419,8 +699,8 @@ TEST_P(CWAJParameterizedFixture, Boatright) { .MassGrains(kShot.mass) .NoseLengthInch(kShot.nose_length) .TailLengthInch(kShot.tail_length) - .BaseDiameterInch(kShot.base_diameter) .MeplatDiameterInch(kShot.meplat_diameter) + .BaseDiameterInch(kShot.base_diameter) .OgiveRtR(kShot.ogive_rtr) .BallisticCoefficientPsi(kShot.g1_bc) .InitialVelocityFps(kShot.velocity) diff --git a/test/source/lob_spin_drift_test.cpp b/test/source/lob_spin_drift_test.cpp index 6bbd32d..0d7cbe0 100644 --- a/test/source/lob_spin_drift_test.cpp +++ b/test/source/lob_spin_drift_test.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -21,16 +22,24 @@ struct LobSpinTestFixture : public testing::Test { LobSpinTestFixture() : puut(nullptr) {} + const double kZeroDistance = 300; + const double kOgiveLength = 0.748; + const double kTailLength = 0.257; + const double kMeplatDiameter = 0.069; + const double kBaseDiameter = 0.276; + const double kRtR = 0.99; + void SetUp() override { ASSERT_EQ(puut, nullptr); puut = std::make_unique(); ASSERT_NE(puut, nullptr); - const double kTestBC = 0.310; + // Lapua 250gr FMJBT pg 656, Ballistic Performance of Rifle Bullets - Litz + const double kTestBC = 0.308; const lob::DragFunctionT kDragFunction = lob::DragFunctionT::kG7; const double kTestDiameter = 0.338; const double kTestWeight = 250.0; - const double kBulletLength = 1.457; + const double kBulletLength = 1.471; const uint16_t kTestMuzzleVelocity = 3071; const double kTestZeroAngle = 6.53; const double kTestOpticHeight = 2.0; @@ -54,9 +63,8 @@ struct LobSpinTestFixture : public testing::Test { TEST_F(LobSpinTestFixture, ZeroAngleSearch) { ASSERT_NE(puut, nullptr); auto input1 = puut->Build(); - const double kZeroRange = 300.0; auto input2 = puut->ZeroAngleMOA(std::numeric_limits::quiet_NaN()) - .ZeroDistanceYds(kZeroRange) + .ZeroDistanceYds(kZeroDistance) .Build(); const double kError = 0.01; EXPECT_NEAR(input1.zero_angle, input2.zero_angle, kError); @@ -80,24 +88,25 @@ TEST_F(LobSpinTestFixture, SolveWithoutSpin) { constexpr double kTimeOfFlightError = 0.01; constexpr size_t kSolutionLength = 14; const auto kInput = puut->Build(); + EXPECT_TRUE(std::isnan(kInput.spindrift_factor)); const std::array kRanges = { 0, 150, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 0.95, 0.00, 0.049}, - {300, 2914, 4709, 2.93, 0.00, 0.100}, - {600, 2761, 4227, 3.76, 0.00, 0.206}, - {900, 2612, 3783, 0.03, 0.00, 0.318}, - {1200, 2467, 3376, -8.80, 0.00, 0.436}, - {1500, 2327, 3004, -23.35, 0.00, 0.561}, - {1800, 2192, 2665, -44.32, 0.00, 0.694}, - {2100, 2062, 2357, -72.53, 0.00, 0.835}, - {2400, 1936, 2078, -108.94, 0.00, 0.985}, - {2700, 1814, 1824, -154.62, 0.00, 1.145}, - {3000, 1695, 1593, -210.93, 0.00, 1.316}, - {4500, 1159, 745, -717.03, 0.00, 2.388}, - {6000, 952, 503, -1865.85, 0.00, 3.851}}; + {150, 2992, 4963, 0.95, 0.00, 0.049}, + {300, 2913, 4706, 2.93, 0.00, 0.100}, + {600, 2759, 4221, 3.76, 0.00, 0.206}, + {900, 2609, 3775, 0.02, 0.00, 0.318}, + {1200, 2464, 3366, -8.83, 0.00, 0.436}, + {1500, 2323, 2992, -23.42, 0.00, 0.562}, + {1800, 2187, 2652, -44.45, 0.00, 0.695}, + {2100, 2056, 2344, -72.76, 0.00, 0.836}, + {2400, 1929, 2064, -109.32, 0.00, 0.987}, + {2700, 1806, 1810, -155.22, 0.00, 1.148}, + {3000, 1687, 1579, -211.78, 0.00, 1.319}, + {4500, 1150, 734, -721.83, 0.00, 2.397}, + {6000, 949, 500, -1882.00, 0.00, 3.867}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -127,9 +136,9 @@ TEST_F(LobSpinTestFixture, SolveWithoutSpin) { } // NOLINTNEXTLINE(readability-function-cognitive-complexity) -TEST_F(LobSpinTestFixture, RightHandSpinDrift) { +TEST_F(LobSpinTestFixture, LitzRightHandSpinDrift) { ASSERT_NE(puut, nullptr); - constexpr double kBarrelTwist = 11.0; + const double kBarrelTwist = 11.0; constexpr uint16_t kVelocityError = 1; constexpr uint16_t kEnergyError = 5; constexpr double kMoaError = 0.1; @@ -137,24 +146,25 @@ TEST_F(LobSpinTestFixture, RightHandSpinDrift) { constexpr double kTimeOfFlightError = 0.01; constexpr size_t kSolutionLength = 14; const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist).Build(); + EXPECT_TRUE(std::isnan(kInput.spindrift_factor)); const std::array kRanges = { 0, 150, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 0.95, 0.02, 0.049}, - {300, 2914, 4709, 2.93, 0.06, 0.100}, - {600, 2761, 4227, 3.76, 0.24, 0.206}, - {900, 2612, 3783, 0.03, 0.53, 0.318}, - {1200, 2467, 3376, -8.80, 0.94, 0.436}, - {1500, 2327, 3004, -23.35, 1.49, 0.561}, - {1800, 2192, 2665, -44.32, 2.20, 0.694}, - {2100, 2062, 2357, -72.53, 3.09, 0.835}, - {2400, 1936, 2078, -108.94, 4.19, 0.985}, - {2700, 1814, 1824, -154.62, 5.51, 1.145}, - {3000, 1695, 1593, -210.93, 7.12, 1.316}, - {4500, 1159, 745, -717.03, 21.15, 2.388}, - {6000, 952, 503, -1865.85, 50.74, 3.851}}; + {150, 2992, 4963, 0.95, 0.02, 0.049}, + {300, 2913, 4706, 2.93, 0.06, 0.100}, + {600, 2759, 4221, 3.76, 0.23, 0.206}, + {900, 2609, 3775, 0.02, 0.52, 0.318}, + {1200, 2464, 3366, -8.84, 0.93, 0.436}, + {1500, 2323, 2992, -23.42, 1.47, 0.562}, + {1800, 2187, 2652, -44.45, 2.17, 0.695}, + {2100, 2056, 2344, -72.77, 3.05, 0.836}, + {2400, 1929, 2064, -109.33, 4.12, 0.987}, + {2700, 1806, 1810, -155.23, 5.44, 1.148}, + {3000, 1687, 1579, -211.78, 7.02, 1.319}, + {4500, 1150, 734, -721.84, 20.93, 2.397}, + {6000, 949, 500, -1882.01, 50.22, 3.867}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -184,7 +194,7 @@ TEST_F(LobSpinTestFixture, RightHandSpinDrift) { } // NOLINTNEXTLINE(readability-function-cognitive-complexity) -TEST_F(LobSpinTestFixture, RightHandSpinDriftFast) { +TEST_F(LobSpinTestFixture, LitzRightHandSpinDriftFast) { ASSERT_NE(puut, nullptr); constexpr double kBarrelTwist = 9.375; constexpr uint16_t kVelocityError = 1; @@ -194,24 +204,25 @@ TEST_F(LobSpinTestFixture, RightHandSpinDriftFast) { constexpr double kTimeOfFlightError = 0.01; constexpr size_t kSolutionLength = 14; const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist).Build(); + EXPECT_TRUE(std::isnan(kInput.spindrift_factor)); const std::array kRanges = { 0, 150, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 0.95, 0.02, 0.049}, - {300, 2914, 4709, 2.93, 0.08, 0.100}, - {600, 2761, 4227, 3.76, 0.30, 0.206}, - {900, 2612, 3783, 0.03, 0.66, 0.318}, - {1200, 2467, 3376, -8.80, 1.17, 0.436}, - {1500, 2327, 3004, -23.35, 1.86, 0.561}, - {1800, 2192, 2665, -44.32, 2.74, 0.694}, - {2100, 2062, 2357, -72.53, 3.85, 0.835}, - {2400, 1936, 2078, -108.94, 5.21, 0.985}, - {2700, 1814, 1824, -154.62, 6.87, 1.145}, - {3000, 1695, 1593, -210.93, 8.86, 1.316}, - {4500, 1159, 745, -717.03, 26.34, 2.388}, - {6000, 952, 503, -1865.85, 63.19, 3.851}}; + {150, 2992, 4963, 0.95, 0.02, 0.049}, + {300, 2913, 4706, 2.93, 0.08, 0.100}, + {600, 2759, 4221, 3.76, 0.29, 0.206}, + {900, 2609, 3775, 0.02, 0.64, 0.318}, + {1200, 2464, 3366, -8.84, 1.15, 0.436}, + {1500, 2323, 2992, -23.42, 1.83, 0.562}, + {1800, 2187, 2652, -44.45, 2.70, 0.695}, + {2100, 2056, 2344, -72.77, 3.79, 0.836}, + {2400, 1929, 2064, -109.33, 5.13, 0.987}, + {2700, 1806, 1810, -155.23, 6.76, 1.148}, + {3000, 1687, 1579, -211.78, 8.72, 1.319}, + {4500, 1150, 734, -721.84, 26.01, 2.397}, + {6000, 949, 500, -1882.01, 62.42, 3.867}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -241,7 +252,7 @@ TEST_F(LobSpinTestFixture, RightHandSpinDriftFast) { } // NOLINTNEXTLINE(readability-function-cognitive-complexity) -TEST_F(LobSpinTestFixture, LeftHandSpinDrift) { +TEST_F(LobSpinTestFixture, LitzLeftHandSpinDrift) { ASSERT_NE(puut, nullptr); constexpr double kBarrelTwist = -11.0; constexpr uint16_t kVelocityError = 1; @@ -251,24 +262,25 @@ TEST_F(LobSpinTestFixture, LeftHandSpinDrift) { constexpr double kTimeOfFlightError = 0.01; constexpr size_t kSolutionLength = 14; const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist).Build(); + EXPECT_TRUE(std::isnan(kInput.spindrift_factor)); const std::array kRanges = { 0, 150, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 0.95, -0.02, 0.049}, - {300, 2914, 4709, 2.93, -0.06, 0.100}, - {600, 2761, 4227, 3.76, -0.24, 0.206}, - {900, 2612, 3783, 0.03, -0.53, 0.318}, - {1200, 2467, 3376, -8.80, -0.94, 0.436}, - {1500, 2327, 3004, -23.35, -1.49, 0.561}, - {1800, 2192, 2665, -44.32, -2.20, 0.694}, - {2100, 2062, 2357, -72.53, -3.09, 0.835}, - {2400, 1936, 2078, -108.94, -4.19, 0.985}, - {2700, 1814, 1824, -154.62, -5.51, 1.145}, - {3000, 1695, 1593, -210.93, -7.12, 1.316}, - {4500, 1159, 745, -717.03, -21.15, 2.388}, - {6000, 952, 503, -1865.85, -50.74, 3.851}}; + {150, 2992, 4963, 0.95, -0.02, 0.049}, + {300, 2913, 4706, 2.93, -0.06, 0.100}, + {600, 2759, 4221, 3.76, -0.23, 0.206}, + {900, 2609, 3775, 0.02, -0.52, 0.318}, + {1200, 2464, 3366, -8.84, -0.93, 0.436}, + {1500, 2323, 2992, -23.42, -1.47, 0.562}, + {1800, 2187, 2652, -44.45, -2.17, 0.695}, + {2100, 2056, 2344, -72.77, -3.05, 0.836}, + {2400, 1929, 2064, -109.33, -4.12, 0.987}, + {2700, 1806, 1810, -155.23, -5.44, 1.148}, + {3000, 1687, 1579, -211.78, -7.02, 1.319}, + {4500, 1150, 734, -721.84, -20.93, 2.397}, + {6000, 949, 500, -1882.01, -50.22, 3.867}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions); @@ -298,7 +310,7 @@ TEST_F(LobSpinTestFixture, LeftHandSpinDrift) { } // NOLINTNEXTLINE(readability-function-cognitive-complexity) -TEST_F(LobSpinTestFixture, LeftHandSpinDriftFast) { +TEST_F(LobSpinTestFixture, LitzLeftHandSpinDriftFast) { ASSERT_NE(puut, nullptr); constexpr double kBarrelTwist = -9.375; constexpr uint16_t kVelocityError = 1; @@ -308,24 +320,281 @@ TEST_F(LobSpinTestFixture, LeftHandSpinDriftFast) { constexpr double kTimeOfFlightError = 0.01; constexpr size_t kSolutionLength = 14; const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist).Build(); + EXPECT_TRUE(std::isnan(kInput.spindrift_factor)); + const std::array kRanges = { + 0, 150, 300, 600, 900, 1200, 1500, + 1800, 2100, 2400, 2700, 3000, 4500, 6000}; + const std::vector kExpected = { + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 0.95, -0.02, 0.049}, + {300, 2913, 4706, 2.93, -0.08, 0.100}, + {600, 2759, 4221, 3.76, -0.29, 0.206}, + {900, 2609, 3775, 0.02, -0.64, 0.318}, + {1200, 2464, 3366, -8.84, -1.15, 0.436}, + {1500, 2323, 2992, -23.42, -1.83, 0.562}, + {1800, 2187, 2652, -44.45, -2.70, 0.695}, + {2100, 2056, 2344, -72.77, -3.79, 0.836}, + {2400, 1929, 2064, -109.33, -5.13, 0.987}, + {2700, 1806, 1810, -155.23, -6.76, 1.148}, + {3000, 1687, 1579, -211.78, -8.72, 1.319}, + {4500, 1150, 734, -721.84, -26.01, 2.397}, + {6000, 949, 500, -1882.01, -62.42, 3.867}}; + + std::array solutions = {}; + const size_t kSize = lob::Solve(kInput, kRanges, solutions); + EXPECT_EQ(kSize, kSolutionLength); + for (size_t i = 0; i < kSolutionLength; i++) { + EXPECT_EQ(solutions.at(i).range, kExpected.at(i).range); + EXPECT_NEAR(solutions.at(i).velocity, kExpected.at(i).velocity, + kVelocityError); + EXPECT_NEAR(solutions.at(i).energy, kExpected.at(i).energy, kEnergyError); + const double kSolutionElevationMoa = + lob::InchToMoa(solutions.at(i).elevation, solutions.at(i).range); + const double kExpectedElevationMoa = + lob::InchToMoa(kExpected.at(i).elevation, kExpected.at(i).range); + EXPECT_NEAR(kSolutionElevationMoa, kExpectedElevationMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).elevation, kExpected.at(i).elevation, + kInchError); + const double kSolutionDeflectionMoa = + lob::InchToMoa(solutions.at(i).deflection, solutions.at(i).range); + const double kExpectedDeflectionMoa = + lob::InchToMoa(kExpected.at(i).deflection, kExpected.at(i).range); + EXPECT_NEAR(kSolutionDeflectionMoa, kExpectedDeflectionMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).deflection, kExpected.at(i).deflection, + kInchError); + EXPECT_NEAR(solutions.at(i).time_of_flight, kExpected.at(i).time_of_flight, + kTimeOfFlightError); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(LobSpinTestFixture, BoatrightRightHandSpinDrift) { + ASSERT_NE(puut, nullptr); + const double kBarrelTwist = 11.0; + constexpr uint16_t kVelocityError = 1; + constexpr uint16_t kEnergyError = 5; + constexpr double kMoaError = 0.1; + constexpr double kInchError = 0.5; + constexpr double kTimeOfFlightError = 0.01; + constexpr size_t kSolutionLength = 14; + const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) + .NoseLengthInch(kOgiveLength) + .TailLengthInch(kTailLength) + .BaseDiameterInch(kBaseDiameter) + .MeplatDiameterInch(kMeplatDiameter) + .OgiveRtR(kRtR) + .Build(); + EXPECT_FALSE(std::isnan(kInput.spindrift_factor)); + const std::array kRanges = { + 0, 150, 300, 600, 900, 1200, 1500, + 1800, 2100, 2400, 2700, 3000, 4500, 6000}; + const std::vector kExpected = { + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 0.95, 0.02, 0.049}, + {300, 2913, 4706, 2.93, 0.06, 0.100}, + {600, 2759, 4221, 3.76, 0.23, 0.206}, + {900, 2609, 3775, 0.02, 0.52, 0.318}, + {1200, 2464, 3366, -8.84, 0.93, 0.436}, + {1500, 2323, 2992, -23.42, 1.47, 0.562}, + {1800, 2187, 2652, -44.45, 2.17, 0.695}, + {2100, 2056, 2344, -72.77, 3.05, 0.836}, + {2400, 1929, 2064, -109.33, 4.12, 0.987}, + {2700, 1806, 1810, -155.23, 5.44, 1.148}, + {3000, 1687, 1579, -211.78, 7.02, 1.319}, + {4500, 1150, 734, -721.84, 20.93, 2.397}, + {6000, 949, 500, -1882.01, 50.22, 3.867}}; + + std::array solutions = {}; + const size_t kSize = lob::Solve(kInput, kRanges, solutions); + EXPECT_EQ(kSize, kSolutionLength); + for (size_t i = 0; i < kSolutionLength; i++) { + EXPECT_EQ(solutions.at(i).range, kExpected.at(i).range); + EXPECT_NEAR(solutions.at(i).velocity, kExpected.at(i).velocity, + kVelocityError); + EXPECT_NEAR(solutions.at(i).energy, kExpected.at(i).energy, kEnergyError); + const double kSolutionElevationMoa = + lob::InchToMoa(solutions.at(i).elevation, solutions.at(i).range); + const double kExpectedElevationMoa = + lob::InchToMoa(kExpected.at(i).elevation, kExpected.at(i).range); + EXPECT_NEAR(kSolutionElevationMoa, kExpectedElevationMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).elevation, kExpected.at(i).elevation, + kInchError); + const double kSolutionDeflectionMoa = + lob::InchToMoa(solutions.at(i).deflection, solutions.at(i).range); + const double kExpectedDeflectionMoa = + lob::InchToMoa(kExpected.at(i).deflection, kExpected.at(i).range); + EXPECT_NEAR(kSolutionDeflectionMoa, kExpectedDeflectionMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).deflection, kExpected.at(i).deflection, + kInchError); + EXPECT_NEAR(solutions.at(i).time_of_flight, kExpected.at(i).time_of_flight, + kTimeOfFlightError); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(LobSpinTestFixture, BoatrightRightHandSpinDriftFast) { + ASSERT_NE(puut, nullptr); + constexpr double kBarrelTwist = 9.375; + constexpr uint16_t kVelocityError = 1; + constexpr uint16_t kEnergyError = 5; + constexpr double kMoaError = 0.1; + constexpr double kInchError = 0.5; + constexpr double kTimeOfFlightError = 0.01; + constexpr size_t kSolutionLength = 14; + const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) + .NoseLengthInch(kOgiveLength) + .TailLengthInch(kTailLength) + .BaseDiameterInch(kBaseDiameter) + .MeplatDiameterInch(kMeplatDiameter) + .OgiveRtR(kRtR) + .Build(); + EXPECT_FALSE(std::isnan(kInput.spindrift_factor)); + const std::array kRanges = { + 0, 150, 300, 600, 900, 1200, 1500, + 1800, 2100, 2400, 2700, 3000, 4500, 6000}; + const std::vector kExpected = { + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 0.95, 0.02, 0.049}, + {300, 2913, 4706, 2.93, 0.08, 0.100}, + {600, 2759, 4221, 3.76, 0.29, 0.206}, + {900, 2609, 3775, 0.02, 0.64, 0.318}, + {1200, 2464, 3366, -8.84, 1.15, 0.436}, + {1500, 2323, 2992, -23.42, 1.83, 0.562}, + {1800, 2187, 2652, -44.45, 2.70, 0.695}, + {2100, 2056, 2344, -72.77, 3.79, 0.836}, + {2400, 1929, 2064, -109.33, 5.13, 0.987}, + {2700, 1806, 1810, -155.23, 6.76, 1.148}, + {3000, 1687, 1579, -211.78, 8.72, 1.319}, + {4500, 1150, 734, -721.84, 26.01, 2.397}, + {6000, 949, 500, -1882.01, 62.42, 3.867}}; + + std::array solutions = {}; + const size_t kSize = lob::Solve(kInput, kRanges, solutions); + EXPECT_EQ(kSize, kSolutionLength); + for (size_t i = 0; i < kSolutionLength; i++) { + EXPECT_EQ(solutions.at(i).range, kExpected.at(i).range); + EXPECT_NEAR(solutions.at(i).velocity, kExpected.at(i).velocity, + kVelocityError); + EXPECT_NEAR(solutions.at(i).energy, kExpected.at(i).energy, kEnergyError); + const double kSolutionElevationMoa = + lob::InchToMoa(solutions.at(i).elevation, solutions.at(i).range); + const double kExpectedElevationMoa = + lob::InchToMoa(kExpected.at(i).elevation, kExpected.at(i).range); + EXPECT_NEAR(kSolutionElevationMoa, kExpectedElevationMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).elevation, kExpected.at(i).elevation, + kInchError); + const double kSolutionDeflectionMoa = + lob::InchToMoa(solutions.at(i).deflection, solutions.at(i).range); + const double kExpectedDeflectionMoa = + lob::InchToMoa(kExpected.at(i).deflection, kExpected.at(i).range); + EXPECT_NEAR(kSolutionDeflectionMoa, kExpectedDeflectionMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).deflection, kExpected.at(i).deflection, + kInchError); + EXPECT_NEAR(solutions.at(i).time_of_flight, kExpected.at(i).time_of_flight, + kTimeOfFlightError); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(LobSpinTestFixture, BoatrightLeftHandSpinDrift) { + ASSERT_NE(puut, nullptr); + constexpr double kBarrelTwist = -11.0; + constexpr uint16_t kVelocityError = 1; + constexpr uint16_t kEnergyError = 5; + constexpr double kMoaError = 0.1; + constexpr double kInchError = 0.5; + constexpr double kTimeOfFlightError = 0.01; + constexpr size_t kSolutionLength = 14; + const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) + .NoseLengthInch(kOgiveLength) + .TailLengthInch(kTailLength) + .BaseDiameterInch(kBaseDiameter) + .MeplatDiameterInch(kMeplatDiameter) + .OgiveRtR(kRtR) + .Build(); + EXPECT_FALSE(std::isnan(kInput.spindrift_factor)); + const std::array kRanges = { + 0, 150, 300, 600, 900, 1200, 1500, + 1800, 2100, 2400, 2700, 3000, 4500, 6000}; + const std::vector kExpected = { + {0, 3071, 5230, -2.00, 0.00, 0.000}, + {150, 2992, 4963, 0.95, -0.02, 0.049}, + {300, 2913, 4706, 2.93, -0.06, 0.100}, + {600, 2759, 4221, 3.76, -0.23, 0.206}, + {900, 2609, 3775, 0.02, -0.52, 0.318}, + {1200, 2464, 3366, -8.84, -0.93, 0.436}, + {1500, 2323, 2992, -23.42, -1.47, 0.562}, + {1800, 2187, 2652, -44.45, -2.17, 0.695}, + {2100, 2056, 2344, -72.77, -3.05, 0.836}, + {2400, 1929, 2064, -109.33, -4.12, 0.987}, + {2700, 1806, 1810, -155.23, -5.44, 1.148}, + {3000, 1687, 1579, -211.78, -7.02, 1.319}, + {4500, 1150, 734, -721.84, -20.93, 2.397}, + {6000, 949, 500, -1882.01, -50.22, 3.867}}; + + std::array solutions = {}; + const size_t kSize = lob::Solve(kInput, kRanges, solutions); + EXPECT_EQ(kSize, kSolutionLength); + for (size_t i = 0; i < kSolutionLength; i++) { + EXPECT_EQ(solutions.at(i).range, kExpected.at(i).range); + EXPECT_NEAR(solutions.at(i).velocity, kExpected.at(i).velocity, + kVelocityError); + EXPECT_NEAR(solutions.at(i).energy, kExpected.at(i).energy, kEnergyError); + const double kSolutionElevationMoa = + lob::InchToMoa(solutions.at(i).elevation, solutions.at(i).range); + const double kExpectedElevationMoa = + lob::InchToMoa(kExpected.at(i).elevation, kExpected.at(i).range); + EXPECT_NEAR(kSolutionElevationMoa, kExpectedElevationMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).elevation, kExpected.at(i).elevation, + kInchError); + const double kSolutionDeflectionMoa = + lob::InchToMoa(solutions.at(i).deflection, solutions.at(i).range); + const double kExpectedDeflectionMoa = + lob::InchToMoa(kExpected.at(i).deflection, kExpected.at(i).range); + EXPECT_NEAR(kSolutionDeflectionMoa, kExpectedDeflectionMoa, kMoaError); + EXPECT_NEAR(solutions.at(i).deflection, kExpected.at(i).deflection, + kInchError); + EXPECT_NEAR(solutions.at(i).time_of_flight, kExpected.at(i).time_of_flight, + kTimeOfFlightError); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(LobSpinTestFixture, BoatrightLeftHandSpinDriftFast) { + ASSERT_NE(puut, nullptr); + constexpr double kBarrelTwist = -9.375; + constexpr uint16_t kVelocityError = 1; + constexpr uint16_t kEnergyError = 5; + constexpr double kMoaError = 0.1; + constexpr double kInchError = 0.5; + constexpr double kTimeOfFlightError = 0.01; + constexpr size_t kSolutionLength = 14; + const auto kInput = puut->TwistInchesPerTurn(kBarrelTwist) + .NoseLengthInch(kOgiveLength) + .TailLengthInch(kTailLength) + .BaseDiameterInch(kBaseDiameter) + .MeplatDiameterInch(kMeplatDiameter) + .OgiveRtR(kRtR) + .Build(); + EXPECT_FALSE(std::isnan(kInput.spindrift_factor)); const std::array kRanges = { 0, 150, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000, 4500, 6000}; const std::vector kExpected = { {0, 3071, 5230, -2.00, 0.00, 0.000}, - {150, 2992, 4965, 0.95, -0.02, 0.049}, - {300, 2914, 4709, 2.93, -0.08, 0.100}, - {600, 2761, 4227, 3.76, -0.30, 0.206}, - {900, 2612, 3783, 0.03, -0.66, 0.318}, - {1200, 2467, 3376, -8.80, -1.17, 0.436}, - {1500, 2327, 3004, -23.35, -1.86, 0.561}, - {1800, 2192, 2665, -44.32, -2.74, 0.694}, - {2100, 2062, 2357, -72.53, -3.85, 0.835}, - {2400, 1936, 2078, -108.94, -5.21, 0.985}, - {2700, 1814, 1824, -154.62, -6.87, 1.145}, - {3000, 1695, 1593, -210.93, -8.86, 1.316}, - {4500, 1159, 745, -717.03, -26.34, 2.388}, - {6000, 952, 503, -1865.85, -63.19, 3.851}}; + {150, 2992, 4963, 0.95, -0.02, 0.049}, + {300, 2913, 4706, 2.93, -0.08, 0.100}, + {600, 2759, 4221, 3.76, -0.29, 0.206}, + {900, 2609, 3775, 0.02, -0.64, 0.318}, + {1200, 2464, 3366, -8.84, -1.15, 0.436}, + {1500, 2323, 2992, -23.42, -1.83, 0.562}, + {1800, 2187, 2652, -44.45, -2.70, 0.695}, + {2100, 2056, 2344, -72.77, -3.79, 0.836}, + {2400, 1929, 2064, -109.33, -5.13, 0.987}, + {2700, 1806, 1810, -155.23, -6.76, 1.148}, + {3000, 1687, 1579, -211.78, -8.72, 1.319}, + {4500, 1150, 734, -721.84, -26.01, 2.397}, + {6000, 949, 500, -1882.01, -62.42, 3.867}}; std::array solutions = {}; const size_t kSize = lob::Solve(kInput, kRanges, solutions);