diff --git a/.vscode/launch.json b/.vscode/launch.json index e0bf49c..edc0d75 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,7 +29,7 @@ ] }, { - // Локальний debug стартера ДЗ 4. preLaunchTask збирає debug preset. + // debug ДЗ 4. preLaunchTask збирає debug preset. "name": "Debug homework_04: ugv_odometry", "type": "cppdbg", "request": "launch", @@ -75,6 +75,51 @@ "ignoreFailures": true } ] + }, + { + // debug ДЗ 6. preLaunchTask збирає debug preset. + "name": "Debug homework_06: ballistics", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/debug/homework_06/ballistics", + "args": [ + "${workspaceFolder}/homework_06/data/input.txt" + ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "preLaunchTask": "CMake: configure and build", + "setupCommands": [ + { + "description": "Увімкнути pretty-printing для STL типів у GDB.", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + // debug тестів ДЗ 6. preLaunchTask збирає debug preset. + "type": "cppdbg", + "request": "launch", + "name": "Debug homework_06: ballistics_tests", + "program": "${workspaceFolder}/build/debug/homework_06/ballistics_tests", + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "preLaunchTask": "CMake: configure and build", + "setupCommands": [ + { + "description": "Увімкнути pretty-printing для STL типів у GDB.", + "text": "-enable-pretty-printing", + "ignoreFailures": false + } + ] } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a29b2ed..20bfa8f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -32,6 +32,11 @@ "panel": "dedicated", "clear": true } + }, + { + "label": "CTest: run all test", + "type": "shell", + "command": "ctest --test-dir build/debug --output-on-failure" } ] } diff --git a/CMakeLists.txt b/CMakeLists.txt index 19f96e9..633b3aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,8 +15,12 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Домашні роботи. По мірі появи додаємо кожну окремим add_subdirectory. +enable_testing() + add_subdirectory(homework_04) add_subdirectory(homework_05) +add_subdirectory(homework_06) # Demo-код для заняття 2.4: локальне і віддалене відлагодження через GDB. add_subdirectory(demos/lesson_2_4/debug_probe) + diff --git a/homework_06/CMakeLists.txt b/homework_06/CMakeLists.txt new file mode 100644 index 0000000..8412669 --- /dev/null +++ b/homework_06/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 3.20) +project(telemetry_check CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_library(ballistics src/ballistics.cpp) + +target_include_directories(ballistics PUBLIC include) + +add_executable(ballistics_cli src/main.cpp) + +target_link_libraries(ballistics_cli PRIVATE ballistics) + +if(NOT CMAKE_CROSSCOMPILING) + + include(FetchContent) + + include(GoogleTest) + + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + + FetchContent_MakeAvailable(googletest) + + add_executable(ballistics_tests tests/ballistics_tests.cpp) + + target_compile_definitions( + ballistics_tests + PRIVATE TEST_DATA_DIR="${CMAKE_SOURCE_DIR}/homework_06/data") + + target_link_libraries(ballistics_tests PRIVATE ballistics GTest::gtest_main) + + gtest_discover_tests(ballistics_tests DISCOVERY_MODE PRE_TEST) + +endif() diff --git a/homework_06/data/sample_vog17.txt b/homework_06/data/sample_vog17.txt new file mode 100644 index 0000000..5a297de --- /dev/null +++ b/homework_06/data/sample_vog17.txt @@ -0,0 +1 @@ +100 100 100 200 200 10 10 VOG-17 diff --git a/homework_06/data/unknown_ammo.txt b/homework_06/data/unknown_ammo.txt new file mode 100644 index 0000000..dd8481d --- /dev/null +++ b/homework_06/data/unknown_ammo.txt @@ -0,0 +1 @@ +100 100 100 200 200 10 10 F1 \ No newline at end of file diff --git a/homework_06/include/ballistics.hpp b/homework_06/include/ballistics.hpp new file mode 100644 index 0000000..5e96103 --- /dev/null +++ b/homework_06/include/ballistics.hpp @@ -0,0 +1,65 @@ +#include + +struct Coord { + double x; + double y; + double z = 0.0f; +}; + +struct Ammo { + std::string name = "Unknown"; + float mass; // маса (кг) + float drag; // коефіцієнт опору + float lift; // коефіцієнт підйому +}; + +struct DroneConfig { + Coord startPos; // початкова позиція (x, y) + Ammo ammo; + float altitude; // висота + float initialDir; // початковий напрямок (рад) + float attackSpeed; // швидкість атаки (м/с) + float accelPath; // шлях розгону (м) + float arrayTimeStep; // крок часу масиву цілей + float simTimeStep; // крок симуляції + float hitRadius; // радіус влучення + float angularSpeed; // кутова швидкість (рад/с) + float turnThreshold; // поріг повороту (рад) +}; + +struct BallisticsInput { + float droneX; + float droneY; + float droneZ; + float targetX; + float targetY; + float attackSpeed; + float accelerationPath; + std::string ammoName; +}; +struct DropSolution { + float fireX; + float fireY; + bool isManoeuvrePerformed = false; + float manouvreX; + float manouvreY; + std::string errorMessage; +}; + +float calcAmmoFallTime(const Ammo &ammo, const float &attackSpeed, + const float &droneHeight); +float calcDistance(const Coord &a, const Coord &b); +Coord calcFireCoordinates(const float &horizontalDistance, + const float &distanceToTarget, const float &xd, + const float &yd, const float &targetX, + const float &targetY); +float calcHorizontalDistance(const float &fallTime, DroneConfig &drone, + const Coord &targetPosition); +bool isManoeuvreNeeded(const float &horizontalDistance, + const float &accelerationPath, + const float &distanceToTarget); +void processManouvre(DroneConfig &drone, const Coord &targetPosition); +BallisticsInput parseInputFile(const std::string &filePath); +DropSolution computeDropSolution(const BallisticsInput &input); +void writeOutputFile(const std::string &filePath, + const DropSolution &dropSolution); diff --git a/homework_06/src/ballistics.cpp b/homework_06/src/ballistics.cpp new file mode 100644 index 0000000..f1ad452 --- /dev/null +++ b/homework_06/src/ballistics.cpp @@ -0,0 +1,284 @@ +#define _USE_MATH_DEFINES +#include "ballistics.hpp" + +#include +#include +#include +#include +#include + +using namespace std; + +const float GRAVITY_ACCEL = 9.81f; +const int AMMO_TYPES_COUNT = 5; + +const Ammo ARSENAL[AMMO_TYPES_COUNT] = { + {.name = "VOG-17", .mass = 0.35f, .drag = 0.07f, .lift = 0.0f}, + {.name = "M67", .mass = 0.6f, .drag = 0.1f, .lift = 0.0f}, + {.name = "RKG-3", .mass = 1.2f, .drag = 0.1f, .lift = 0.0f}, + {.name = "GLIDING-VOG", .mass = 0.45f, .drag = 0.1f, .lift = 1.0f}, + {.name = "GLIDING-RKG", .mass = 1.4f, .drag = 0.1f, .lift = 1.0f}, +}; + +float calcAmmoFallTime(const Ammo &ammo, const float &attackSpeed, + const float &droneHeight) { + /* + V₀ — швидкість атаки дрона + Z₀ — висота дрона (zd) + g = 9.81 м/с² + + a = d·g·m − 2d²·l·V₀ + b = −3g·m² + 3d·l·m·V₀ + c = 6m²·Z₀ + */ + + float drag2 = ammo.drag * ammo.drag; + float mass2 = ammo.mass * ammo.mass; + float a = ammo.drag * GRAVITY_ACCEL * ammo.mass - + 2.0f * drag2 * ammo.lift * attackSpeed; + float b = -3.0f * GRAVITY_ACCEL * mass2 + + 3.0f * ammo.drag * ammo.lift * ammo.mass * attackSpeed; + float c = 6.0f * mass2 * droneHeight; + + /* + p = − b² / (3a²) + q = 2b³ / (27a³) + c / a + φ = arccos( 3q / (2p) · √(−3/p) ) + t = 2√(−p/3) · cos( (φ + 4π) / 3 ) − b / (3a) + */ + + float a2 = a * a; + float a3 = a2 * a; + float b2 = b * b; + float b3 = b2 * b; + + float p = -b2 / static_cast(3.0 * a2); + float q = + 2.0 * b3 / static_cast(27.0 * a3) + c / static_cast(a); + float phi = acos(3.0 * q / static_cast(2.0 * p) * + sqrt(-3.0 / static_cast(p))); + float t = 2.0 * sqrt(-p / 3.0) * cos((phi + 4.0 * M_PI) / 3.0) - + b / static_cast(3.0 * a); + + return t; +} + +float calcHorizontalDistance(const float &fallTime, DroneConfig &drone, + const Coord &targetPosition) { + /* + h = V₀t − t²d·V₀/(2m) + t³(6d·g·l·m − 6d²(l²-1)·V₀)/(36m²) + + + t⁴ (−6d²g·l·(1+l²+l⁴)m + 3d³l²(1+l²)V₀ + 6d³l⁴(1+l²)V₀) / (36(1+l²)²m³) + + t⁵(3d³g·l³m − 3d⁴l²(1+l²)V₀) / (36(1+l²)m⁴) + */ + float t = fallTime; + float drag = drone.ammo.drag; + float lift = drone.ammo.lift; + float mass = drone.ammo.mass; + float attackSpeed = drone.attackSpeed; + + float t2 = t * t; + float t3 = t2 * t; + float t4 = t3 * t; + float t5 = t4 * t; + float drag2 = drag * drag; + float drag3 = drag2 * drag; + float drag4 = drag3 * drag; + float lift2 = lift * lift; + float lift3 = lift2 * lift; + float lift4 = lift3 * lift; + float mass2 = mass * mass; + float mass3 = mass2 * mass; + float mass4 = mass3 * mass; + + float h = + attackSpeed * t - t2 * drag * attackSpeed / (2.0 * mass) + + t3 * + (6.0 * drag * GRAVITY_ACCEL * lift * mass - + 6.0 * drag2 * (lift2 - 1.0) * attackSpeed) / + (36.0 * mass2) + + t4 * + (-6.0 * drag2 * GRAVITY_ACCEL * lift * (1.0 + lift2 + lift4) * mass + + 3.0 * drag3 * lift2 * (1.0 + lift2) * attackSpeed + + 6.0 * drag3 * lift4 * (1.0 + lift2) * attackSpeed) / + (36.0 * pow(1.0 + lift2, 2) * mass3) + + t5 * + (3.0 * drag3 * GRAVITY_ACCEL * lift3 * mass - + 3.0 * drag4 * lift2 * (1.0 + lift2) * attackSpeed) / + (36.0 * (1.0 + lift2) * mass4); + + // CHECK IF DRONE IS PRECISELY OVER THE TARGET SO DISTANCE IS NOT ZERO ------ + if (targetPosition.x == drone.startPos.x && + targetPosition.y == drone.startPos.y) { + drone.startPos.x = drone.startPos.x - 0.0001f; + drone.startPos.y = drone.startPos.y - 0.0001f; + } + + return h; +} + +float calcDistance(const Coord &a, const Coord &b) { + /* + D = √( (targetX − xd)² + (targetY − yd)² ) + */ + + float distance = sqrt(pow(b.x - a.x, 2) + pow(b.y - a.y, 2)); + + return distance; +} + +Coord calcFireCoordinates(const float &horizontalDistance, + const float &distanceToTarget, const float &xd, + const float &yd, const float &targetX, + const float &targetY) { + /* + ratio = (D − h) / D + fireX = xd + (targetX − xd) · ratio + fireY = yd + (targetY − yd) · ratio + */ + + float ratio = (distanceToTarget - horizontalDistance) / + static_cast(distanceToTarget); + float fireX = xd + (targetX - xd) * ratio; + float fireY = yd + (targetY - yd) * ratio; + + return {fireX, fireY}; +} + +bool isManoeuvreNeeded(const float &horizontalDistance, + const float &accelerationPath, + const float &distanceToTarget) { + /* + Manoeuvre is needed if (accelerationPath + horizontalDistance) > + distanceToTarget + */ + + return (accelerationPath + horizontalDistance) > distanceToTarget; +} + +void processManouvre(DroneConfig &drone, const Coord &targetPosition) { + /* + xd' = targetX − (targetX − xd) · (h + accelerationPath) / D + yd' = targetY − (targetY − yd) · (h + accelerationPath) / D + */ + + float h = calcHorizontalDistance( + calcAmmoFallTime(drone.ammo, drone.attackSpeed, drone.startPos.z), drone, + targetPosition); + float distanceToTarget = calcDistance(drone.startPos, targetPosition); + + if (isManoeuvreNeeded(h, drone.accelPath, distanceToTarget)) { + drone.startPos.x = + targetPosition.x - (targetPosition.x - drone.startPos.x) * + (h + drone.accelPath) / distanceToTarget; + drone.startPos.y = + targetPosition.y - (targetPosition.y - drone.startPos.y) * + (h + drone.accelPath) / distanceToTarget; + } +} + +BallisticsInput parseInputFile(const string &filePath) { + ifstream inputFile(filePath); + + if (!inputFile.is_open()) { + cerr << "Error: Could not open the file:" << filePath << endl; + exit(1); + } + + BallisticsInput input; + + inputFile >> input.droneX >> input.droneY >> input.droneZ >> input.targetX >> + input.targetY >> input.attackSpeed >> input.accelerationPath >> + input.ammoName; + + inputFile.close(); + + return input; +} + +DropSolution computeDropSolution(const BallisticsInput &input) { + DroneConfig drone = { + .startPos = {.x = input.droneX, .y = input.droneY, .z = input.droneZ}, + .attackSpeed = input.attackSpeed, + .accelPath = input.accelerationPath}; + + for (int i = 0; i < AMMO_TYPES_COUNT; i++) { + if (input.ammoName == ARSENAL[i].name) { + drone.ammo = ARSENAL[i]; + break; + } + } + + if (drone.ammo.name == "Unknown") { + return {.errorMessage = "Unknown ammo type: " + input.ammoName}; + } + + cout << "AMMO TYPE: " << drone.ammo.name << endl; + + float fallTime = + calcAmmoFallTime(drone.ammo, drone.attackSpeed, drone.startPos.z); + + cout << "Fall time: " << fallTime << "s" << endl; + + Coord targetPosition = {input.targetX, input.targetY}; + + float horizontalDistance = + calcHorizontalDistance(fallTime, drone, targetPosition); + + cout << "Horizontal distance: " << horizontalDistance << "m" << endl; + + float distanceToTarget = calcDistance(drone.startPos, targetPosition); + + cout << "Distance to target: " << distanceToTarget << "m" << endl; + + DropSolution dropSolution{}; + + if (isManoeuvreNeeded(horizontalDistance, drone.accelPath, + distanceToTarget)) { + cout << endl << "Manoeuvre is needed!" << endl; + + dropSolution.isManoeuvrePerformed = true; + dropSolution.manouvreX = drone.startPos.x; + dropSolution.manouvreY = drone.startPos.y; + + processManouvre(drone, targetPosition); + horizontalDistance = calcHorizontalDistance( + calcAmmoFallTime(drone.ammo, drone.attackSpeed, drone.startPos.z), + drone, targetPosition); + distanceToTarget = calcDistance(drone.startPos, targetPosition); + + cout << "Drone moves to new x: " << drone.startPos.x + << " y: " << drone.startPos.y << endl + << endl; + } + + Coord fireCoordinates = calcFireCoordinates( + horizontalDistance, distanceToTarget, drone.startPos.x, drone.startPos.y, + targetPosition.x, targetPosition.y); + + cout << endl + << "Fire x: " << fireCoordinates.x << " y: " << fireCoordinates.y + << endl; + + dropSolution.fireX = fireCoordinates.x; + dropSolution.fireY = fireCoordinates.y; + + return dropSolution; +} + +void writeOutputFile(const string &filePath, const DropSolution &dropSolution) { + ofstream outputFile(filePath); + + if (!outputFile.is_open()) { + cerr << "Error: Could not open the file for writing:" << filePath << endl; + exit(1); + } + + if (dropSolution.isManoeuvrePerformed) { + outputFile << dropSolution.manouvreX << " " << dropSolution.manouvreY + << " "; + } + + outputFile << dropSolution.fireX << " " << dropSolution.fireY; + + outputFile.close(); +} diff --git a/homework_06/src/main.cpp b/homework_06/src/main.cpp new file mode 100644 index 0000000..bac05c2 --- /dev/null +++ b/homework_06/src/main.cpp @@ -0,0 +1,25 @@ +#include + +#include "ballistics.hpp" + +using namespace std; + +int main(int argc, char **argv) { + if (argc != 2) { + cerr << "Usage: ballistics_cli " << endl; + return 1; + } + + const string INPUT_FILE = argv[1]; + + BallisticsInput input = parseInputFile(INPUT_FILE); + + DropSolution dropSolution = computeDropSolution(input); + + if (!dropSolution.errorMessage.empty()) { + cerr << "Error: " << dropSolution.errorMessage << endl; + return 1; + } + + writeOutputFile("output.txt", dropSolution); +} \ No newline at end of file diff --git a/homework_06/tests/ballistics_tests.cpp b/homework_06/tests/ballistics_tests.cpp new file mode 100644 index 0000000..dc449c8 --- /dev/null +++ b/homework_06/tests/ballistics_tests.cpp @@ -0,0 +1,76 @@ +#include + +#include + +#include "ballistics.hpp" + +const Ammo TEST_AMMO = { + .name = "VOG-17", .mass = 0.35f, .drag = 0.07f, .lift = 0.0f}; + +TEST(BallisticsTests, TestComputesKnownDropPoint) { + BallisticsInput input = parseInputFile(TEST_DATA_DIR "/sample_vog17.txt"); + + const DropSolution solution = computeDropSolution(input); + + EXPECT_NEAR(solution.fireX, 173.759, 0.01); + EXPECT_NEAR(solution.fireY, 173.759, 0.01); +} + +TEST(BallisticsTests, TestComputesUnknownAmmo) { + BallisticsInput input = parseInputFile(TEST_DATA_DIR "/unknown_ammo.txt"); + + const DropSolution solution = computeDropSolution(input); + + EXPECT_EQ(solution.errorMessage, "Unknown ammo type: F1"); +} + +TEST(BallisticsTests, TestCalcFallTime) { + float attackSpeed = 50.0f; + float droneHeight = 100.0f; + + float fallTime = calcAmmoFallTime(TEST_AMMO, attackSpeed, droneHeight); + + EXPECT_NEAR(fallTime, 5.74f, 0.01f); +} + +TEST(BallisticsTests, TestCalcHorizontalDistance) { + DroneConfig drone = {.startPos = {10.0f, 10.0f, 100.0f}, + .ammo = TEST_AMMO, + .attackSpeed = 50.0f}; + + drone.attackSpeed = 50.0f; + + float fallTime = + calcAmmoFallTime(drone.ammo, drone.attackSpeed, drone.startPos.z); + + Coord targetPosition = {100.0f, 100.0f, 0.}; + + float horizontalDistance = + calcHorizontalDistance(fallTime, drone, targetPosition); + + EXPECT_NEAR(horizontalDistance, 185.5f, 0.06f); +} + +TEST(BallisticsTests, TestCalcDistance) { + Coord a = {0.0f, 0.0f}; + Coord b = {3.0f, 4.0f}; + + float distance = calcDistance(a, b); + + EXPECT_NEAR(distance, 5.0f, 0.001f); +} + +TEST(BallisticsTests, TestCalcFireCoordinates) { + float horizontalDistance = 185.5f; + float distanceToTarget = 190.0f; + float xd = 10.0f; + float yd = 10.0f; + float targetX = 100.0f; + float targetY = 0.0f; + + Coord fireCoords = calcFireCoordinates(horizontalDistance, distanceToTarget, + xd, yd, targetX, targetY); + + EXPECT_NEAR(fireCoords.x, 12.1f, 0.1f); + EXPECT_NEAR(fireCoords.y, 9.7f, 0.1f); +} \ No newline at end of file