tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it./opt/ballistics/src/solver.cpp
1 #include "ballistics/integrator.hpp"
2
3 #include <cstddef>
4 #include <stdexcept>
5 #include <vector>
6
7 #include "ballistics/model.hpp"
8 #include "ballistics/types.hpp"
9
10 namespace ballistics {
11
12 // ============================================================================
13 // STUB IMPLEMENTATION -- replace the bodies below with the real 3D high-order
14 // fixed-step integrator and sub-step event reporting.
15 //
16 // As shipped this compiles cleanly (so the Docker image builds) but is WRONG:
17 // * step() returns the state unchanged -- the projectile is frozen;
18 // * simulate() records only the launch state, never integrates, and reports
19 // no apex and no impact.
20 // Every flight test therefore fails. Implement `step` as one fourth-order step
21 // of the six-state system, assembling the acceleration from the projectile
22 // parameters and using axpy for state algebra. Implement `simulate` by marching
23 // from the launch state, reporting apex and ground impact as sub-step events
24 // rather than grid samples, and filling EVERY Flight field exactly per the
25 // header contract, including range as horizontal distance from launch, time as
26 // steps*dt, and the impacted / has_apex flags. Honor the dt > 0 contract.
27 // ============================================================================
28
29 State Integrator::step(const State& s, double dt) const {
30 if (!(dt > 0.0)) {
31 throw std::invalid_argument("Integrator::step: dt must be positive");
32 }
33 // TODO: replace with one fourth-order step over [0, dt].
34 (void)projectile_;
35 return s;
36 }
37
38 Flight Integrator::simulate(const State& initial, double dt,
39 std::size_t max_steps, double z_impact) const {
40 if (!(dt > 0.0)) {
41 throw std::invalid_argument(
42 "Integrator::simulate: dt must be positive");
43 }
44 (void)max_steps;
45 (void)z_impact;
46 // TODO: march, locate the apex and impact, and fill the Flight.
47 Flight f;
48 f.states.push_back(initial);
49 f.times.push_back(0.0);
50 f.steps = 0;
51 f.time = 0.0;
52 f.impacted = false;
53 f.has_apex = false;
54 return f;
55 }
56
57 } // namespace ballistics
58
List project structure
ls -R include src tests 2>/dev/null | head -50
include: ballistics include/ballistics: integrator.hpp io.hpp model.hpp types.hpp src: io.cpp main.cpp model.cpp solver.cpp tests: CMakeLists.txt test_smoke.cpp test_util.hpp [stdout] include: ballistics include/ballistics: integrator.hpp io.hpp model.hpp types.hpp src: io.cpp main.cpp model.cpp solver.cpp tests: CMakeLists.txt test_smoke.cpp test_util.hpp
/opt/ballistics/include/ballistics/types.hpp
1 #ifndef BALLISTICS_TYPES_HPP
2 #define BALLISTICS_TYPES_HPP
3
4 #include <cmath>
5
6 namespace ballistics {
7
8 /// A 3D Cartesian vector [m] / [m/s] / [m/s^2] depending on context.
9 /// Plain value type with the small amount of vector algebra the dynamics need.
10 struct Vec3 {
11 double x{0.0};
12 double y{0.0};
13 double z{0.0};
14 };
15
16 inline Vec3 operator+(const Vec3& a, const Vec3& b) {
17 return Vec3{a.x + b.x, a.y + b.y, a.z + b.z};
18 }
19 inline Vec3 operator-(const Vec3& a, const Vec3& b) {
20 return Vec3{a.x - b.x, a.y - b.y, a.z - b.z};
21 }
22 inline Vec3 operator*(double s, const Vec3& a) {
23 return Vec3{s * a.x, s * a.y, s * a.z};
24 }
25 inline double dot(const Vec3& a, const Vec3& b) {
26 return a.x * b.x + a.y * b.y + a.z * b.z;
27 }
28 /// Right-handed cross product a x b.
29 inline Vec3 cross(const Vec3& a, const Vec3& b) {
30 return Vec3{a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z,
31 a.x * b.y - a.y * b.x};
32 }
33 inline double norm(const Vec3& a) { return std::sqrt(dot(a, a)); }
34
35 /// Phase-space state of a point-mass projectile in 3D: position r = (x, y, z)
36 /// [m] with z measured upward, and velocity v = (vx, vy, vz) [m/s]. Plain value
37 /// type used throughout. The dynamics are autonomous (time does not appear
38 /// explicitly), so the integrator carries the simulation clock separately.
39 struct State {
40 Vec3 r{};
41 Vec3 v{};
42 };
43
44 /// Element-wise scaled-add over the full 6-component state: returns a + h * b.
45 /// Keep this helper here so the integrator stays a thin numerical layer.
46 /// (Support code.)
47 inline State axpy(const State& a, double h, const State& b) {
48 return State{Vec3{a.r.x + h * b.r.x, a.r.y + h * b.r.y, a.r.z + h * b.r.z},
49 Vec3{a.v.x + h * b.v.x, a.v.y + h * b.v.y, a.v.z + h * b.v.z}};
50 }
51
52 } // namespace ballistics
53
54 #endif // BALLISTICS_TYPES_HPP
55
/opt/ballistics/include/ballistics/integrator.hpp
1 #ifndef BALLISTICS_INTEGRATOR_HPP
2 #define BALLISTICS_INTEGRATOR_HPP
3
4 #include <cstddef>
5 #include <vector>
6
7 #include "ballistics/model.hpp"
8 #include "ballistics/types.hpp"
9
10 namespace ballistics {
11
12 /// Result of a 3D projectile flight simulation.
13 ///
14 /// `states[i]` is the fixed-step sample at grid time `times[i] = i * dt`, with
15 /// states[0] the launch state. The apex and impact are sub-step events (not the
16 /// nearest grid sample), reported to high accuracy.
17 struct Flight {
18 /// Grid samples; states[0] is the launch state.
19 std::vector<State> states;
20 /// Simulation time [s] of each sample (times[i] = i * dt). Parallel to
21 /// `states`, supplied so energy budgets can be integrated directly.
22 std::vector<double> times;
23
24 /// Number of grid steps actually taken (== states.size() - 1).
25 std::size_t steps{0};
26 /// Final grid time [s] = steps * dt (NOT the root-found impact time).
27 double time{0.0};
28
29 /// True iff the run terminated at a ground-plane impact (z crossing
30 /// z_impact from above). False if it stopped at the step cap instead, in
31 /// which case the impact fields below are not meaningful.
32 bool impacted{false};
33
34 /// Root-found apex (highest point: vz crosses zero downward). Valid once at
35 /// least one apex crossing has been seen.
36 State apex{};
37 double apex_time{0.0};
38 bool has_apex{false};
39
40 /// Root-found impact state and time (z == z_impact, descending). Valid iff
41 /// `impacted` is true.
42 State impact{};
43 double impact_time{0.0};
44
45 /// Horizontal range [m] from the launch point to the impact point, i.e.
46 /// sqrt(dx^2 + dy^2) measured in the ground (x, y) plane. Zero unless the
47 /// projectile impacted.
48 double range{0.0};
49 };
50
51 /// Fixed-step fourth-order integrator for the 3D projectile, augmented with
52 /// sub-step event reporting for the apex and ground impact.
53 class Integrator {
54 public:
55 explicit Integrator(const Projectile& projectile)
56 : projectile_(projectile) {}
57
58 /// Advance a single state by one fourth-order step of size `dt`. `dt` must be > 0
59 /// (throws std::invalid_argument otherwise).
60 State step(const State& s, double dt) const;
61
62 /// Simulate the flight from `initial`, taking fixed steps of size `dt`, until
63 /// EITHER the projectile crosses the impact plane z = `z_impact` while
64 /// descending OR `max_steps` steps have been taken, whichever comes first.
65 ///
66 /// The launch state is recorded as states[0]; each stepped grid state is
67 /// appended. The apex (vz: + -> -) and the impact (z crossing z_impact from
68 /// above) are located as sub-step events and stored in `apex` / `impact`
69 /// with their times; `range` is the horizontal
70 /// launch-to-impact distance. `dt` must be > 0 (throws
71 /// std::invalid_argument otherwise). Returns a Flight with
72 /// steps == states.size() - 1, time == steps * dt, and `impacted` /
73 /// `has_apex` set appropriately.
74 Flight simulate(const State& initial, double dt, std::size_t max_steps,
75 double z_impact = 0.0) const;
76
77 /// Alias for simulate(), matching exterior-ballistics "fire a shot"
78 /// terminology used by the CLI and the .shot format.
79 Flight fire(const State& initial, double dt, std::size_t max_steps,
80 double z_impact = 0.0) const {
81 return simulate(initial, dt, max_steps, z_impact);
82 }
83
84 private:
85 const Projectile& projectile_;
86 };
87
88 } // namespace ballistics
89
90 #endif // BALLISTICS_INTEGRATOR_HPP
91
/opt/ballistics/include/ballistics/model.hpp
1 #ifndef BALLISTICS_MODEL_HPP
2 #define BALLISTICS_MODEL_HPP
3
4 #include "ballistics/types.hpp"
5
6 namespace ballistics {
7
8 /// Two-layer model atmosphere. Air density decays with altitude using one scale
9 /// height below a transition altitude and a second scale height above it, with
10 /// the density kept continuous at the transition. Fully implemented support code.
11 class Atmosphere {
12 public:
13 /// Construct from sea-level density and lower-layer scale height. The
14 /// default transition is 400 m, and ordinary atmospheric cases use a shorter
15 /// upper-layer scale height; near-uniform atmospheres keep the supplied scale
16 /// height in both layers. Throws std::invalid_argument on invalid parameters.
17 Atmosphere(double rho0, double scale_height);
18
19 /// Construct with explicit lower-layer scale height, transition altitude, and
20 /// upper-layer scale height.
21 Atmosphere(double rho0, double lower_scale_height,
22 double transition_altitude, double upper_scale_height);
23
24 /// A vacuum (rho0 = 0): density is identically zero at every altitude.
25 static Atmosphere vacuum();
26
27 double rho0() const { return rho0_; }
28 double scale_height() const { return lower_scale_height_; }
29 double lower_scale_height() const { return lower_scale_height_; }
30 double transition_altitude() const { return transition_altitude_; }
31 double upper_scale_height() const { return upper_scale_height_; }
32
33 /// Air density at altitude z [m]. For rho0 = 0 this is identically 0.
34 double density(double z) const;
35
36 private:
37 double rho0_;
38 double lower_scale_height_;
39 double transition_altitude_;
40 double upper_scale_height_;
41 };
42
43 /// A point-mass projectile flying through a model atmosphere under uniform
44 /// gravity, quadratic aerodynamic drag, spin/Magnus deflection, and an optional
45 /// rotating-frame Coriolis term. The state is the full 3D phase vector s = (r, v)
46 /// with z up.
47 ///
48 /// Drag opposes the air-relative velocity, where air-relative velocity means the
49 /// projectile velocity minus the constant wind. The drag magnitude scales with
50 /// air density and the square of the air-relative speed. The spin/Magnus force is
51 /// perpendicular to the air-relative velocity and is directed by the right-handed
52 /// spin axis, so it curves the path without doing mechanical work. The Coriolis
53 /// convention is the rotating-frame acceleration from the stored omega vector.
54 ///
55 /// This class is a fully-implemented data container plus energy diagnostics; the
56 /// numerical integrator assembles the dynamics from these parameters.
57 class Projectile {
58 public:
59 /// Construct from mass [kg], drag coefficient Cd [-], reference area A
60 /// [m^2], gravity g [m/s^2], the atmosphere, the constant wind vector w
61 /// [m/s], the spin vector S [rad/s] (only its direction matters; sets the
62 /// Magnus axis), the Magnus lift coefficient Cl [-], and optionally the
63 /// rotating-frame angular-velocity vector omega [rad/s].
64 ///
65 /// Throws std::invalid_argument if mass <= 0, Cd < 0, area < 0, g < 0, or
66 /// Cl < 0.
67 Projectile(double mass, double Cd, double area, double gravity,
68 const Atmosphere& atmosphere, const Vec3& wind, const Vec3& spin,
69 double Cl, const Vec3& omega = Vec3{});
70
71 double mass() const { return mass_; }
72 double Cd() const { return Cd_; }
73 double area() const { return area_; }
74 double gravity() const { return gravity_; }
75 double Cl() const { return Cl_; }
76 const Vec3& wind() const { return wind_; }
77 const Vec3& spin() const { return spin_; }
78 const Vec3& omega() const { return omega_; }
79 const Atmosphere& atmosphere() const { return atmosphere_; }
80
81 /// Air density at altitude z [m] (delegates to the atmosphere).
82 double density(double z) const { return atmosphere_.density(z); }
83
84 /// Ground speed magnitude |v| [m/s]. Fully implemented.
85 double speed(const State& s) const { return norm(s.v); }
86
87 /// Total mechanical energy per unit mass E/m = 1/2 |v|^2 + g z. For the
88 /// no-wind case this decreases monotonically with aerodynamic dissipation.
89 /// Fully implemented.
90 double specific_energy(const State& s) const;
91
92 /// Instantaneous mechanical-energy dissipation rate per unit mass. In the
93 /// no-wind case this equals -d/dt(specific_energy). Fully implemented.
94 double drag_power(const State& s) const;
95
96 private:
97 double mass_;
98 double Cd_;
99 double area_;
100 double gravity_;
101 Atmosphere atmosphere_;
102 Vec3 wind_;
103 Vec3 spin_;
104 Vec3 omega_;
105 double Cl_;
106 };
107
108 } // namespace ballistics
109
110 #endif // BALLISTICS_MODEL_HPP
111
/opt/ballistics/src/model.cpp
1 #include "ballistics/model.hpp"
2
3 #include <cmath>
4 #include <stdexcept>
5
6 namespace ballistics {
7
8 namespace {
9
10 double default_upper_scale_height(double lower_scale_height) {
11 return lower_scale_height > 1.0e9 ? lower_scale_height : 6200.0;
12 }
13
14 } // namespace
15
16 Atmosphere::Atmosphere(double rho0, double scale_height)
17 : Atmosphere(rho0, scale_height, 400.0,
18 default_upper_scale_height(scale_height)) {}
19
20 Atmosphere::Atmosphere(double rho0, double lower_scale_height,
21 double transition_altitude, double upper_scale_height)
22 : rho0_(rho0),
23 lower_scale_height_(lower_scale_height),
24 transition_altitude_(transition_altitude),
25 upper_scale_height_(upper_scale_height) {
26 if (rho0_ < 0.0) {
27 throw std::invalid_argument("Atmosphere: rho0 must be non-negative");
28 }
29 if (!(lower_scale_height_ > 0.0)) {
30 throw std::invalid_argument(
31 "Atmosphere: lower scale height must be positive");
32 }
33 if (!(upper_scale_height_ > 0.0)) {
34 throw std::invalid_argument(
35 "Atmosphere: upper scale height must be positive");
36 }
37 if (transition_altitude_ < 0.0) {
38 throw std::invalid_argument(
39 "Atmosphere: transition altitude must be non-negative");
40 }
41 }
42
43 Atmosphere Atmosphere::vacuum() { return Atmosphere(0.0, 1.0); }
44
45 double Atmosphere::density(double z) const {
46 if (rho0_ == 0.0) return 0.0;
47 if (z <= transition_altitude_) {
48 return rho0_ * std::exp(-z / lower_scale_height_);
49 }
50 const double rho_transition =
51 rho0_ * std::exp(-transition_altitude_ / lower_scale_height_);
52 return rho_transition *
53 std::exp(-(z - transition_altitude_) / upper_scale_height_);
54 }
55
56 Projectile::Projectile(double mass, double Cd, double area, double gravity,
57 const Atmosphere& atmosphere, const Vec3& wind,
58 const Vec3& spin, double Cl, const Vec3& omega)
59 : mass_(mass),
60 Cd_(Cd),
61 area_(area),
62 gravity_(gravity),
63 atmosphere_(atmosphere),
64 wind_(wind),
65 spin_(spin),
66 omega_(omega),
67 Cl_(Cl) {
68 if (!(mass_ > 0.0)) {
69 throw std::invalid_argument("Projectile: mass must be positive");
70 }
71 if (Cd_ < 0.0) {
72 throw std::invalid_argument("Projectile: Cd must be non-negative");
73 }
74 if (area_ < 0.0) {
75 throw std::invalid_argument("Projectile: area must be non-negative");
76 }
77 if (gravity_ < 0.0) {
78 throw std::invalid_argument("Projectile: gravity must be non-negative");
79 }
80 if (Cl_ < 0.0) {
81 throw std::invalid_argument("Projectile: Cl must be non-negative");
82 }
83 }
84
85 double Projectile::specific_energy(const State& s) const {
86 const double v = speed(s);
87 return 0.5 * v * v + gravity_ * s.r.z;
88 }
89
90 double Projectile::drag_power(const State& s) const {
91 const double rho = density(s.r.z);
92 const Vec3 vrel = s.v - wind_;
93 const double q = norm(vrel);
94 const double kd = rho * Cd_ * area_ / (2.0 * mass_);
95 return kd * q * dot(s.v, vrel);
96 }
97
98 } // namespace ballistics
99
/opt/ballistics/tests/test_smoke.cpp
1 #include <stdexcept>
2
3 #include "ballistics/integrator.hpp"
4 #include "ballistics/model.hpp"
5 #include "ballistics/types.hpp"
6 #include "test_util.hpp"
7
8 using namespace ballistics;
9
10 namespace {
11
12 Projectile vacuum_projectile() {
13 return Projectile(1.0, 0.0, 0.0, 9.80665, Atmosphere::vacuum(),
14 Vec3{0.0, 0.0, 0.0}, Vec3{0.0, 0.0, 0.0}, 0.0);
15 }
16
17 } // namespace
18
19 BALLISTICS_TEST("vacuum_flight_smoke") {
20 const Projectile projectile = vacuum_projectile();
21 Integrator integrator(projectile);
22 const State launch{Vec3{0.0, 0.0, 0.0}, Vec3{40.0, 2.0, 45.0}};
23 const Flight flight = integrator.simulate(launch, 0.01, 20000, 0.0);
24
25 ballistics_test::expect(flight.impacted, "flight reports an impact");
26 ballistics_test::expect(flight.has_apex, "flight reports an apex");
27 ballistics_test::expect(flight.states.size() > 100, "many samples recorded");
28 ballistics_test::expect(flight.steps == flight.states.size() - 1,
29 "step count matches samples");
30 ballistics_test::expect(flight.times.size() == flight.states.size(),
31 "times parallel states");
32 ballistics_test::expect(flight.apex.r.z > launch.r.z, "apex is above launch");
33 ballistics_test::expect(flight.range > 0.0, "impact range is positive");
34 ballistics_test::expect(flight.impact.v.z < 0.0, "impact is descending");
35 }
36
37 BALLISTICS_TEST("step_cap_smoke") {
38 const Projectile projectile = vacuum_projectile();
39 Integrator integrator(projectile);
40 const State launch{Vec3{0.0, 0.0, 0.0}, Vec3{20.0, 0.0, 80.0}};
41 const std::size_t cap = 10;
42 const Flight flight = integrator.simulate(launch, 0.001, cap, -1.0e9);
43 ballistics_test::expect(!flight.impacted, "cap run does not impact");
44 ballistics_test::expect(flight.steps == cap, "cap run honors max_steps");
45 ballistics_test::expect(flight.states.back().r.z > flight.states.front().r.z,
46 "cap run advances upward");
47 }
48
49 BALLISTICS_TEST("nonpositive_dt_throws") {
50 const Projectile projectile = vacuum_projectile();
51 Integrator integrator(projectile);
52 const State launch{};
53 bool threw_step = false;
54 bool threw_sim = false;
55 try {
56 (void)integrator.step(launch, 0.0);
57 } catch (const std::invalid_argument&) {
58 threw_step = true;
59 }
60 try {
61 (void)integrator.simulate(launch, -0.01, 1, 0.0);
62 } catch (const std::invalid_argument&) {
63 threw_sim = true;
64 }
65 ballistics_test::expect(threw_step, "step rejects dt <= 0");
66 ballistics_test::expect(threw_sim, "simulate rejects dt <= 0");
67 }
68
69 BALLISTICS_TEST_MAIN()
70
/opt/ballistics/tests/test_util.hpp
1 #ifndef BALLISTICS_TEST_UTIL_HPP
2 #define BALLISTICS_TEST_UTIL_HPP
3
4 // Minimal dependency-free test harness for the ballistics suite.
5 //
6 // Each test executable defines test functions and registers them with
7 // BALLISTICS_TEST(name) { ... }. main() (provided by BALLISTICS_TEST_MAIN) runs
8 // every registered test and returns non-zero if any assertion fails, which
9 // CTest interprets as a failure.
10
11 #include <cmath>
12 #include <cstddef>
13 #include <functional>
14 #include <iostream>
15 #include <string>
16 #include <vector>
17
18 namespace ballistics_test {
19
20 struct Case {
21 std::string name;
22 std::function<void()> fn;
23 };
24
25 inline std::vector<Case>& registry() {
26 static std::vector<Case> cases;
27 return cases;
28 }
29
30 struct Registrar {
31 Registrar(const std::string& name, std::function<void()> fn) {
32 registry().push_back(Case{name, std::move(fn)});
33 }
34 };
35
36 // Thrown by an assertion failure to abort the current test case.
37 struct AssertionError {
38 std::string message;
39 };
40
41 inline void expect(bool cond, const std::string& what) {
42 if (!cond) {
43 throw AssertionError{what};
44 }
45 }
46
47 inline void expect_near(double got, double want, double tol, const std::string& what) {
48 if (std::fabs(got - want) > tol) {
49 throw AssertionError{what + " (got " + std::to_string(got) +
50 ", want " + std::to_string(want) +
51 ", tol " + std::to_string(tol) + ")"};
52 }
53 }
54
55 // Relative-error check (with an absolute floor) for quantities whose scale
56 // varies widely across cases.
57 inline void expect_rel(double got, double want, double rel_tol,
58 const std::string& what) {
59 const double denom = std::fabs(want) > 1e-300 ? std::fabs(want) : 1.0;
60 if (std::fabs(got - want) / denom > rel_tol) {
61 throw AssertionError{what + " (got " + std::to_string(got) +
62 ", want " + std::to_string(want) +
63 ", rel_tol " + std::to_string(rel_tol) + ")"};
64 }
65 }
66
67 inline int run_all() {
68 int failures = 0;
69 for (const auto& c : registry()) {
70 try {
71 c.fn();
72 std::cout << "[ PASS ] " << c.name << "\n";
73 } catch (const AssertionError& e) {
74 std::cout << "[ FAIL ] " << c.name << ": " << e.message << "\n";
75 ++failures;
76 } catch (const std::exception& e) {
77 std::cout << "[ FAIL ] " << c.name
78 << ": unexpected exception: " << e.what() << "\n";
79 ++failures;
80 } catch (...) {
81 std::cout << "[ FAIL ] " << c.name << ": unknown exception\n";
82 ++failures;
83 }
84 }
85 std::cout << "----\n"
86 << (registry().size() - failures) << "/" << registry().size()
87 << " tests passed\n";
88 return failures == 0 ? 0 : 1;
89 }
90
91 } // namespace ballistics_test
92
93 #define BALLISTICS_CONCAT_INNER(a, b) a##b
94 #define BALLISTICS_CONCAT(a, b) BALLISTICS_CONCAT_INNER(a, b)
95
96 #define BALLISTICS_TEST(NAME) \
97 static void BALLISTICS_CONCAT(ballistics_test_fn_, __LINE__)(); \
98 static ::ballistics_test::Registrar BALLISTICS_CONCAT( \
99 ballistics_test_reg_, __LINE__){ \
100 NAME, &BALLISTICS_CONCAT(ballistics_test_fn_, __LINE__)}; \
101 static void BALLISTICS_CONCAT(ballistics_test_fn_, __LINE__)()
102
103 #define BALLISTICS_TEST_MAIN() \
104 int main() { return ::ballistics_test::run_all(); }
105
106 #endif // BALLISTICS_TEST_UTIL_HPP
107
Inspect CMake and io header
cat /opt/ballistics/tests/CMakeLists.txt; echo "---ROOT---"; cat /opt/ballistics/CMakeLists.txt 2>/dev/null | head -60; echo "---IO---"; sed -n '1,40p' /opt/ballistics/include/ballistics/io.hpp
# Smoke test only. The hidden verifier supplies the authoritative grading suite.
add_executable(test_smoke test_smoke.cpp)
target_link_libraries(test_smoke PRIVATE ballistics)
target_include_directories(test_smoke PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME smoke COMMAND test_smoke)
---ROOT---
cmake_minimum_required(VERSION 3.16)
project(ballistics LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
add_compile_options(-Wall -Wextra)
# --- Core library -----------------------------------------------------------
add_library(ballistics
src/model.cpp
src/solver.cpp
src/io.cpp
)
target_include_directories(ballistics PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# --- Demo CLI ---------------------------------------------------------------
add_executable(ballistics_cli src/main.cpp)
target_link_libraries(ballistics_cli PRIVATE ballistics)
# --- Tests ------------------------------------------------------------------
enable_testing()
add_subdirectory(tests)
---IO---
#ifndef BALLISTICS_IO_HPP
#define BALLISTICS_IO_HPP
#include <cstddef>
#include <iosfwd>
#include <string>
#include "ballistics/integrator.hpp"
#include "ballistics/model.hpp"
#include "ballistics/types.hpp"
namespace ballistics {
/// A complete scenario parsed from the `.shot` text format.
struct Scenario {
Projectile projectile;
State launch;
double dt{0.0};
std::size_t max_steps{0};
double z_impact{0.0};
};
/// Parse a scenario from the engine's plain-text `.shot` format.
///
/// The format is line-oriented. Blank lines and lines beginning with '#' are
/// ignored. Recognized records:
/// mass <m> # projectile mass [kg] (required)
/// drag <Cd> <area> # drag coefficient and area [m^2] (required)
/// gravity <g> # gravity [m/s^2] (default 9.80665)
/// atmosphere <rho0> <H> # sea-level density, lower scale height
/// # (default 1.225 8500); rho0=0 vacuum
/// wind <wx> <wy> <wz> # constant wind vector [m/s] (default 0 0 0)
/// spin <sx> <sy> <sz> # spin vector [rad/s], sets Magnus axis
/// # (default 0 0 0)
/// magnus <Cl> # Magnus lift coefficient (default 0)
/// omega <ox> <oy> <oz> # rotating-frame angular velocity [rad/s]
/// # (default 0 0 0)
/// position <x> <y> <z> # launch position [m] (default 0 0 0)
/// launch <speed> <elev_deg> <azim_deg>
/// # launch speed [m/s], elevation above the
[stdout]
# Smoke test only. The hidden verifier supplies the authoritative grading suite.
add_executable(test_smoke test_smoke.cpp)
target_link_libraries(test_smoke PRIVATE ballistics)
target_include_directories(test_smoke PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME smoke COMMAND test_smoke)
---ROOT---
cmake_minimum_required(VERSION 3.16)
project(ballistics LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
add_compile_options(-Wall -Wextra)
# --- Core library -----------------------------------------------------------
add_library(ballistics
src/model.cpp
src/solver.cpp
src/io.cpp
)
target_include_directories(ballistics PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# --- Demo CLI ---------------------------------------------------------------
add_executable(ballistics_cli src/main.cpp)
target_link_libraries(ballistics_cli PRIVATE ballistics)
# --- Tests ------------------------------------------------------------------
enable_testing()
add_subdirectory(tests)
---IO---
#ifndef BALLISTICS_IO_HPP
#define BALLISTICS_IO_HPP
#include <cstddef>
#include <iosfwd>
#include <string>
#include "ballistics/integrator.hpp"
#include "ballistics/model.hpp"
#include "ballistics/types.hpp"
namespace ballistics {
/// A complete scenario parsed from the `.shot` text format.
struct Scenario {
Projectile projectile;
State launch;
double dt{0.0};
std::size_t max_steps{0};
double z_impact{0.0};
};
/// Parse a scenario from the engine's plain-text `.shot` format.
///
/// The format is line-oriented. Blank lines and lines beginning with '#' are
/// ignored. Recognized records:
/// mass <m> # projectile mass [kg] (required)
/// drag <Cd> <area> # drag coefficient and area [m^2] (required)
/// gravity <g> # gravity [m/s^2] (default 9.80665)
/// atmosphere <rho0> <H> # sea-level density, lower scale height
/// # (default 1.225 8500); rho0=0 vacuum
/// wind <wx> <wy> <wz> # constant wind vector [m/s] (default 0 0 0)
/// spin <sx> <sy> <sz> # spin vector [rad/s], sets Magnus axis
/// # (default 0 0 0)
/// magnus <Cl> # Magnus lift coefficient (default 0)
/// omega <ox> <oy> <oz> # rotating-frame angular velocity [rad/s]
/// # (default 0 0 0)
/// position <x> <y> <z> # launch position [m] (default 0 0 0)
/// launch <speed> <elev_deg> <azim_deg>
/// # launch speed [m/s], elevation above the/opt/ballistics/src/solver.cpp
State Integrator::step(const State& s, double dt) const {
if (!(dt > 0.0)) {
throw std::invalid_argument("Integrator::step: dt must be positive");
}
// TODO: replace with one fourth-order step over [0, dt].
(void)projectile_;
return s;
}
Flight Integrator::simulate(const State& initial, double dt,
std::size_t max_steps, double z_impact) const {
if (!(dt > 0.0)) {
throw std::invalid_argument(
"Integrator::simulate: dt must be positive");
}
(void)max_steps;
(void)z_impact;
// TODO: march, locate the apex and impact, and fill the Flight.
Flight f;
f.states.push_back(initial);
f.times.push_back(0.0);
f.steps = 0;
f.time = 0.0;
f.impacted = false;
f.has_apex = false;
return f;
}namespace {
/// Right-hand side of the autonomous 6-state system: returns the time
/// derivative ds/dt = (dr/dt, dv/dt) = (v, a) packed into a State, where the
/// `.r` slot holds dr/dt = v and the `.v` slot holds dv/dt = a.
State derivative(const Projectile& p, const State& s) {
const Vec3& v = s.v;
// Air-relative velocity (projectile velocity minus the constant wind).
const Vec3 vrel = v - p.wind();
const double q = norm(vrel);
const double rho = p.density(s.r.z);
// Acceleration accumulator. Start with uniform gravity (z up).
Vec3 a{0.0, 0.0, -p.gravity()};
if (rho > 0.0 && q > 0.0) {
// Quadratic drag opposing the air-relative velocity. The coefficient
// matches Projectile::drag_power exactly: kd = rho*Cd*A/(2 m), and the
// force magnitude scales with |vrel|^2, so the acceleration is
// -kd * |vrel| * vrel.
const double kd = rho * p.Cd() * p.area() / (2.0 * p.mass());
a = a + (-kd * q) * vrel;
// Spin/Magnus deflection: perpendicular to vrel, directed by the
// right-handed spin axis (only the spin direction matters). Same
// density/area/mass scaling as drag but with the Magnus coefficient Cl;
// magnitude scales with |vrel|^2 and the direction is shat x vrel.
// Being perpendicular to v it does no mechanical work.
const double spin_mag = norm(p.spin());
if (p.Cl() > 0.0 && spin_mag > 0.0) {
const double kl = rho * p.Cl() * p.area() / (2.0 * p.mass());
const Vec3 shat = (1.0 / spin_mag) * p.spin();
a = a + (kl * q) * cross(shat, vrel);
}
}
// Optional rotating-frame Coriolis term: a_cor = -2 omega x v.
const Vec3& omega = p.omega();
if (omega.x != 0.0 || omega.y != 0.0 || omega.z != 0.0) {
a = a + (-2.0) * cross(omega, v);
}
return State{v, a};
}
} // namespace
State Integrator::step(const State& s, double dt) const {
if (!(dt > 0.0)) {
throw std::invalid_argument("Integrator::step: dt must be positive");
}
// Classic fourth-order Runge-Kutta over the full six-state system.
const State k1 = derivative(projectile_, s);
const State k2 = derivative(projectile_, axpy(s, 0.5 * dt, k1));
const State k3 = derivative(projectile_, axpy(s, 0.5 * dt, k2));
const State k4 = derivative(projectile_, axpy(s, dt, k3));
// increment = k1 + 2 k2 + 2 k3 + k4
State increment = axpy(k1, 2.0, k2);
increment = axpy(increment, 2.0, k3);
increment = axpy(increment, 1.0, k4);
return axpy(s, dt / 6.0, increment);
}
Flight Integrator::simulate(const State& initial, double dt,
std::size_t max_steps, double z_impact) const {
if (!(dt > 0.0)) {
throw std::invalid_argument(
"Integrator::simulate: dt must be positive");
}
Flight f;
f.states.push_back(initial);
f.times.push_back(0.0);
// Locate a sub-step event time tau in (0, dt] where g(step(prev, tau)) == 0
// by bisection, given that g changes sign across [0, dt]. `g` is evaluated
// by sub-stepping from `prev` with the same integrator, so the event state
// is consistent with the high-order flight path.
const auto refine = [&](const State& prev, double lo_t, double hi_t,
auto g) {
double lo = lo_t;
double hi = hi_t;
for (int iter = 0; iter < 100 && (hi - lo) > 1e-15; ++iter) {
const double mid = 0.5 * (lo + hi);
// mid may be 0 only if lo==hi==0, which the loop guard excludes.
const State sm = this->step(prev, mid);
if (g(prev) * g(sm) <= 0.0) {
hi = mid;
} else {
lo = mid;
}
}
return 0.5 * (lo + hi);
};
State prev = initial;
for (std::size_t i = 1; i <= max_steps; ++i) {
const State cur = this->step(prev, dt);
const double t_prev = static_cast<double>(i - 1) * dt;
const double t_cur = static_cast<double>(i) * dt;
f.states.push_back(cur);
f.times.push_back(t_cur);
// Apex: the first time vz crosses from positive to non-positive.
if (!f.has_apex && prev.v.z > 0.0 && cur.v.z <= 0.0) {
const auto vz = [](const State& s) { return s.v.z; };
const double tau = refine(prev, 0.0, dt, vz);
f.apex = this->step(prev, tau);
f.apex_time = t_prev + tau;
f.has_apex = true;
}
// Impact: z crosses the impact plane from above while descending.
if (prev.r.z > z_impact && cur.r.z <= z_impact) {
const auto zrel = [&](const State& s) { return s.r.z - z_impact; };
const double tau = refine(prev, 0.0, dt, zrel);
f.impact = this->step(prev, tau);
f.impact_time = t_prev + tau;
const double dx = f.impact.r.x - initial.r.x;
const double dy = f.impact.r.y - initial.r.y;
f.range = std::sqrt(dx * dx + dy * dy);
f.impacted = true;
break;
}
prev = cur;
}
f.steps = f.states.size() - 1;
f.time = static_cast<double>(f.steps) * dt;
return f;
}Configure, build, and test
cmake -S /opt/ballistics -B /opt/ballistics/build -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1 && cmake --build /opt/ballistics/build --parallel 2>&1 | tail -20 && cd /opt/ballistics/build && ctest --output-on-failure 2>&1 | tail -20
[ 25%] Building CXX object CMakeFiles/ballistics.dir/src/solver.cpp.o
[ 12%] Building CXX object CMakeFiles/ballistics.dir/src/model.cpp.o
[ 37%] Building CXX object CMakeFiles/ballistics.dir/src/io.cpp.o
[ 50%] Linking CXX static library libballistics.a
[ 50%] Built target ballistics
[ 75%] Building CXX object tests/CMakeFiles/test_smoke.dir/test_smoke.cpp.o
[ 75%] Building CXX object CMakeFiles/ballistics_cli.dir/src/main.cpp.o
[ 87%] Linking CXX executable ballistics_cli
[ 87%] Built target ballistics_cli
[100%] Linking CXX executable test_smoke
[100%] Built target test_smoke
Test project /opt/ballistics/build
Start 1: smoke
1/1 Test #1: smoke ............................ Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.00 sec
[stdout]
[ 25%] Building CXX object CMakeFiles/ballistics.dir/src/solver.cpp.o
[ 12%] Building CXX object CMakeFiles/ballistics.dir/src/model.cpp.o
[ 37%] Building CXX object CMakeFiles/ballistics.dir/src/io.cpp.o
[ 50%] Linking CXX static library libballistics.a
[ 50%] Built target ballistics
[ 75%] Building CXX object tests/CMakeFiles/test_smoke.dir/test_smoke.cpp.o
[ 75%] Building CXX object CMakeFiles/ballistics_cli.dir/src/main.cpp.o
[ 87%] Linking CXX executable ballistics_cli
[ 87%] Built target ballistics_cli
[100%] Linking CXX executable test_smoke
[100%] Built target test_smoke
Test project /opt/ballistics/build
Start 1: smoke
1/1 Test #1: smoke ............................ Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.00 secAnalytic vacuum trajectory verification
cat > /tmp/chk.cpp <<'EOF'
#include <cstdio>
#include <cmath>
#include "ballistics/integrator.hpp"
#include "ballistics/model.hpp"
using namespace ballistics;
int main(){
Projectile p(1,0,0,9.80665,Atmosphere::vacuum(),{0,0,0},{0,0,0},0);
Integrator it(p);
State l{{0,0,0},{40,2,45}};
Flight f=it.simulate(l,0.001,200000,0.0);
double g=9.80665, vz=45, vh=std::sqrt(40.0*40+2*2);
printf("apex_t got %.6f want %.6f\n", f.apex_time, vz/g);
printf("apex_z got %.6f want %.6f\n", f.apex.r.z, vz*vz/(2*g));
printf("imp_t got %.6f want %.6f\n", f.impact_time, 2*vz/g);
printf("range got %.6f want %.6f\n", f.range, vh*2*vz/g);
printf("imp_z got %.2e (want 0)\n", f.impact.r.z);
printf("apex_vz got %.2e (want 0)\n", f.apex.v.z);
return 0;
}
EOF
g++ -std=c++17 -O2 -I/opt/ballistics/include /tmp/chk.cpp -L/opt/ballistics/build -lballistics -o /tmp/chk && /tmp/chkapex_t got 4.588723 want 4.588723 apex_z got 103.246267 want 103.246267 imp_t got 9.177446 want 9.177446 range got 367.556423 want 367.556423 imp_z got -1.77e-14 (want 0) apex_vz got 3.87e-15 (want 0) [stdout] apex_t got 4.588723 want 4.588723 apex_z got 103.246267 want 103.246267 imp_t got 9.177446 want 9.177446 range got 367.556423 want 367.556423 imp_z got -1.77e-14 (want 0) apex_vz got 3.87e-15 (want 0)
No file changes, the agent left the workspace untouched.
== Restoring pristine project ==
== Staging candidate solver ==
== Injecting hidden grading tests ==
== Configuring (cmake) ==
-- The CXX compiler identification is GNU 11.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp/tmp.EQJVgdY0RB/ballistics/build_grade
== Building ==
[ 4%] Building CXX object CMakeFiles/ballistics.dir/src/solver.cpp.o
[ 8%] Building CXX object CMakeFiles/ballistics.dir/src/model.cpp.o
[ 12%] Building CXX object CMakeFiles/ballistics.dir/src/io.cpp.o
[ 16%] Linking CXX static library libballistics.a
[ 16%] Built target ballistics
[ 20%] Building CXX object CMakeFiles/ballistics_cli.dir/src/main.cpp.o
[ 25%] Building CXX object tests/CMakeFiles/test_vacuum.dir/test_vacuum.cpp.o
[ 29%] Building CXX object tests/CMakeFiles/test_step.dir/test_step.cpp.o
[ 33%] Building CXX object tests/CMakeFiles/test_events.dir/test_events.cpp.o
[ 41%] Building CXX object tests/CMakeFiles/test_drag.dir/test_drag.cpp.o
[ 45%] Building CXX object tests/CMakeFiles/test_events_dp.dir/test_events_dp.cpp.o
[ 45%] Building CXX object tests/CMakeFiles/test_lateral.dir/test_lateral.cpp.o
[ 54%] Building CXX object tests/CMakeFiles/test_consistency.dir/test_consistency.cpp.o
[ 50%] Building CXX object tests/CMakeFiles/test_book.dir/test_book.cpp.o
[ 58%] Building CXX object tests/CMakeFiles/test_energy.dir/test_energy.cpp.o
[ 62%] Linking CXX executable ballistics_cli
[ 62%] Built target ballistics_cli
[ 66%] Linking CXX executable test_energy
[ 70%] Linking CXX executable test_consistency
[ 75%] Linking CXX executable test_events
[ 75%] Built target test_consistency
[ 79%] Linking CXX executable test_vacuum
[ 79%] Built target test_energy
[ 79%] Built target test_events
[ 83%] Linking CXX executable test_book
[ 83%] Built target test_vacuum
[ 87%] Linking CXX executable test_drag
[ 91%] Linking CXX executable test_lateral
[ 95%] Linking CXX executable test_step
[ 95%] Built target test_drag
[ 95%] Built target test_book
[100%] Linking CXX executable test_events_dp
[100%] Built target test_lateral
[100%] Built target test_step
[100%] Built target test_events_dp
== Running hidden tests ==
Test project /tmp/tmp.EQJVgdY0RB/ballistics/build_grade
Start 1: test_step
1/9 Test #1: test_step ........................ Passed 0.00 sec
Start 2: test_vacuum
2/9 Test #2: test_vacuum ...................... Passed 0.01 sec
Start 3: test_events
3/9 Test #3: test_events ......................***Failed 0.00 sec
[ PASS ] apex_residual_coarse_dt
[ PASS ] impact_residual_coarse_dt
[ FAIL ] degenerate_immediate_impact: immediate impact detected
----
2/3 tests passed
Start 4: test_events_dp
4/9 Test #4: test_events_dp ................... Passed 0.01 sec
Start 5: test_drag
5/9 Test #5: test_drag ........................ Passed 0.01 sec
Start 6: test_lateral
6/9 Test #6: test_lateral ..................... Passed 0.02 sec
Start 7: test_energy
7/9 Test #7: test_energy ...................... Passed 0.01 sec
Start 8: test_book
8/9 Test #8: test_book ........................***Failed 0.02 sec
[ PASS ] step_cap_bookkeeping
[ PASS ] range_is_horizontal_from_launch
[ PASS ] grid_time_not_impact_time
[ PASS ] offaxis_3d_impact_bookkeeping
[ FAIL ] immediate_impact_nonorigin_bookkeeping: immediate impact detected
[ PASS ] apex_strictly_before_impact
[ PASS ] descending_launch_has_no_apex
[ PASS ] step_cap_no_apex_no_impact
[ PASS ] simulate_rejects_nonpositive_dt
----
8/9 tests passed
Start 9: test_consistency
9/9 Test #9: test_consistency ................. Passed 0.01 sec
78% tests passed, 2 tests failed out of 9
Label Time Summary:
hidden = 0.09 sec*proc (9 tests)
Total Test time (real) = 0.09 sec
The following tests FAILED:
3 - test_events (Failed)
8 - test_book (Failed)
Errors while running CTest
FAIL: hidden tests failedReproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_e07e8c70f4e04a7a. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_e07e8c70f4e04a7a · verifier authoritative; classifier explanatory.