From 5d571fd4505285a628671c0684099eb0d24301d8 Mon Sep 17 00:00:00 2001 From: Tomo Sasaki Date: Sat, 28 Mar 2026 16:20:08 -0400 Subject: [PATCH 1/4] Remove optional solver integrations and examples Drop the Torch, ACADO, Gurobi, OSQP, and SQP integration paths from the build and public API. Keep examples, tests, Docker setup, and docs aligned with the remaining supported solver surface. --- CMakeLists.txt | 215 +-- Dockerfile | 15 - README.md | 5 - examples/CMakeLists.txt | 85 - examples/acado_car.cpp | 96 - examples/cddp_bicycle.cpp | 2 +- examples/cddp_car.cpp | 3 +- examples/cddp_hcw.cpp | 2 +- examples/cddp_lti_system.cpp | 2 +- examples/cddp_manipulator.cpp | 2 +- examples/cddp_neural_pendulum.cpp | 239 --- examples/cddp_unicycle_safe.cpp | 4 +- examples/cddp_unicycle_safe_comparison.cpp | 364 ---- examples/generate_unicycle_acados.py | 4 +- .../neural_dynamics/data/pendulum_dataset.csv | 101 -- .../neural_dynamics/data/pendulum_dataset.png | Bin 36113 -> 0 bytes examples/neural_dynamics/prepare_cartpole.cpp | 0 examples/neural_dynamics/prepare_pendulum.cpp | 173 -- examples/neural_dynamics/run_cartpole.cpp | 0 examples/neural_dynamics/run_pendulum.cpp | 240 --- examples/neural_dynamics/train_cartpole.cpp | 0 examples/neural_dynamics/train_pendulum.cpp | 339 ---- examples/neural_dynamics/train_pendulum.ipynb | 505 ------ examples/quadrotor_benchmark.cpp | 1577 ----------------- examples/sqp_car.cpp | 0 examples/sqp_cartpole.cpp | 0 examples/sqp_unicycle.cpp | 127 -- examples/unicycle_benchmark.cpp | 1172 ------------ include/cddp-cpp/cddp.hpp | 7 +- include/cddp-cpp/cddp_core/asddp_solver.hpp | 128 -- include/cddp-cpp/cddp_core/cddp_core.hpp | 5 +- .../cddp_core/neural_dynamical_system.hpp | 167 -- include/cddp-cpp/sqp_core/sqp_core.hpp | 178 -- src/cddp_core/asddp_solver.cpp | 822 --------- src/cddp_core/cddp_core.cpp | 7 +- src/cddp_core/neural_dynamical_system.cpp | 251 --- src/sqp_core/sqp_core.cpp | 317 ---- tests/CMakeLists.txt | 35 - tests/cddp_core/test_asddp_core.cpp | 114 -- tests/cddp_core/test_asddp_solver.cpp | 305 ---- tests/cddp_core/test_boxqp.cpp | 47 - tests/dynamics_model/test_neural_pendulum.cpp | 296 ---- tests/sqp_core/test_sqp.cpp | 141 -- tests/sqp_core/test_sqp_core.cpp | 182 -- tests/test_gurobi.cpp | 84 - tests/test_qp_solvers.cpp | 273 --- tests/test_torch.cpp | 116 -- 47 files changed, 15 insertions(+), 8732 deletions(-) delete mode 100644 examples/acado_car.cpp delete mode 100644 examples/cddp_neural_pendulum.cpp delete mode 100644 examples/cddp_unicycle_safe_comparison.cpp delete mode 100644 examples/neural_dynamics/data/pendulum_dataset.csv delete mode 100644 examples/neural_dynamics/data/pendulum_dataset.png delete mode 100644 examples/neural_dynamics/prepare_cartpole.cpp delete mode 100644 examples/neural_dynamics/prepare_pendulum.cpp delete mode 100644 examples/neural_dynamics/run_cartpole.cpp delete mode 100644 examples/neural_dynamics/run_pendulum.cpp delete mode 100644 examples/neural_dynamics/train_cartpole.cpp delete mode 100644 examples/neural_dynamics/train_pendulum.cpp delete mode 100644 examples/neural_dynamics/train_pendulum.ipynb delete mode 100644 examples/quadrotor_benchmark.cpp delete mode 100644 examples/sqp_car.cpp delete mode 100644 examples/sqp_cartpole.cpp delete mode 100644 examples/sqp_unicycle.cpp delete mode 100644 examples/unicycle_benchmark.cpp delete mode 100644 include/cddp-cpp/cddp_core/asddp_solver.hpp delete mode 100644 include/cddp-cpp/cddp_core/neural_dynamical_system.hpp delete mode 100644 include/cddp-cpp/sqp_core/sqp_core.hpp delete mode 100644 src/cddp_core/asddp_solver.cpp delete mode 100644 src/cddp_core/neural_dynamical_system.cpp delete mode 100644 src/sqp_core/sqp_core.cpp delete mode 100644 tests/cddp_core/test_asddp_core.cpp delete mode 100644 tests/cddp_core/test_asddp_solver.cpp delete mode 100644 tests/dynamics_model/test_neural_pendulum.cpp delete mode 100644 tests/sqp_core/test_sqp.cpp delete mode 100644 tests/sqp_core/test_sqp_core.cpp delete mode 100644 tests/test_gurobi.cpp delete mode 100644 tests/test_qp_solvers.cpp delete mode 100644 tests/test_torch.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a527ac30..0414a29f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,26 +44,9 @@ endif() # Options option(CDDP_CPP_BUILD_TESTS "Whether to build tests." ON) -# SQP Configuration -option(CDDP_CPP_SQP "Whether to use SQP solver" OFF) - # CasADi Configuration option(CDDP_CPP_CASADI "Whether to use CasADi" OFF) -# Acado Configuration -option(CDDP_CPP_ACADO "Whether to use Acado" OFF) - -# Gurobi Configuration -# If you want to install Gurobi, please follow the instructions at: -# https://support.gurobi.com/hc/en-us/articles/4534161999889-How-do-I-install-Gurobi-Optimizer -option(CDDP_CPP_GUROBI "Whether to use Gurobi solver." OFF) -option(GUROBI_ROOT "Path to Gurobi installation" "") -if(APPLE) - set(GUROBI_ROOT /Library/gurobi1201/macos_universal2) # macOS default path -else() - set(GUROBI_ROOT /usr/local/lib/gurobi1201/linux64) # Linux default path -endif() - # SNOPT Configuration # If you want to install SNOPT, please follow the instructions at: # https://ccom.ucsd.edu/~optimizers/ @@ -85,14 +68,6 @@ else() set(ACADOS_ROOT /home/astomodynamics/acados) # Linux default path endif() -# LibTorch Configuration -# If you want to install LibTorch, please follow the instructions at: -# https://pytorch.org/ -# $ sudo cp -r libtorch /usr/local/lib/ -option(CDDP_CPP_TORCH "Whether to use LibTorch" OFF) -option(CDDP_CPP_TORCH_GPU "Whether to use GPU support in LibTorch" OFF) -set(LIBTORCH_DIR /usr/local/lib/libtorch CACHE PATH "Path to local LibTorch installation") # FIXME: Change this to your local LibTorch installation directory - # Find packages find_package(Eigen3 QUIET) @@ -125,15 +100,6 @@ if (CDDP_CPP_CASADI) endif() endif() -if (CDDP_CPP_ACADO) - # Assuming that Acado is installed by: https://acado.github.io/install_linux.html - set(ACADO_DIR $ENV{HOME}/github/ACADOtoolkit) # FIXME: Change this to your local Acado installation directory - - # Include ACADO header files - include_directories(${ACADO_DIR}) - include_directories(${ACADO_DIR}/acado) -endif() - # Enable FetchContent for downloading dependencies include(FetchContent) @@ -170,131 +136,6 @@ if (CDDP_CPP_BUILD_TESTS) include(GoogleTest) endif() -# LibTorch -if (CDDP_CPP_TORCH) - # Function to download and extract LibTorch - function(download_libtorch cuda_support download_dir) - if(APPLE) - # macOS has no CUDA support in PyTorch's pre-built binaries - set(LIBTORCH_URL "https://download.pytorch.org/libtorch/cpu/libtorch-macos-2.5.1.zip") - else() - if(cuda_support) - set(LIBTORCH_URL "https://download.pytorch.org/libtorch/cu124/libtorch-cxx11-abi-shared-with-deps-2.5.1%2Bcu124.zip") - else() - set(LIBTORCH_URL "https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-2.5.1%2Bcpu.zip") - endif() - endif() - - set(DOWNLOAD_PATH "${download_dir}/libtorch-shared-with-deps-latest.zip") - - message(STATUS "Downloading LibTorch from ${LIBTORCH_URL}") - file(DOWNLOAD "${LIBTORCH_URL}" "${DOWNLOAD_PATH}" - SHOW_PROGRESS - STATUS DOWNLOAD_STATUS - ) - - list(GET DOWNLOAD_STATUS 0 STATUS_CODE) - list(GET DOWNLOAD_STATUS 1 ERROR_MESSAGE) - - if(NOT STATUS_CODE EQUAL 0) - message(FATAL_ERROR "Failed to download LibTorch: ${ERROR_MESSAGE}") - endif() - - message(STATUS "Extracting LibTorch...") - execute_process( - COMMAND ${CMAKE_COMMAND} -E tar xf "${DOWNLOAD_PATH}" - WORKING_DIRECTORY "${download_dir}" - RESULT_VARIABLE EXTRACT_RESULT - ) - - if(NOT EXTRACT_RESULT EQUAL 0) - message(FATAL_ERROR "Failed to extract LibTorch") - endif() - - file(REMOVE "${DOWNLOAD_PATH}") - endfunction() - - # Try to find LibTorch in the following priority order: - # 0. Default locations - # 1. Local LibTorch directory (if specified) - # 2. Previously installed LibTorch under build directory - # 3. Download and install new copy - find_package(Torch QUIET) # Try finding in default locations first - - # Priority 0: Check default locations - if (TORCH_FOUND) - message(STATUS "Found LibTorch in default locations") - set(TORCH_FOUND TRUE) - endif() - - # Priority 1: Check local LibTorch directory - if(NOT TORCH_FOUND AND LIBTORCH_DIR) - if(EXISTS "${LIBTORCH_DIR}/share/cmake/Torch/TorchConfig.cmake") - find_package(Torch REQUIRED PATHS "${LIBTORCH_DIR}" NO_DEFAULT_PATH) - set(TORCH_FOUND TRUE) - message(STATUS "Found LibTorch in local directory: ${LIBTORCH_DIR}") - else() - message(WARNING "Specified LIBTORCH_DIR does not contain a valid LibTorch installation") - endif() - endif() - - # Priority 2: Check previously installed LibTorch under build directory - if(NOT TORCH_FOUND) - set(BUILD_LIBTORCH_DIR "${CMAKE_BINARY_DIR}/libtorch") - if(EXISTS "${BUILD_LIBTORCH_DIR}/share/cmake/Torch/TorchConfig.cmake") - find_package(Torch REQUIRED PATHS "${BUILD_LIBTORCH_DIR}" NO_DEFAULT_PATH) - set(TORCH_FOUND TRUE) - message(STATUS "Found LibTorch in build directory: ${BUILD_LIBTORCH_DIR}") - endif() - endif() - - # Priority 3: Download and install new copy - if(NOT TORCH_FOUND) - message(STATUS "LibTorch not found in preferred locations. Downloading fresh copy...") - download_libtorch(${CDDP_CPP_TORCH_GPU} "${CMAKE_BINARY_DIR}") - find_package(Torch REQUIRED PATHS "${CMAKE_BINARY_DIR}/libtorch" NO_DEFAULT_PATH) - message(STATUS "Successfully downloaded and installed LibTorch to: ${CMAKE_BINARY_DIR}/libtorch") - endif() - - # Set compilation flags for CUDA if GPU support is enabled - if(CDDP_CPP_TORCH_GPU) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}") - endif() - - # Export LibTorch variables for other parts of the build - set(TORCH_INSTALL_PREFIX ${Torch_DIR}/../../../ CACHE PATH "LibTorch installation directory") -endif() - -# OSQP-CPP for ASDDP -FetchContent_Declare( - osqp-cpp - GIT_REPOSITORY https://github.com/astomodynamics/osqp-cpp.git - GIT_TAG master - CMAKE_ARGS - -DCMAKE_POLICY_DEFAULT_CMP0048=NEW - -DCMAKE_POLICY_DEFAULT_CMP0069=NEW - -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -) - -# Custom processing before making osqp-cpp available -if(NOT osqp-cpp_POPULATED) - FetchContent_Populate(osqp-cpp) - - # Add this to the top of the OSQP CMakeLists.txt - file(WRITE ${osqp-cpp_SOURCE_DIR}/CMakeLists.txt.new - "cmake_minimum_required(VERSION 3.5)\n\n") - - # Append the original content - file(READ ${osqp-cpp_SOURCE_DIR}/CMakeLists.txt osqp_content) - file(APPEND ${osqp-cpp_SOURCE_DIR}/CMakeLists.txt.new "${osqp_content}") - - # Replace the original file - file(RENAME ${osqp-cpp_SOURCE_DIR}/CMakeLists.txt.new ${osqp-cpp_SOURCE_DIR}/CMakeLists.txt) - - # Add the subdirectory - add_subdirectory(${osqp-cpp_SOURCE_DIR} ${osqp-cpp_BINARY_DIR}) -endif() - # Include directories include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/include @@ -310,17 +151,12 @@ set(cddp_core_srcs src/cddp_core/qp_solver.cpp src/cddp_core/cddp_core.cpp src/cddp_core/clddp_solver.cpp - src/cddp_core/asddp_solver.cpp src/cddp_core/logddp_solver.cpp src/cddp_core/ipddp_solver.cpp src/cddp_core/msipddp_solver.cpp src/cddp_core/alddp_solver.cpp ) -if (CDDP_CPP_TORCH) - list(APPEND cddp_core_srcs src/cddp_core/neural_dynamical_system.cpp) -endif() - set(dynamics_model_srcs src/dynamics_model/pendulum.cpp src/dynamics_model/unicycle.cpp @@ -356,68 +192,22 @@ target_link_libraries(${PROJECT_NAME} $,Eigen3::Eigen,> matplot autodiff - osqp-cpp ) if(NOT Eigen3_FOUND) target_include_directories(${PROJECT_NAME} PUBLIC ${EIGEN3_INCLUDE_DIRS}) endif() -if (CDDP_CPP_TORCH) -target_compile_definitions(${PROJECT_NAME} PRIVATE CDDP_CPP_TORCH_ENABLED=1) - target_link_libraries(${PROJECT_NAME} ${TORCH_LIBRARIES}) -endif() - target_include_directories(${PROJECT_NAME} PUBLIC $ - $ - ${TORCH_INCLUDE_DIRS} + $ ) -# Ensure proper CUDA support if enabled -if(TORCH_FOUND AND CDDP_CPP_TORCH_GPU) - set_property(TARGET ${PROJECT_NAME} PROPERTY CUDA_ARCHITECTURES native) -endif() - -if (CDDP_CPP_SQP AND CDDP_CPP_CASADI) - # add sqp solver to cddp - target_sources(${PROJECT_NAME} PRIVATE src/sqp_core/sqp_core.cpp) -endif() - if (CDDP_CPP_CASADI) target_include_directories(${PROJECT_NAME} PUBLIC ${CASADI_INCLUDE_DIR}) target_link_libraries(${PROJECT_NAME} ${CASADI_LIBRARIES}) endif() -if (CDDP_CPP_ACADO) - target_link_libraries(${PROJECT_NAME} ${ACADO_DIR}/build/lib/libacado_toolkit_s.so) -endif() - -# Gurobi -if (CDDP_CPP_GUROBI) - if (NOT GUROBI_ROOT) - message(FATAL_ERROR "Please set GUROBI_ROOT.") - endif() - set(GUROBI_INCLUDE_DIRS ${GUROBI_ROOT}/include) # Set the path to the Gurobi include directory - set(GUROBI_LIBRARIES ${GUROBI_ROOT}/lib/libgurobi_c++.a) # Set the path to the Gurobi library - find_library(GUROBI_LIBRARY gurobi_c++ PATHS ${GUROBI_ROOT}/lib) - find_path(GUROBI_INCLUDE_DIR gurobi_c++.h PATHS ${GUROBI_ROOT}/include) - link_directories(${GUROBI_ROOT}/lib) - - if (GUROBI_LIBRARIES AND GUROBI_INCLUDE_DIR) - message(STATUS "Found Gurobi: ${GUROBI_LIBRARIES} ${GUROBI_LIBRARY}") - message(STATUS "Gurobi include directory: ${GUROBI_INCLUDE_DIR}") - - include_directories(${GUROBI_INCLUDE_DIR}) - target_link_libraries(${PROJECT_NAME} ${GUROBI_LIBRARIES} gurobi_c++ - # Platform-specific Gurobi library - $,gurobi120,gurobi120>) # Adjust version as needed - message(STATUS "Successfully linked Gurobi version 120.") - else() - message(FATAL_ERROR "Could not find Gurobi. Please set GUROBI_ROOT.") - endif() -endif() - # SNOPT if (CDDP_CPP_SNOPT) @@ -524,7 +314,6 @@ add_subdirectory(examples) # Cmake compile commmand: # $ mkdir build # $ cd build -# $ cmake -DCDDP_CPP_BUILD_TESTS=ON -DCDDP_CPP_TORCH=ON -DCDDP_CPP_TORCH_GPU=ON -DLIBTORCH_DIR=/usr/local/lib/libtorch -DCDDP_CPP_SQP=ON -DCDDP_CPP_GUROBI=ON -DGUROBI_ROOT=/usr/local/lib/gurobi1201/linux64 -DCDDP_CPP_CASADI=ON -DCDDP_CPP_ACADO=ON .. +# $ cmake -DCDDP_CPP_BUILD_TESTS=ON -DCDDP_CPP_CASADI=ON .. # $ make -j4 # $ make test - diff --git a/Dockerfile b/Dockerfile index 364fd387..b52c2e3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,18 +24,6 @@ RUN wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86 apt-get update && \ apt-get -y install cuda-toolkit-12-4 -# Install LibTorch (adjust URL for your desired version) -RUN apt-get update && \ - apt-get install -y --no-install-recommends unzip && \ - wget https://download.pytorch.org/libtorch/cu124/libtorch-cxx11-abi-shared-with-deps-2.5.1%2Bcu124.zip && \ - unzip libtorch-cxx11-abi-shared-with-deps-2.5.1+cu124.zip -d libtorch && \ - rm libtorch-cxx11-abi-shared-with-deps-2.5.1+cu124.zip && \ - rm -rf /var/lib/apt/lists/* - -# Set environment variables for LibTorch -ENV LIBTORCH_DIR=/libtorch/libtorch -ENV LD_LIBRARY_PATH=$LIBTORCH_DIR/lib:$LD_LIBRARY_PATH - # Create a directory for your project WORKDIR /app @@ -50,11 +38,8 @@ RUN rm -rf build && \ cmake \ -DCMAKE_BUILD_TYPE=Release \ -DCDDP_CPP_BUILD_TESTS=ON \ - -DCDDP_CPP_TORCH=ON \ - -DCDDP_CPP_TORCH_GPU=ON \ -DCDDP_CPP_CASADI=OFF \ -DPython_EXECUTABLE=/usr/bin/python3 \ - -DLIBTORCH_DIR=/libtorch/libtorch \ .. && \ make -j$(nproc) && \ make test diff --git a/README.md b/README.md index a4fdfce5..438f04fb 100644 --- a/README.md +++ b/README.md @@ -194,18 +194,13 @@ If you want to use this library for ROS2 MPC node, please refer [CDDP MPC Packag This library uses the following open-source libraries as core dependencies: -* [OSQP](https://osqp.org/) (Apache License 2.0) -* [osqp-cpp](https://github.com/google/osqp-cpp) (Apache License 2.0) * [matplotplusplus](https://github.com/alandefreitas/matplotplusplus) (MIT License) -* [libtorch](https://github.com/pytorch/pytorch) (BSD 3-Clause License) * [autodiff](https://github.com/autodiff/autodiff) (MIT License) This library also uses the following open-source libraries for optional features: * [Ipopt](https://github.com/coin-or/Ipopt) (EPL License) * [CasADi](https://web.casadi.org/) (GPL License) -* [Acado](https://www.acado.org/) (GPL License) -* [Gurobi](https://www.gurobi.com/) (GPL License) * [SNOPT](https://ccom.ucsd.edu/~optimizers/) (GPL License) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 86cc96aa..2ce08a13 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -77,9 +77,6 @@ target_link_libraries(cddp_unicycle_safe_ipddp_v2 cddp) add_executable(cddp_unicycle_safe_ipddp_v3 cddp_unicycle_safe_ipddp_v3.cpp) target_link_libraries(cddp_unicycle_safe_ipddp_v3 cddp) -add_executable(cddp_unicycle_safe_comparison cddp_unicycle_safe_comparison.cpp) -target_link_libraries(cddp_unicycle_safe_comparison cddp) - add_executable(cddp_spacecraft_linear_docking cddp_spacecraft_linear_docking.cpp) target_link_libraries(cddp_spacecraft_linear_docking cddp) @@ -130,85 +127,3 @@ if (CDDP_CPP_CASADI AND CDDP_CPP_SNOPT) add_executable(snopt_unicycle snopt_unicycle.cpp) target_link_libraries(snopt_unicycle cddp) endif() - -if (CDDP_CPP_CASADI AND CDDP_CPP_SNOPT) - add_executable(quadrotor_benchmark quadrotor_benchmark.cpp) - target_link_libraries(quadrotor_benchmark cddp) - - add_executable(unicycle_benchmark unicycle_benchmark.cpp) - target_link_libraries(unicycle_benchmark cddp) - - # If ACADOS is available, enable it for the benchmark - if (CDDP_CPP_ACADOS) - target_compile_definitions(unicycle_benchmark PRIVATE CDDP_CPP_ACADOS_ENABLED=1) - target_compile_definitions(quadrotor_benchmark PRIVATE CDDP_CPP_ACADOS_ENABLED=1) - - # Add ACADOS generated sources for unicycle benchmark - set(ACADOS_GENERATED_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/c_generated_code) - - set(ACADOS_UNICYCLE_SOURCES - ${ACADOS_GENERATED_SRC_DIR}/acados_solver_unicycle.c - ${ACADOS_GENERATED_SRC_DIR}/unicycle_model/unicycle_expl_ode_fun.c - ${ACADOS_GENERATED_SRC_DIR}/unicycle_model/unicycle_expl_vde_forw.c - ${ACADOS_GENERATED_SRC_DIR}/unicycle_model/unicycle_expl_vde_adj.c - ${ACADOS_GENERATED_SRC_DIR}/unicycle_constraints/unicycle_constr_h_fun.c - ${ACADOS_GENERATED_SRC_DIR}/unicycle_constraints/unicycle_constr_h_fun_jac_uxt_zt.c - ) - - target_sources(unicycle_benchmark PRIVATE ${ACADOS_UNICYCLE_SOURCES}) - target_include_directories(unicycle_benchmark PRIVATE - ${ACADOS_GENERATED_SRC_DIR} - ${ACADOS_GENERATED_SRC_DIR}/unicycle_model - ${ACADOS_GENERATED_SRC_DIR}/unicycle_constraints - ) - - # Add ACADOS generated sources for quadrotor benchmark - set(ACADOS_QUADROTOR_SOURCES - ${ACADOS_GENERATED_SRC_DIR}/acados_solver_quadrotor.c - ${ACADOS_GENERATED_SRC_DIR}/quadrotor_model/quadrotor_expl_ode_fun.c - ${ACADOS_GENERATED_SRC_DIR}/quadrotor_model/quadrotor_expl_vde_forw.c - ${ACADOS_GENERATED_SRC_DIR}/quadrotor_model/quadrotor_expl_vde_adj.c - ) - - target_sources(quadrotor_benchmark PRIVATE ${ACADOS_QUADROTOR_SOURCES}) - target_include_directories(quadrotor_benchmark PRIVATE - ${ACADOS_GENERATED_SRC_DIR} - ${ACADOS_GENERATED_SRC_DIR}/quadrotor_model - ) - endif() -endif() - -if (CDDP_CPP_CASADI AND CDDP_CPP_SQP) - add_executable(sqp_unicycle sqp_unicycle.cpp) - target_link_libraries(sqp_unicycle cddp) -endif() - -# Acado examples -if (CDDP_CPP_ACADO) - add_executable(acado_car acado_car.cpp) - target_link_libraries(acado_car cddp) -endif() - -# Neural dynamics examples -if (CDDP_CPP_TORCH) - add_executable(prepare_pendulum neural_dynamics/prepare_pendulum.cpp) - target_link_libraries(prepare_pendulum cddp) - - # add_executable(prepare_cartpole neural_dynamics/prepare_cartpole.cpp) - # target_link_libraries(prepare_cartpole cddp) - - add_executable(train_pendulum neural_dynamics/train_pendulum.cpp) - target_link_libraries(train_pendulum cddp) - - # add_executable(train_cartpole neural_dynamics/train_cartpole.cpp) - # target_link_libraries(train_cartpole cddp) - - add_executable(run_pendulum neural_dynamics/run_pendulum.cpp) - target_link_libraries(run_pendulum cddp) - - # add_executable(run_cartpole neural_dynamics/run_cartpole.cpp) - # target_link_libraries(run_cartpole cddp) - - # add_executable(cddp_pendulum_neural _cddp_pendulum_neural.cpp) - # target_link_libraries(cddp_pendulum_neural cddp) -endif() diff --git a/examples/acado_car.cpp b/examples/acado_car.cpp deleted file mode 100644 index 643076a1..00000000 --- a/examples/acado_car.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2024 Tomo Sasaki - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include -#include -#include -#include -#include -#include -using namespace ACADO; - - -int main() { - // Problem parameters - const int state_dim = 4; // [x, y, theta, v] - const int control_dim = 2; // [steering angle, acceleration] - const int horizon = 500; // Discretization steps - const double timestep = 0.03; - const double T = horizon * timestep; - const double wheelbase = 2.0; - - // Define differential states and controls - DifferentialState x, y, theta, v; - Control delta, a; - - // Define the dynamic system (differential equation) - DifferentialEquation f(0.0, T); - f << dot(x) == v * cos(theta); - f << dot(y) == v * sin(theta); - f << dot(theta) == v * tan(delta) / wheelbase; - f << dot(v) == a; - - // Set up OCP - OCP ocp(0.0, T, horizon); - - // Subject to the system dynamics - ocp.subjectTo( f ); - - // Initial conditions (at t=0) - ocp.subjectTo( AT_START, x == 1.0 ); - ocp.subjectTo( AT_START, y == 1.0 ); - ocp.subjectTo( AT_START, theta == 1.5 * M_PI ); - ocp.subjectTo( AT_START, v == 0.0 ); - - // (Loose) state bounds - ocp.subjectTo( -1e20 <= x <= 1e20 ); - ocp.subjectTo( -1e20 <= y <= 1e20 ); - ocp.subjectTo( -1e20 <= theta <= 1e20 ); - ocp.subjectTo( -1e20 <= v <= 1e20 ); - - // Control bounds (steering angle and acceleration) - ocp.subjectTo( -0.5 <= delta <= 0.5 ); - ocp.subjectTo( -2.0 <= a <= 2.0 ); - - // Define the cost (objective) terms. - ocp.minimizeLagrangeTerm( 1e-2 * delta * delta + 1e-4 * a * a + 1e-3 * (x*x + y*y) ); - - // Terminal cost - ocp.minimizeMayerTerm( 0.1 * (x*x + y*y) + 1.0 * (theta*theta) + 0.3 * (v*v) ); - - // Set up the optimization algorithm and options - OptimizationAlgorithm algorithm(ocp); - algorithm.set( MAX_NUM_ITERATIONS, 1000 ); - - // Solve the OCP while measuring solve time - auto start_time = std::chrono::high_resolution_clock::now(); - algorithm.solve(); - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - std::cout << "Solve time: " << duration.count() << " microseconds" << std::endl; - - // Retrieve the solution: state and control trajectories - VariablesGrid stateGrid, controlGrid; - algorithm.getDifferentialStates(stateGrid); - algorithm.getControls(controlGrid); - - std::cout << "Solution retrieved: " - << stateGrid.getNumPoints() << " states, " - << controlGrid.getNumPoints() << " controls." << std::endl; - - return 0; -} diff --git a/examples/cddp_bicycle.cpp b/examples/cddp_bicycle.cpp index d5ab2fb3..fc50eecc 100644 --- a/examples/cddp_bicycle.cpp +++ b/examples/cddp_bicycle.cpp @@ -63,7 +63,7 @@ int main() cddp_solver.setInitialTrajectory(X, U); // Solve the problem - cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::ASDDP); + cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::CLDDP); // Extract solution auto X_sol = std::any_cast>(solution.at("state_trajectory")); diff --git a/examples/cddp_car.cpp b/examples/cddp_car.cpp index bf259f26..1aca0376 100644 --- a/examples/cddp_car.cpp +++ b/examples/cddp_car.cpp @@ -241,7 +241,6 @@ int main() // Solve Time: 5.441e+05 micro sec // Final Cost: 1.90517 // ======================================== - // cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::ASDDP); // ======================================== // CDDP Solution // ======================================== @@ -333,4 +332,4 @@ int main() return 0; } -// convert -delay 3 ../results/frames/frame_*.png ../results/animations/car_parking.gif \ No newline at end of file +// convert -delay 3 ../results/frames/frame_*.png ../results/animations/car_parking.gif diff --git a/examples/cddp_hcw.cpp b/examples/cddp_hcw.cpp index 7d771c0f..ac30c9d6 100644 --- a/examples/cddp_hcw.cpp +++ b/examples/cddp_hcw.cpp @@ -143,7 +143,7 @@ int main() { cddp_solver.setInitialTrajectory(X, U); // Solve - cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::ASDDP); + cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::CLDDP); // Extract solution and print result auto cost_sequence = std::any_cast>(solution.at("cost_trajectory")); diff --git a/examples/cddp_lti_system.cpp b/examples/cddp_lti_system.cpp index 5ac3f703..aad85860 100644 --- a/examples/cddp_lti_system.cpp +++ b/examples/cddp_lti_system.cpp @@ -143,7 +143,7 @@ int main() { cddp_solver.setInitialTrajectory(X, U); // Solve using the CDDP solver - cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::ASDDP); + cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::CLDDP); // Alternatively: cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::LogDDP); // Extract solution trajectories diff --git a/examples/cddp_manipulator.cpp b/examples/cddp_manipulator.cpp index 009a48fe..8d1d8127 100644 --- a/examples/cddp_manipulator.cpp +++ b/examples/cddp_manipulator.cpp @@ -86,7 +86,7 @@ int main() { cddp_solver.setInitialTrajectory(X, U); // Solve the optimal control problem - cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::ASDDP); + cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::CLDDP); // -------------------- Extract Trajectories for Static Plots -------------------- auto X_sol = std::any_cast>(solution.at("state_trajectory")); diff --git a/examples/cddp_neural_pendulum.cpp b/examples/cddp_neural_pendulum.cpp deleted file mode 100644 index 362eeb40..00000000 --- a/examples/cddp_neural_pendulum.cpp +++ /dev/null @@ -1,239 +0,0 @@ -/* - Copyright 2024 Tomo - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include -#include - -#include "cddp.hpp" - -namespace plt = matplotlibcpp; -namespace fs = std::filesystem; - -struct ODEFuncImpl : public torch::nn::Module { - ODEFuncImpl(int64_t hidden_dim=32) { - net = register_module("net", torch::nn::Sequential( - torch::nn::Linear(/*in_features=*/2, hidden_dim), - torch::nn::Tanh(), - torch::nn::Linear(hidden_dim, hidden_dim), - torch::nn::Tanh(), - torch::nn::Linear(hidden_dim, 2) - )); - } - // forward(t, y) -> dy/dt - torch::Tensor forward(const torch::Tensor &t, const torch::Tensor &y) { - return net->forward(y); - } - torch::nn::Sequential net; -}; -TORCH_MODULE(ODEFunc); - -static torch::Tensor rk4_step( - ODEFunc &func, - const torch::Tensor &t, - const torch::Tensor &y, - double dt -) { - auto half_dt = dt * 0.5; - auto k1 = func->forward(t, y); - auto k2 = func->forward(t + half_dt, y + half_dt * k1); - auto k3 = func->forward(t + half_dt, y + half_dt * k2); - auto k4 = func->forward(t + dt, y + dt * k3); - return y + (dt / 6.0) * (k1 + 2.0*k2 + 2.0*k3 + k4); -} - -struct NeuralODEImpl : public torch::nn::Module { - NeuralODEImpl(int64_t hidden_dim=32) { - func_ = register_module("func", ODEFunc(hidden_dim)); - } - torch::Tensor forward(const torch::Tensor &y0, - const torch::Tensor &t, - double dt) - { - int64_t batch_size = y0.size(0); - int64_t steps = t.size(0); - - // shape: [B, steps, 2] - auto trajectory = torch::zeros({batch_size, steps, 2}, - torch::TensorOptions().device(y0.device()).dtype(y0.dtype())); - - // first step - trajectory.select(1, 0) = y0.clone(); - auto state = y0.clone(); - - for (int64_t i = 0; i < steps - 1; ++i) { - auto t_i = t[i]; - state = rk4_step(func_, t_i, state, dt); - trajectory.select(1, i+1) = state; - } - return trajectory; - } - - torch::Tensor step_once(const torch::Tensor &y, double dt) { - // We'll treat 't' as 0.0 for the step - auto t_0 = torch::tensor(0.0, y.options()); - return rk4_step(func_, t_0, y, dt); - } - - ODEFunc func_; -}; -TORCH_MODULE(NeuralODE); - -class NeuralPendulum : public cddp::DynamicalSystem { -public: - NeuralPendulum(const std::string& model_file, double dt, int64_t hidden_dim=32) - : dt_(dt) - { - // create and load - neural_ode_ = std::make_shared(hidden_dim); - torch::load(neural_ode_, model_file); - neural_ode_->eval(); - device_ = torch::kCPU; // keep everything CPU for simplicity - } - - Eigen::VectorXd getDiscreteDynamics(const Eigen::VectorXd &state, - const Eigen::VectorXd &control) override - { - auto options = torch::TensorOptions().dtype(torch::kFloat32).device(device_); - auto y_in = torch::from_blob( - const_cast(state.data()), - {1, 2}, - torch::TensorOptions().dtype(torch::kFloat64) - ).clone().to(options); - - - auto y_next = neural_ode_->step_once(y_in, dt_); - auto y_next_cpu = y_next.to(torch::kCPU); - auto data_ptr = y_next_cpu.data_ptr(); - - Eigen::VectorXd x_next(2); - x_next << static_cast(data_ptr[0]), static_cast(data_ptr[1]); - - return x_next; - } - - std::unique_ptr clone() const override { - throw std::runtime_error("NeuralPendulum clone not implemented."); - } - -private: - std::shared_ptr neural_ode_; - torch::Device device_; - double dt_; -}; - - -int main() -{ - int state_dim = 2; - int control_dim = 1; - int horizon = 100; - double timestep = 0.02; - - std::string model_file = "../examples/neural_dynamics/neural_models/neural_pendulum.pth"; - - std::unique_ptr system = - std::make_unique(model_file, timestep /*dt*/); - - // Cost matrices - Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); - Eigen::MatrixXd R = 0.1 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 100.0, 0.0, - 0.0, 100.0; - - // Goal state = (0, 0) upright - Eigen::VectorXd goal_state(state_dim); - goal_state << 0.0, 0.0; - - // We have no "reference" states, so pass an empty vector - std::vector empty_reference_states; - auto objective = std::make_unique(Q, R, Qf, goal_state, empty_reference_states, timestep); - - // Initial state (pendulum pointing down, or any) - Eigen::VectorXd initial_state(state_dim); - initial_state << M_PI, 0.0; // (theta=pi, dot=0) - - // Construct zero control sequence - std::vector zero_control_sequence(horizon, Eigen::VectorXd::Zero(control_dim)); - - // Construct initial trajectory - std::vector X_init(horizon + 1, initial_state); - - // Solver options - cddp::CDDPOptions options; - options.max_iterations = 20; - options.regularization.type = "none"; - options.regularization.control = 1e-7; - - // Create CDDP solver with new API - cddp::CDDP cddp_solver(initial_state, goal_state, horizon, timestep, - std::move(system), std::move(objective), options); - - // Control constraints - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -10.0; // clamp torque - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 10.0; - cddp_solver.addPathConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, control_upper_bound)); - - // Set initial guess - cddp_solver.setInitialTrajectory(X_init, zero_control_sequence); - - // Solve - cddp::CDDPSolution solution = cddp_solver.solve(cddp::SolverType::ASDDP); - - auto X_sol = std::any_cast>(solution.at("state_trajectory")); - auto U_sol = std::any_cast>(solution.at("control_trajectory")); - auto t_sol = std::any_cast>(solution.at("time_points")); - - // Create a directory for plots - const std::string plotDirectory = "../results/tests_neural"; - if (!fs::exists(plotDirectory)) { - fs::create_directories(plotDirectory); - } - - // Extract solution data for plotting - std::vector theta_arr, theta_dot_arr, torque_arr; - for (auto &x : X_sol) { - theta_arr.push_back(x(0)); - theta_dot_arr.push_back(x(1)); - } - for (auto &u : U_sol) { - torque_arr.push_back(u(0)); - } - - // Plot - plt::figure(); - plt::subplot(2, 1, 1); - plt::named_plot("Theta", theta_arr); - plt::named_plot("ThetaDot", theta_dot_arr); - plt::title("Neural Pendulum State Trajectory"); - plt::legend(); - - plt::subplot(2, 1, 2); - plt::named_plot("Torque", torque_arr); - plt::title("Control Input"); - plt::legend(); - - std::string plot_file = plotDirectory + "/neural_pendulum_cddp.png"; - plt::save(plot_file); - std::cout << "Saved plot: " << plot_file << std::endl; - - return 0; -} diff --git a/examples/cddp_unicycle_safe.cpp b/examples/cddp_unicycle_safe.cpp index e688798f..cbdea2c1 100644 --- a/examples/cddp_unicycle_safe.cpp +++ b/examples/cddp_unicycle_safe.cpp @@ -102,7 +102,7 @@ int main() { solver_baseline.setInitialTrajectory(X_baseline, U_baseline); // Solve - cddp::CDDPSolution solution_baseline = solver_baseline.solve(cddp::SolverType::ASDDP); + cddp::CDDPSolution solution_baseline = solver_baseline.solve(cddp::SolverType::IPDDP); auto X_baseline_sol = std::any_cast>(solution_baseline.at("state_trajectory")); // size horizon + 1 // ------------------------------------------------------- @@ -132,7 +132,7 @@ int main() { solver_ball.setInitialTrajectory(X_ball, U_ball); // Solve - cddp::CDDPSolution solution_ball = solver_ball.solve(cddp::SolverType::ASDDP); + cddp::CDDPSolution solution_ball = solver_ball.solve(cddp::SolverType::IPDDP); auto X_ball_sol = std::any_cast>(solution_ball.at("state_trajectory")); // horizon+1 // ------------------------------------------------------- diff --git a/examples/cddp_unicycle_safe_comparison.cpp b/examples/cddp_unicycle_safe_comparison.cpp deleted file mode 100644 index 86a20511..00000000 --- a/examples/cddp_unicycle_safe_comparison.cpp +++ /dev/null @@ -1,364 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "cddp.hpp" -#include "matplot/matplot.h" - -namespace fs = std::filesystem; -using namespace matplot; - -int main() { - // -------------------------- - // 1. Shared problem setup - // -------------------------- - const int state_dim = 3; // [x, y, theta] - const int control_dim = 2; // [v, omega] - const int horizon = 100; - const double timestep = 0.03; - const std::string integration_type = "euler"; - - // Create a unicycle instance - auto dyn_system = std::make_unique(timestep, integration_type); - - // Quadratic cost - Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); - Eigen::MatrixXd R = 0.05 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 100.0, 0.0, 0.0, - 0.0, 100.0, 0.0, - 0.0, 0.0, 100.0; - - // Goal state - Eigen::VectorXd goal_state(state_dim); - goal_state << 2.0, 2.0, M_PI / 2.0; - - // Empty reference states - std::vector empty_ref; - - // Initial state - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI / 4.0; - - // Options for the baseline #2 solver (10 iterations) - cddp::CDDPOptions options_10; - options_10.max_iterations = 10; - options_10.verbose = true; - options_10.debug = false; - options_10.enable_parallel = false; - options_10.num_threads = 1; - options_10.tolerance = 1e-5; - options_10.acceptable_tolerance = 1e-4; - options_10.regularization.initial_value = 1e-2; - options_10.ipddp.barrier.mu_initial = 1e-1; - - cddp::CDDPOptions options_ipddp; - options_ipddp.max_iterations = 1000; - options_ipddp.verbose = true; - options_ipddp.debug = false; - options_ipddp.enable_parallel = false; - options_ipddp.num_threads = 1; - options_ipddp.tolerance = 1e-5; - options_ipddp.acceptable_tolerance = 1e-4; - options_ipddp.regularization.initial_value = 1e-4; - options_ipddp.ipddp.barrier.mu_initial = 1e-1; - - cddp::CDDPOptions options_asddp; - options_asddp.max_iterations = 100; - options_asddp.verbose = true; - options_asddp.debug = false; - options_asddp.enable_parallel = false; - options_asddp.num_threads = 1; - options_asddp.tolerance = 1e-5; - options_asddp.acceptable_tolerance = 1e-4; - options_asddp.regularization.initial_value = 1e-5; - - // Constraint parameters - // (Used only by baseline #2 and the subsequent 4 solutions, - // but not by the unconstrained solver.) - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 2.0, M_PI; - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -2.0, -M_PI; - double radius = 0.4; - Eigen::Vector2d center(1.0, 1.0); - - // Create a directory for saving plots - const std::string plotDirectory = "../results/tests"; - if (!fs::exists(plotDirectory)) { - fs::create_directories(plotDirectory); - } - - // -------------------------------------------------------- - // 2. Baseline #1: Unconstrained (no ball, no control constraint) - // -------------------------------------------------------- - cddp::CDDP solver_unconstrained( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_10 // We can also reuse the same options, or set different ones - ); - // We do NOT add any constraint here - // Simple initial guess - std::vector X_unconstrained_init(horizon + 1, initial_state); - std::vector U_unconstrained_init(horizon, Eigen::VectorXd::Zero(control_dim)); - solver_unconstrained.setInitialTrajectory(X_unconstrained_init, U_unconstrained_init); - - // Solve for baseline #1 - cddp::CDDPSolution sol_unconstrained = solver_unconstrained.solve(cddp::SolverType::IPDDP); - auto X_unconstrained_sol = std::any_cast>(sol_unconstrained.at("state_trajectory")); - auto U_unconstrained_sol = std::any_cast>(sol_unconstrained.at("control_trajectory")); - - // -------------------------------------------------------- - // 3. Baseline #2: IPDDP with constraints (10 iterations) - // => "BallConstraint" + "ControlConstraint" - // -------------------------------------------------------- - cddp::CDDP solver_ipddp_10( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_10 - ); - // Add constraints - solver_ipddp_10.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound)); - solver_ipddp_10.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - - // Simple initial guess - std::vector X_ipddp10_init(horizon + 1, initial_state); - std::vector U_ipddp10_init(horizon, Eigen::VectorXd::Zero(control_dim)); - solver_ipddp_10.setInitialTrajectory(X_unconstrained_sol, U_unconstrained_sol); - - // Solve for baseline #2 - cddp::CDDPSolution sol_ipddp_10 = solver_ipddp_10.solve(cddp::SolverType::IPDDP); - auto X_ipddp10_sol = std::any_cast>(sol_ipddp_10.at("state_trajectory")); - auto U_ipddp10_sol = std::any_cast>(sol_ipddp_10.at("control_trajectory")); - - // -------------------------------------------------------- - // 4. IPDDP and ASDDP (with constraints) using - // each baseline solution as the initial guess - // -------------------------------------------------------- - // - // We'll create 4 new solvers: - // A) IPDDP from unconstrained - // B) ASDDP from unconstrained - // C) IPDDP from ipddp_10 baseline - // D) ASDDP from ipddp_10 baseline - // - // -------------------------------------------------------- - auto makeConstrainedSolver = [&](const std::string &method_name, bool use_baseline = false) { - if (use_baseline) { - std::cout << "Using baseline solution as initial guess." << std::endl; - } else { - std::cout << "Using unconstrained solution as initial guess." << std::endl; - } - - if (use_baseline) { - return cddp::CDDP( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_10 - ); - } else { - if (method_name == "IPDDP") { - std::cout << "Using IPDDP method." << std::endl; - return cddp::CDDP( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_ipddp - ); - } else if (method_name == "ASDDP") { - std::cout << "Using ASDDP method." << std::endl; - return cddp::CDDP( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_asddp - ); - } - - } - }; - - // (A) IPDDP from unconstrained - cddp::CDDP solver_ipddp_from_unconstrained = makeConstrainedSolver("IPDDP"); - solver_ipddp_from_unconstrained.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound)); - solver_ipddp_from_unconstrained.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - solver_ipddp_from_unconstrained.setInitialTrajectory( - X_unconstrained_sol, U_unconstrained_sol - ); - auto sol_ipddp_from_uncon = solver_ipddp_from_unconstrained.solve(cddp::SolverType::IPDDP); - auto X_ipddp_from_uncon = std::any_cast>(sol_ipddp_from_uncon.at("state_trajectory")); - - // (B) ASDDP from unconstrained - cddp::CDDP solver_asddp_from_unconstrained = makeConstrainedSolver("ASDDP"); - solver_asddp_from_unconstrained.addPathConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, - control_upper_bound)); - solver_asddp_from_unconstrained.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - solver_asddp_from_unconstrained.setInitialTrajectory( - X_unconstrained_sol, U_unconstrained_sol - ); - auto sol_asddp_from_uncon = solver_asddp_from_unconstrained.solve(cddp::SolverType::ASDDP); - auto X_asddp_from_uncon = std::any_cast>(sol_asddp_from_uncon.at("state_trajectory")); - - // (C) IPDDP from ipddp_10 baseline - cddp::CDDP solver_ipddp_from_ipddp10 = makeConstrainedSolver("IPDDP"); - solver_ipddp_from_ipddp10.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound)); - solver_ipddp_from_ipddp10.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - solver_ipddp_from_ipddp10.setInitialTrajectory( - X_ipddp10_sol, U_ipddp10_sol - ); - auto sol_ipddp_from_ipddp10 = solver_ipddp_from_ipddp10.solve(cddp::SolverType::IPDDP); - auto X_ipddp_from_ipddp10 = std::any_cast>(sol_ipddp_from_ipddp10.at("state_trajectory")); - - // (D) ASDDP from ipddp_10 baseline - cddp::CDDP solver_asddp_from_ipddp10 = makeConstrainedSolver("ASDDP"); - solver_asddp_from_ipddp10.addPathConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, - control_upper_bound)); - solver_asddp_from_ipddp10.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - solver_asddp_from_ipddp10.setInitialTrajectory( - X_ipddp10_sol, U_ipddp10_sol - ); - auto sol_asddp_from_ipddp10 = solver_asddp_from_ipddp10.solve(cddp::SolverType::ASDDP); - auto X_asddp_from_ipddp10 = std::any_cast>(sol_asddp_from_ipddp10.at("state_trajectory")); - - // -------------------------------------------------------- - // 5. Convert all 6 solutions to (x,y) data for a single plot - // -------------------------------------------------------- - auto stateSeqToXY = [&](const std::vector &X_seq) { - std::vector xv, yv; - xv.reserve(X_seq.size()); - yv.reserve(X_seq.size()); - for (auto &st : X_seq) { - xv.push_back(st(0)); - yv.push_back(st(1)); - } - return std::make_pair(xv, yv); - }; - - // Baseline #1: unconstrained - auto [x_uncon, y_uncon] = stateSeqToXY(X_unconstrained_sol); - // Baseline #2: ipddp_10 - auto [x_ipddp10, y_ipddp10] = stateSeqToXY(X_ipddp10_sol); - - // IPDDP & ASDDP from unconstrained - auto [x_ipddp_from_un, y_ipddp_from_un] = stateSeqToXY(X_ipddp_from_uncon); - auto [x_asddp_from_un, y_asddp_from_un] = stateSeqToXY(X_asddp_from_uncon); - - // IPDDP & ASDDP from ipddp_10 - auto [x_ipddp_from_ip10, y_ipddp_from_ip10] = stateSeqToXY(X_ipddp_from_ipddp10); - auto [x_asddp_from_ip10, y_asddp_from_ip10] = stateSeqToXY(X_asddp_from_ipddp10); - - // -------------------------------------------------------- - // 6. Plot all 6 lines in one figure - // -------------------------------------------------------- - auto f1 = figure(true); - f1->size(1000, 800); - auto ax = f1->current_axes(); - - // Plot baseline #1: unconstrained - auto l0 = plot(ax, x_uncon, y_uncon, "-k"); - l0->display_name("Baseline Unconstrained"); - l0->line_width(2); - - hold(ax, true); - - // Plot baseline #2: ipddp_10 - auto l1 = plot(ax, x_ipddp10, y_ipddp10, "-b"); - l1->display_name("Baseline IPDDP(10)"); - l1->line_width(2); - - // Plot IPDDP from unconstrained - auto l2 = plot(ax, x_ipddp_from_un, y_ipddp_from_un, "-r"); - l2->display_name("IPDDP from Unconstrained"); - l2->line_width(2); - - // Plot ASDDP from unconstrained - auto l3 = plot(ax, x_asddp_from_un, y_asddp_from_un, "--r"); - l3->display_name("ASDDP from Unconstrained"); - l3->line_width(2); - - // Plot IPDDP from ipddp_10 - auto l4 = plot(ax, x_ipddp_from_ip10, y_ipddp_from_ip10, "-m"); - l4->display_name("IPDDP from IPDDP(10)"); - l4->line_width(2); - - // Plot ASDDP from ipddp_10 - auto l5 = plot(ax, x_asddp_from_ip10, y_asddp_from_ip10, "--m"); - l5->display_name("ASDDP from IPDDP(10)"); - l5->line_width(2); - - // Plot the Ball constraint circle - // (just for reference — it won't show up on the unconstrained solver) - std::vector cx, cy; - for (double th = 0.0; th < 2.0 * M_PI; th += 0.01) { - cx.push_back(center(0) + radius * std::cos(th)); - cy.push_back(center(1) + radius * std::sin(th)); - } - auto cplot = plot(ax, cx, cy, "--g"); - cplot->display_name("Ball Constraint"); - cplot->line_width(2); - - title(ax, "Comparison of 6 Trajectories"); - xlabel(ax, "x [m]"); - ylabel(ax, "y [m]"); - xlim(ax, {-0.2, 2.5}); - ylim(ax, {-0.2, 2.5}); - auto l = matplot::legend(ax); - l->location(legend::general_alignment::topleft); - grid(ax, true); - - f1->draw(); - f1->save(plotDirectory + "/unicycle_six_trajectories_comparison.png"); - std::cout << "Saved figure with 6 trajectories to " - << (plotDirectory + "/unicycle_six_trajectories_comparison.png") << std::endl; - - return 0; -} diff --git a/examples/generate_unicycle_acados.py b/examples/generate_unicycle_acados.py index 382a84af..e3bca863 100755 --- a/examples/generate_unicycle_acados.py +++ b/examples/generate_unicycle_acados.py @@ -142,8 +142,8 @@ def main(): print("\nNext steps:") print("1. Review the generated code") - print("2. Update unicycle_benchmark.cpp to use the generated solver") + print("2. Wire the generated solver into your ACADOS-enabled example") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/neural_dynamics/data/pendulum_dataset.csv b/examples/neural_dynamics/data/pendulum_dataset.csv deleted file mode 100644 index ada695a1..00000000 --- a/examples/neural_dynamics/data/pendulum_dataset.csv +++ /dev/null @@ -1,101 +0,0 @@ -theta,theta_dot,control,theta_next,theta_dot_next -3.64053,1.89705,0,3.6775,1.79961 -3.4489,0.876242,0,3.46582,0.815127 -3.78724,1.83921,0,3.82282,1.71801 -4.18144,0.600154,0,4.19174,0.430326 -3.9263,-0.0667156,0,3.92358,-0.20517 -3.89169,-1.36546,0,3.86306,-1.49689 -3.50305,1.744,0,3.53721,1.67113 -3.56444,0.209524,0,3.56782,0.128651 -4.03885,-1.83586,0,4.00062,-1.98649 -3.78647,1.31168,0,3.81151,1.19151 -3.87524,0.564401,0,3.88521,0.432174 -4.23232,0.914387,0,4.24886,0.739432 -3.97675,0.0839921,0,3.97697,-0.0615193 -3.51079,-0.120459,0,3.50768,-0.190967 -4.25349,0.945521,0,4.27064,0.768686 -4.05462,0.746598,0,4.06799,0.590371 -4.03259,1.85757,0,4.0682,1.70243 -3.15158,-1.87436,0,3.1141,-1.87227 -4.16375,-1.49541,0,4.13218,-1.66088 -4.40394,-0.270357,0,4.39667,-0.457025 -4.24252,0.215367,0,4.24508,0.0402644 -4.4871,-1.36638,0,4.45787,-1.55667 -3.92402,-1.99544,0,3.88276,-2.13047 -3.7381,1.76498,0,3.77228,1.65164 -3.80781,0.387822,0,3.81435,0.265967 -3.78567,-1.77445,0,3.74903,-1.88903 -3.80453,1.15333,0,3.82637,1.03065 -3.3371,-0.427475,0,3.32818,-0.464654 -3.40871,-1.69247,0,3.37437,-1.74067 -3.98255,1.78896,0,4.01685,1.64015 -4.46385,-1.04845,0,4.44098,-1.23784 -4.58261,-1.80027,0,4.54466,-1.99392 -4.46701,0.561674,0,4.47634,0.371023 -4.36874,-0.187736,0,4.36314,-0.372243 -3.22913,-0.238545,0,3.22419,-0.255171 -4.67232,1.27611,0,4.69588,1.07976 -3.79834,-1.66011,0,3.76396,-1.77689 -4.44057,1.96561,0,4.47798,1.77529 -3.92759,0.0725562,0,3.92766,-0.0662989 -4.15808,1.96831,0,4.19576,1.79917 -4.58177,1.24365,0,4.60469,1.04861 -4.18633,-0.984185,0,4.16495,-1.15261 -3.65412,1.56036,0,3.68435,1.46125 -3.38195,1.31345,0,3.40773,1.26402 -4.28036,1.39675,0,4.3065,1.21724 -3.359,0.74484,0,3.37347,0.700976 -4.11,0.438426,0,4.11715,0.276263 -3.61446,-0.6018,0,3.60154,-0.689924 -4.50955,-0.358782,0,4.50046,-0.550699 -4.39753,0.706302,0,4.40979,0.519238 -3.96313,0.99196,0,3.98152,0.846867 -3.16703,1.24951,0,3.19195,1.24183 -4.57282,0.728389,0,4.58544,0.533795 -3.94056,-1.58944,0,3.90738,-1.72744 -4.49998,0.240105,0,4.50286,0.0482135 -4.45236,-0.203043,0,4.4464,-0.392452 -3.70057,0.594455,0,3.71142,0.489368 -3.33987,-0.867797,0,3.32214,-0.904572 -4.34581,1.4933,0,4.37384,1.30887 -3.75877,-1.64092,0,3.72484,-1.75142 -4.1176,-0.376009,0,4.10846,-0.537948 -3.1642,0.191711,0,3.16799,0.186865 -4.04692,-1.66126,0,4.01217,-1.81314 -3.97628,-0.115738,0,3.97251,-0.260885 -4.41942,-1.43147,0,4.38892,-1.61813 -4.1243,1.45684,0,4.15179,1.29182 -4.54219,0.0670028,0,4.54159,-0.126357 -4.4106,-0.917575,0,4.39038,-1.10412 -3.47302,-0.00305767,0,3.47232,-0.0668472 -3.59659,-1.27855,0,3.57017,-1.36219 -4.65619,-0.748102,0,4.63927,-0.943724 -3.99296,-0.762743,0,3.97624,-0.909098 -3.5663,0.561103,0,3.5767,0.479201 -3.68528,-0.317903,0,3.67791,-0.418731 -3.52066,0.983796,0,3.53959,0.909258 -3.34339,0.610019,0,3.35519,0.56943 -3.4547,-0.512383,0,3.44385,-0.571712 -4.58229,-1.05551,0,4.55924,-1.24952 -4.05001,-0.044263,0,4.04758,-0.198833 -3.96254,0.0740334,0,3.96259,-0.0695799 -3.35421,1.81222,0,3.39001,1.76702 -3.50062,0.955986,0,3.51903,0.885156 -4.18802,1.4369,0,4.21505,1.26545 -3.82108,0.334896,0,3.82654,0.211103 -4.04763,-0.639619,0,4.0333,-0.793056 -3.88986,-1.58912,0,3.85676,-1.7199 -4.43747,-1.35484,0,4.4085,-1.5426 -3.41769,-1.54927,0,3.38619,-1.59947 -3.70613,0.344238,0,3.71196,0.238696 -3.56707,-1.42113,0,3.53786,-1.49922 -3.84688,-0.256219,0,3.84049,-0.382896 -4.1219,-1.65218,0,4.08725,-1.81291 -3.87296,-0.410114,0,3.86346,-0.540395 -3.53631,-0.495042,0,3.52566,-0.569441 -4.41684,0.441019,0,4.42378,0.253042 -3.87684,-0.933484,0,3.85687,-1.06346 -3.53326,0.469743,0,3.5419,0.393956 -4.19978,-0.39173,0,4.19024,-0.562182 -4.43851,1.20949,0,4.46081,1.01979 -3.65057,-0.683396,0,3.63596,-0.777628 diff --git a/examples/neural_dynamics/data/pendulum_dataset.png b/examples/neural_dynamics/data/pendulum_dataset.png deleted file mode 100644 index 6ea3dd1ffe48385f7eefec49c4458589020cfebe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36113 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�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 diff --git a/examples/neural_dynamics/prepare_cartpole.cpp b/examples/neural_dynamics/prepare_cartpole.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/neural_dynamics/prepare_pendulum.cpp b/examples/neural_dynamics/prepare_pendulum.cpp deleted file mode 100644 index c0152fde..00000000 --- a/examples/neural_dynamics/prepare_pendulum.cpp +++ /dev/null @@ -1,173 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -// Build & run: -// $ ./examples/prepare_pendulum [num_samples] [csv_filename] -// i.e. -// $ ./examples/prepare_pendulum 1000 pendulum_dataset.csv - -#include -#include -#include -#include -#include -#include -#include "cddp.hpp" - - -/** - * @brief Print a simple progress bar in the console. - */ -void printProgressBar(int current, int total, int barWidth = 50) { - float progress = static_cast(current) / static_cast(total); - int pos = static_cast(barWidth * progress); - - std::cout << "["; - for (int i = 0; i < barWidth; ++i) { - if (i < pos) { - std::cout << "="; - } else if (i == pos) { - std::cout << ">"; - } else { - std::cout << " "; - } - } - std::cout << "] " << int(progress * 100.0) << " %\r"; - std::cout.flush(); -} - -int main(int argc, char* argv[]) { - // Number of data samples to generate - int n_samples = 100; - if (argc > 1) { - n_samples = std::stoi(argv[1]); - } - - // CSV filename - std::string csv_filename = "pendulum_dataset.csv"; - if (argc > 2) { - csv_filename = argv[2]; - } - - // Create dataset directory if it doesn't exist - std::string dataset_dir = "../examples/neural_dynamics/data"; - if (!std::filesystem::exists(dataset_dir)) { - std::filesystem::create_directory(dataset_dir); - } - // Full path to CSV - csv_filename = dataset_dir + "/" + csv_filename; - - // Random number engine + distributions - std::default_random_engine rng(1234); - std::uniform_real_distribution angle_dist(M_PI, 3*M_PI/2.0); - std::uniform_real_distribution velocity_dist(-2.0, 2.0); - std::uniform_real_distribution control_dist(-2.0, 2.0); - - // Prepare pendulum system FIXME: change and match constants - double dt = 0.02; - double length = 1.0; - double mass = 1.0; - double damping = 0.01; - std::string integration_type = "rk4"; - - cddp::Pendulum pendulum(dt, length, mass, damping, integration_type); - - // Open CSV file - std::ofstream csv_file(csv_filename); - if (!csv_file.is_open()) { - std::cerr << "Error: Unable to open file " << csv_filename << std::endl; - return -1; - } - - // CSV header: - // theta, theta_dot, control, theta_next, theta_dot_next - csv_file << "theta,theta_dot,control,theta_next,theta_dot_next\n"; - - // For console output - std::cout << "Generating " << n_samples << " samples..." << std::endl; - - // Allocate some storage for states - Eigen::VectorXd state(2), control(1); - - // Storage for plotting - std::vector all_theta; - std::vector all_theta_dot; - - // Main loop: each sample is one step from random initial conditions - for (int i = 0; i < n_samples; ++i) { - // 1) Sample random initial state - double init_theta = angle_dist(rng); - double init_thetadot = velocity_dist(rng); - state << init_theta, init_thetadot; - - // 2) Sample a random control - // control << control_dist(rng); - control << 0.0; // zero torque - - // 3) Integrate one step to get the next state (RK4 inside) - Eigen::VectorXd next_state = pendulum.getDiscreteDynamics(state, control, 0.0); - - // 4) Write the row to CSV - csv_file - << state[0] << "," // theta - << state[1] << "," // theta_dot - << control[0] << "," // control - << next_state[0] << "," // theta_next - << next_state[1] << "\n";// theta_dot_next - - // Keep track of current state for plotting - all_theta.push_back(state[0]); - all_theta_dot.push_back(state[1]); - - // Progress bar (optional) - if ((i+1) % 200 == 0 || i == n_samples - 1) { - printProgressBar(i+1, n_samples); - } - } - - // Final update for the progress bar - printProgressBar(n_samples, n_samples); - std::cout << std::endl; - - // Close file - csv_file.close(); - std::cout << "Dataset saved to " << csv_filename << std::endl; - - // plt::figure_size(1500, 500); // figsize=(15,5) roughly - - // // 1) Plot theta distribution - // plt::subplot(1, 3, 1); - // plt::hist(all_theta, 50); // bins=50 - // plt::title("Theta Distribution"); - // plt::xlabel("Theta (rad)"); - - // // 2) Plot theta_dot distribution - // plt::subplot(1, 3, 2); - // plt::hist(all_theta_dot, 50); // bins=50 - // plt::title("Angular Velocity Distribution"); - // plt::xlabel("Theta_dot (rad/s)"); - - // // 3) Plot phase space - // plt::subplot(1, 3, 3); - // plt::scatter(all_theta, all_theta_dot, /*size=*/2.0); - // plt::title("Phase Space"); - // plt::xlabel("Theta (rad)"); - // plt::ylabel("Theta_dot (rad/s)"); - - // plt::save("../examples/neural_dynamics/data/pendulum_dataset.png"); - // plt::show(); - return 0; -} diff --git a/examples/neural_dynamics/run_cartpole.cpp b/examples/neural_dynamics/run_cartpole.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/neural_dynamics/run_pendulum.cpp b/examples/neural_dynamics/run_pendulum.cpp deleted file mode 100644 index e2976109..00000000 --- a/examples/neural_dynamics/run_pendulum.cpp +++ /dev/null @@ -1,240 +0,0 @@ -/* - Copyright 2024 Tomo - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -// Standard headers -#include -#include -#include -#include -#include -#include - -#include "cddp.hpp" - -struct ODEFuncImpl : public torch::nn::Module { - ODEFuncImpl(int64_t hidden_dim=32) { - net = register_module("net", torch::nn::Sequential( - torch::nn::Linear(/*in_features=*/2, hidden_dim), - torch::nn::Tanh(), - torch::nn::Linear(hidden_dim, hidden_dim), - torch::nn::Tanh(), - torch::nn::Linear(hidden_dim, 2) - )); - } - - // forward(t, y) -> dy/dt - torch::Tensor forward(const torch::Tensor &t, const torch::Tensor &y) { - return net->forward(y); - } - - torch::nn::Sequential net; -}; -TORCH_MODULE(ODEFunc); - -torch::Tensor rk4_step( - ODEFunc &func, - const torch::Tensor &t, - const torch::Tensor &y, - double dt -) { - auto half_dt = dt * 0.5; - auto k1 = func->forward(t, y); - auto k2 = func->forward(t + half_dt, y + half_dt * k1); - auto k3 = func->forward(t + half_dt, y + half_dt * k2); - auto k4 = func->forward(t + dt, y + dt * k3); - return y + (dt / 6.0) * (k1 + 2.0*k2 + 2.0*k3 + k4); -} - -struct NeuralODEImpl : public torch::nn::Module { - NeuralODEImpl(int64_t hidden_dim=32) { - func_ = register_module("func", ODEFunc(hidden_dim)); - } - - // forward(y0, t, dt) -> entire trajectory - torch::Tensor forward(const torch::Tensor &y0, const torch::Tensor &t, double dt) - { - int64_t batch_size = y0.size(0); - int64_t steps = t.size(0); - - // shape: [B, steps, 2] - torch::Tensor trajectory = torch::zeros({batch_size, steps, 2}, - torch::TensorOptions().device(y0.device()).dtype(y0.dtype())); - - // first step is the initial state - trajectory.select(1, 0) = y0; - - auto state = y0.clone(); - for (int64_t i = 0; i < steps - 1; ++i) { - auto t_i = t[i]; - state = rk4_step(func_, t_i, state, dt); - - // Wrap theta to [0.0, 2*pi] - state.select(1, 0) = torch::fmod(state.select(1, 0), 2.0 * M_PI); - state.select(1, 0) = (state.select(1, 0) < 0).to(torch::kFloat32) * (2.0 * M_PI) + state.select(1, 0); - - trajectory.select(1, i+1) = state; - } - - return trajectory; - } - - ODEFunc func_; -}; -TORCH_MODULE(NeuralODE); - - -int main(int argc, char* argv[]) -{ - // 1) Parse command line arguments - std::string model_file = "../examples/neural_dynamics/neural_models/neural_pendulum.pth"; - float init_theta = 1.56f; - float init_thetadot = 0.0f; - int64_t seq_length = 100; // FIXME: - - if (argc > 1) model_file = argv[1]; - if (argc > 2) init_theta = std::stof(argv[2]); - if (argc > 3) init_thetadot= std::stof(argv[3]); - if (argc > 4) seq_length = std::stoll(argv[4]); - - std::cout << "Model file: " << model_file << std::endl; - std::cout << "Initial state: (theta=" << init_theta << ", theta_dot=" << init_thetadot << ")" << std::endl; - std::cout << "Sequence length: " << seq_length << std::endl; - - // 2) Setup device - torch::Device device = torch::kCPU; - - // 3) Load the trained model - auto neural_ode = NeuralODE(/*hidden_dim=*/32); - torch::load(neural_ode, model_file); - neural_ode->to(device); - neural_ode->eval(); - - // 4) Prepare the initial state, time vector - auto y0 = torch::tensor({init_theta, init_thetadot}).view({1,2}).to(device); - - float dt = 0.02f; // FIXME: - auto t_cpu = torch::arange(seq_length, torch::kInt64).to(torch::kFloat32) * dt; - auto t = t_cpu.to(device); - - // 5) Run the neural ODE to get the predicted trajectory - auto pred_traj = neural_ode->forward(y0, t, dt); // shape: [1, seq_length, 2] - pred_traj = pred_traj.squeeze(0).cpu(); // shape [seq_length, 2] - - // 6) Generate the "true" trajectory from cddp::Pendulum - cddp::Pendulum pendulum(// FIXME: - /*dt=*/0.02, /*length=*/1.0, - /*mass=*/1.0, /*damping=*/0.01, - /*integration_type=*/"rk4" - ); - // zero torque - Eigen::VectorXd control(1); - control.setZero(); - - // initial state - Eigen::VectorXd state(2); - state << init_theta, init_thetadot; - - std::vector theta_vec_nn(seq_length); - std::vector thetadot_vec_nn(seq_length); - std::vector theta_vec_true(seq_length); - std::vector thetadot_vec_true(seq_length); - - // fill these vectors in your loop: - for (int64_t i = 0; i < seq_length; ++i) { - theta_vec_nn[i] = pred_traj[i][0].item(); - thetadot_vec_nn[i] = pred_traj[i][1].item(); - theta_vec_true[i] = static_cast(state(0)); - thetadot_vec_true[i] = static_cast(state(1)); - if (i < seq_length - 1) { - state = pendulum.getDiscreteDynamics(state, control, 0.0); - // Wrap theta to [0.0, 2*pi] - state(0) = std::fmod(state(0), 2.0 * M_PI); - if (state(0) < 0) { - state(0) += 2.0 * M_PI; - } - } - } - - // Create a 2D tensor of shape [seq_length, 2] for the true trajectory - auto true_tensor = torch::empty({seq_length, 2}, torch::kFloat32); - for (int64_t i = 0; i < seq_length; ++i) { - true_tensor[i][0] = theta_vec_true[i]; - true_tensor[i][1] = thetadot_vec_true[i]; - } - true_tensor = true_tensor.to(device); - - // 7) Compare predicted vs. true - auto mse = torch::mse_loss(pred_traj, true_tensor); - float mse_val = mse.item(); - - std::cout << "Comparison result:\n"; - std::cout << " - MSE: " << mse_val << std::endl; - - // Print a few sample points - std::cout << "\nIndex | True (theta, theta_dot) | Pred (theta, theta_dot)\n"; - std::cout << "---------------------------------------------------------\n"; - for (int64_t i = 0; i < std::min(seq_length, 5); ++i) { - auto t_th = true_tensor[i][0].item(); - auto t_td = true_tensor[i][1].item(); - auto p_th = pred_traj[i][0].item(); - auto p_td = pred_traj[i][1].item(); - std::cout << i << " | (" - << t_th << ", " << t_td << ") | (" - << p_th << ", " << p_td << ")\n"; - } - - std::string out_file = "pendulum_compare.csv"; - { - std::ofstream ofs(out_file); - ofs << "index,true_theta,true_thetadot,pred_theta,pred_thetadot\n"; - for (int64_t i = 0; i < seq_length; ++i) { - auto t_th = true_tensor[i][0].item(); - auto t_td = true_tensor[i][1].item(); - auto p_th = pred_traj[i][0].item(); - auto p_td = pred_traj[i][1].item(); - ofs << i << "," - << t_th << "," << t_td << "," - << p_th << "," << p_td << "\n"; - } - ofs.close(); - std::cout << "Saved CSV: " << out_file << std::endl; - } - - // // 8) Plot the trajectories - // // plt args: (x, y, color, linestyle, linewidth, label) - - // plt::figure_size(800, 400); - // plt::subplot(1, 2, 1); - // plt::title("True vs Predicted (Theta)"); - // plt::plot(theta_vec_true, {{"color", "red"}, {"linestyle", "-"}, {"label", "True theta"}}); - // plt::plot(theta_vec_nn, {{"color", "blue"}, {"linestyle", "--"}, {"label", "Predicted theta"}}); - // plt::legend(); - // plt::xlabel("Time step"); - // plt::ylabel("Theta"); - - // plt::subplot(1, 2, 2); - // plt::title("True vs Predicted (Theta_dot)"); - // plt::plot(thetadot_vec_true, {{"color", "red"}, {"linestyle", "-"}, {"label", "True theta_dot"}}); - // plt::plot(thetadot_vec_nn, {{"color", "blue"}, {"linestyle", "--"}, {"label", "Predicted theta_dot"}}); - // plt::legend(); - // plt::xlabel("Time step"); - // plt::ylabel("Theta_dot"); - - // plt::save("../examples/neural_dynamics/neural_models/pendulum_compare.png"); - // std::cout << "Saved plot: pendulum_compare.png" << std::endl; - - return 0; -} diff --git a/examples/neural_dynamics/train_cartpole.cpp b/examples/neural_dynamics/train_cartpole.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/neural_dynamics/train_pendulum.cpp b/examples/neural_dynamics/train_pendulum.cpp deleted file mode 100644 index a7161236..00000000 --- a/examples/neural_dynamics/train_pendulum.cpp +++ /dev/null @@ -1,339 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -/* - * Build & run: - * $ ./examples/train_pendulum [pendulum_dataset.csv] [num_epochs] [batch_size] - * i.e. - * $ ./examples/train_pendulum pendulum_dataset.csv 32 100 - */ - -#include -#include -#include -#include -#include -#include -#include -#include // for std::random_shuffle or std::shuffle -#include // for std::min, std::shuffle -#include // for std::iota -#include "cddp.hpp" - -struct ODEFuncImpl : public torch::nn::Module { - // net: 2 -> hidden_dim -> hidden_dim -> 2 - ODEFuncImpl(int64_t hidden_dim=32) { - net = register_module("net", torch::nn::Sequential( - torch::nn::Linear(/*in_features=*/2, hidden_dim), - torch::nn::Tanh(), - torch::nn::Linear(hidden_dim, hidden_dim), - torch::nn::Tanh(), - torch::nn::Linear(hidden_dim, 2) - )); - } - torch::Tensor forward(const torch::Tensor &t, const torch::Tensor &y) { - return net->forward(y); - } - - torch::nn::Sequential net; -}; -TORCH_MODULE(ODEFunc); - -torch::Tensor rk4_step( - ODEFunc &func, - const torch::Tensor &t, - const torch::Tensor &y, - double dt -) { - auto half_dt = dt * 0.5; - auto k1 = func->forward(t, y); - auto k2 = func->forward(t + half_dt, y + half_dt * k1); - auto k3 = func->forward(t + half_dt, y + half_dt * k2); - auto k4 = func->forward(t + dt, y + dt * k3); - return y + (dt / 6.0) * (k1 + 2.0*k2 + 2.0*k3 + k4); -} - -struct NeuralODEImpl : public torch::nn::Module { - NeuralODEImpl(int64_t hidden_dim=32) { - func_ = register_module("func", ODEFunc(hidden_dim)); - } - - torch::Tensor forward(const torch::Tensor &y0, - const torch::Tensor &t, - double dt) - { - int64_t batch_size = y0.size(0); - int64_t steps = t.size(0); - - torch::Tensor trajectory = torch::zeros({batch_size, steps, 2}, - torch::TensorOptions().device(y0.device()).dtype(y0.dtype())); - - trajectory.select(1, 0) = y0; - - auto state = y0.clone(); - for (int64_t i = 0; i < steps - 1; ++i) { - // t[i], shape=() - auto t_i = t[i]; - state = rk4_step(func_, t_i, state, dt); - trajectory.select(1, i+1) = state; - } - - return trajectory; - } - - ODEFunc func_; -}; -TORCH_MODULE(NeuralODE); - -class PendulumDataset : public torch::data::Dataset -{ -public: - // Constructor - explicit PendulumDataset(const std::string &csv_file, int64_t seq_length=200) - : seq_length_(seq_length) - , pendulum_(/*FIXME: change and match constants*/ - /*timestep=*/0.02, /*length=*/1.0, - /*mass=*/1.0, /*damping=*/0.01, - /*integration_type=*/"rk4" - ) - { - // 1) Read CSV into initial_states_ vector - std::ifstream file(csv_file); - if (!file.is_open()) { - throw std::runtime_error("Could not open CSV: " + csv_file); - } - - { - std::string header_line; - if (std::getline(file, header_line)) { - std::cout << "Skipping header: " << header_line << std::endl; - } - } - - // read lines - std::string line; - while (std::getline(file, line)) { - if (line.empty()) continue; - std::stringstream ss(line); - std::vector vals; - while (!ss.eof()) { - std::string cell; - if (!std::getline(ss, cell, ',')) break; - if (!cell.empty()) { - vals.push_back(std::stod(cell)); - } - } - if (vals.size() < 2) { - // not enough columns - continue; - } - // store (theta, theta_dot) as float - initial_states_.push_back({(float)vals[0], (float)vals[1]}); - } - file.close(); - - std::cout << "Loaded " << initial_states_.size() - << " initial states from " << csv_file << std::endl; - - // 2) Generate trajectories with the cddp::Pendulum - generate_trajectories(); - - // 3) Convert to Tensors - states_tensor_ = torch::from_blob( - initial_states_.data(), - {(long)initial_states_.size(), 2}, - torch::TensorOptions().dtype(torch::kFloat32) - ).clone(); - - trajectories_tensor_ = torch::from_blob( - trajectories_.data(), - {(long)initial_states_.size(), seq_length_, 2}, - torch::TensorOptions().dtype(torch::kFloat32) - ).clone(); - - float dt = 0.02f; // FIXME: - t_ = torch::arange(seq_length_, torch::kInt64).to(torch::kFloat32).mul(dt); - } - - // override size() - torch::optional size() const override { - return initial_states_.size(); - } - - torch::data::Example<> get(size_t idx) override { - // x = initial state [2] - auto x = states_tensor_[idx]; - // y = entire trajectory [seq_length_, 2] - auto y = trajectories_tensor_[idx]; - return {x, y}; - } - - // Optionally expose the time vector - torch::Tensor get_time_vector() const { - return t_; - } - -private: - void generate_trajectories() { - trajectories_.resize(initial_states_.size() * seq_length_ * 2, 0.f); - - // Create a zero control input - Eigen::VectorXd control(1); - control.setZero(); // torque = 0 - - std::cout << "Generating " << initial_states_.size() << " trajectories of length " - << seq_length_ << std::endl; - - for (size_t i = 0; i < initial_states_.size(); ++i) { - // Convert our float pair into an Eigen::VectorXd - float theta = initial_states_[i][0]; - float theta_dot = initial_states_[i][1]; - Eigen::VectorXd state(2); - state << theta, theta_dot; - - // if (i == 0) { - // std::cout << "Initial state [" << i << "]: theta=" << theta - // << ", theta_dot=" << theta_dot << std::endl; - // } - - for (int64_t j = 0; j < seq_length_; ++j) { - // store in trajectories_ - size_t base_idx = i * seq_length_ * 2 + j * 2; - - trajectories_[base_idx + 0] = static_cast(state(0)); - trajectories_[base_idx + 1] = static_cast(state(1)); - - // if j < seq_length_-1, step forward - if (j < seq_length_ - 1) { - state = pendulum_.getDiscreteDynamics(state, control, 0.0); - } - } - } - std::cout << "Trajectory generation complete." << std::endl; - } - -private: - int64_t seq_length_; - std::vector> initial_states_; - std::vector trajectories_; // size = num_samples * seq_length_ * 2 - - torch::Tensor states_tensor_; // shape: [num_samples, 2] - torch::Tensor trajectories_tensor_; // shape: [num_samples, seq_length_, 2] - torch::Tensor t_; // shape: [seq_length_] - - cddp::Pendulum pendulum_; -}; - - -int main(int argc, char* argv[]) -{ - // 1. Decide on device (GPU if available) - torch::Device device = torch::cuda::is_available() ? torch::kCUDA : torch::kCPU; - - // 2. Parse command line args - std::string csv_path = "../examples/neural_dynamics/data"; - std::string csv_file = csv_path + "/pendulum_dataset.csv"; - std::string model_path = "../examples/neural_dynamics/neural_models"; - std::string model_file = model_path + "/neural_pendulum.pth"; - // Create a directory if it doesn't exist - if (!std::filesystem::exists(model_path)) { - std::filesystem::create_directories(model_path); - } - - int64_t batch_size = 32; - int64_t num_epochs = 100; // FIXME: - if (argc > 1) csv_file = csv_path + "/" + std::string(argv[1]); - if (argc > 2) batch_size = std::stoll(argv[2]); - if (argc > 3) num_epochs = std::stoll(argv[3]); - - // 3. Create dataset & dataloader - int64_t seq_length = 200; // FIXME: horizon length - PendulumDataset dataset(csv_file, seq_length); - - // 4. Load the dataset into a DataLoader - auto data_loader = torch::data::make_data_loader( - dataset.map(torch::data::transforms::Stack<>()), - /*batch_size=*/batch_size - ); - - // 5. Create the NeuralODE model - auto model = NeuralODE(/*hidden_dim=*/32); - model->to(device); - std::cout << "Training on " << (device.is_cuda() ? "GPU" : "CPU") << std::endl; - - // 6. Create optimizer - double learning_rate = 1e-2; - torch::optim::Adam optimizer(model->parameters(), torch::optim::AdamOptions(learning_rate)); - - // 7. Time vector (CPU, then push to device) - float dt = 0.02f; // FIXME: - auto t = dataset.get_time_vector().to(device); - - // 8. Training loop - std::vector losses; - - for (int64_t epoch = 0; epoch < num_epochs; ++epoch) { - double epoch_loss = 0.0; - int batch_count = 0; - - for (auto& batch : *data_loader) { - // batch.data shape = [B, 2] - // batch.target shape = [B, seq_length, 2] - auto data = batch.data.to(device); - auto target = batch.target.to(device); - - optimizer.zero_grad(); - - auto output = model->forward(data, t, /*dt=*/0.02); - auto loss = torch::mse_loss(output, target); - - // For older libTorch versions: - float loss_val = loss.item().toFloat(); - - loss.backward(); - optimizer.step(); - - epoch_loss += loss_val; - batch_count++; - } - - if (epoch % 10 == 0) { - double avg_loss = epoch_loss / static_cast(batch_count); - torch::save(model, model_file + std::to_string(epoch) + ".pth"); - losses.push_back(avg_loss); - - std::cout << "Epoch " << epoch << " / " << num_epochs - << " | Avg loss: " << avg_loss << std::endl; - } - - } - - std::cout << "Training complete." << std::endl; - - // 9. Save the model - torch::save(model, model_file); - std::cout << "Model saved to " << model_file << std::endl; - - // // 10. Plot the loss - // plt::figure(); - // plt::plot(losses); - // plt::title("Training Loss"); - // plt::xlabel("Epoch"); - // plt::ylabel("MSE Loss"); - // plt::save(model_path + "/training_loss.png"); - // std::cout << "Saved plot: " << model_path + "/training_loss.png" << std::endl; - - return 0; -} diff --git a/examples/neural_dynamics/train_pendulum.ipynb b/examples/neural_dynamics/train_pendulum.ipynb deleted file mode 100644 index 4aba3e33..00000000 --- a/examples/neural_dynamics/train_pendulum.ipynb +++ /dev/null @@ -1,505 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Neural ODE" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Generated 1000 samples and saved to pendulum_train.csv\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAHqCAYAAADrpwd3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAC1WUlEQVR4nOzdeXxU1f3/8feQkEkCIcqShYIBFdxQS0VZrAIuaMQV19JaUGtVXGqpXypQNVgFty+itYL2q4C1KNaK1WJVWgVrxVaouKFUW7YqAUUhgWwm3N8f/GaaSWYy271zz73zej4eeUBmu59z7rk3yfueOROwLMsSAAAAAAAAAABop5PbBQAAAAAAAAAAYCpCdAAAAAAAAAAAYiBEBwAAAAAAAAAgBkJ0AAAAAAAAAABiIEQHAAAAAAAAACAGQnQAAAAAAAAAAGIgRAcAAAAAAAAAIAZCdAAAAAAAAAAAYiBEBwAAAAAAAAAgBkJ0eFogEEjoa/ny5Vq+fLkCgYCefvpp27b/xhtvqKqqSjt27LDtNSWFaw195eXlqVevXjr22GM1ffp0bdy4sd1zFixYoEAgoA0bNiS1rZkzZ+rZZ59N6jnRtjVq1CgNGjQoqdeJ54UXXlBVVVXU+/r166eJEyfauj0AyEb333+/AoGA7edwu1RVVSkQCDi+nXPOOUcFBQUd/kz/7ne/q86dO2vr1q0Jv24gEIj5s8wObV9/7dq1qqqqSvr3gXhC+yH0VVhYqD59+uiUU07RL37xC9XW1rZ7zsSJE9WvX7+ktvPZZ5+pqqpKa9asSep50bYVCAR0zTXXJPU68Tz44INasGBBu9s3bNigQCAQ9T4AgLtCf7+GvnJzc9WnTx9dcskl+vTTT9s9btWqVS5Wm7wPP/xQF198sfbff3/l5+erZ8+e+ta3vqVrrrlGNTU1bpcH+AYhOjxt5cqVEV+nnXaaCgoK2t3+rW99y5Htv/HGG5oxY4btIXrIzJkztXLlSr366qt65JFHNGrUKD366KM65JBD9Jvf/CbisWPHjtXKlStVXl6e9DaSDdFT3VayXnjhBc2YMSPqfUuWLNFNN93k6PYBIBs8+uijkqQPPvhAf/vb31yuxj2XXXaZGhoatGjRoqj379y5U0uWLNHpp5+u0tLSDFcX28qVK/WDH/wg/P3atWs1Y8YM20P0kBdffFErV67Uiy++qHvuuUf77befpkyZosMOO0zvvPNOxGNvuukmLVmyJKnX/+yzzzRjxoykQ/RUtpWKWCF6eXm5Vq5cqbFjxzpeAwAgNfPnz9fKlSu1bNkyXX755XriiSd03HHHaffu3W6XlrK3335bRx11lNauXaubb75ZL774oubNm6exY8fqpZde0pdfful2iYBv5LpdAJCOYcOGRXzfq1cvderUqd3tXjVgwICItpx55pn6yU9+opNOOkkTJ07UEUccocMPP1zS3rb36tXL0Xrq6+uVn5+fkW3FM3jwYFe3DwB+sGrVKr3zzjsaO3asli5dqkceeURDhw51uyxH1dXVqbCwsN3tlZWV6t27tx599FFNmjSp3f1PPPGE6uvrddlll2WizIRl+neeo446Sj179gx/f9FFF+maa67RyJEjdeaZZ+qf//yngsGgJOmAAw5wvJ7Q/szEtjoSDAZ98/snAPjVoEGDNGTIEEnS6NGj1dLSop///Od69tln9d3vftfl6lIzZ84cderUScuXL1dRUVH49vPOO08///nPZVmWi9UB/sJMdGSdr7/+WtOnT1fv3r3VrVs3nXTSSVq3bl27x/3pT3/SiSeeqG7duqmwsFDHHnus/vznP4fvr6qq0v/8z/9Ikvr37x+xdIwkLV68WGPGjFF5ebkKCgp0yCGH6MYbb0z7Knf37t310EMPqbm5Wffee2/49mhLrLz99ts6/fTTVVJSomAwqN69e2vs2LH6z3/+I2nv25x3796thQsXhusfNWpUxOu9/PLLuvTSS9WrVy8VFhaqsbGxw6Vj/vKXv2jYsGEqKCjQN77xDd10001qaWkJ3x9aqibUTyFt3wY9ceJE/fKXvwzXGfoKbTPaci6bNm3S9773vXB7DznkEP3v//6v9uzZ024799xzj2bPnq3+/fura9euGj58uN58880k9gQAeN8jjzwiSbrjjjs0YsQIPfnkk6qrq4t4TLLnzV/96lcaOHCggsGgDj30UC1atKjdUhuJ/iyIJdGfsRMnTlTXrl313nvvacyYMSoqKtKJJ54Y9TVzcnI0YcIErV69Wu+99167++fPn6/y8nJVVlZKkqqrq3XFFVeoT58+ysvLU//+/TVjxgw1Nzd3WLskvf/++zrrrLO07777Kj8/X9/85je1cOHCdo/bsWOHfvKTn2j//fdXMBhUSUmJTjvtNH300Ufhx7RezmXBggU6//zzJe0NB0I/OxcsWKCf//znys3N1ebNm9tt59JLL1WPHj3U0NAQt/ZojjzySE2fPl2bNm3S4sWLw7dHW2Llt7/9rYYOHari4mIVFhZq//3316WXXipp77g4+uijJUmXXHJJuP5Q+zranx0tHfPQQw9FjMknn3wy4v5YSwa1/X2nX79++uCDD7RixYpwbaFtxhq7r7/+uk488UQVFRWpsLBQI0aM0NKlS6Nu59VXX9VVV12lnj17qkePHho3bpw+++yzqG0CAKQvdPGz7XKptbW1cc/Hif4u8u9//1sXXXSRevfurWAwqNLSUp144ont3nG1ePFiDR8+XF26dFHXrl11yimn6O23347bhu3bt6tbt27q2rVr1Ptb/3wLLcEa7292SZoxY4aGDh2q7t27q1u3bvrWt76lRx55JGoov2jRIg0fPlxdu3ZV165d9c1vfjP8O2ZIvHwF8AJCdGSdadOmaePGjfq///s/Pfzww/r44491xhlnRPzQePzxxzVmzBh169ZNCxcu1FNPPaXu3bvrlFNOCZ/of/CDH+jaa6+VJD3zzDPtlo75+OOPddppp+mRRx7Riy++qOuvv15PPfWUzjjjjLTbcPTRR6u8vFyvvfZazMfs3r1bJ598srZu3apf/vKXWrZsmebMmaP99tsvvG7pypUrVVBQoNNOOy1c/4MPPhjxOpdeeqk6d+6sX//613r66afVuXPnmNusrq7WRRddpO9+97v6/e9/r/POO0+33XabfvSjHyXdxptuuknnnXdeuM7QV6wlZD7//HONGDFCL7/8sn7+85/rueee00knnaQbbrgh6nqorfvkN7/5jXbv3q3TTjtNO3fuTLpWAPCi+vp6PfHEEzr66KM1aNAgXXrppaqtrdVvf/vbqI9P5Lz58MMP64c//KGOOOIIPfPMM/rZz36mGTNmtAvL05XMz9impiadeeaZOuGEE/T73/8+5jJh0t6feYFAILzETcjatWv197//XRMmTFBOTo6qq6t1zDHH6KWXXtLNN9+sP/7xj7rssss0a9YsXX755R3Wvm7dOo0YMUIffPCB7r//fj3zzDM69NBDNXHiRN11113hx9XW1urb3/62HnroIV1yySV6/vnnNW/ePA0cOFBbtmyJ+tpjx47VzJkzJe3dX6GfnWPHjtUVV1yh3NxcPfTQQxHP+fLLL/Xkk0/qsssuU35+foe1d+TMM8+UpA5/N1m5cqUuvPBC7b///nryySe1dOlS3XzzzeELD9/61rc0f/58SdLPfvazcP2tl6tJZn9K0nPPPaf7779ft956q55++mlVVFToO9/5TkqfkbNkyRLtv//+Gjx4cLi2jpaQWbFihU444QTt3LlTjzzyiJ544gkVFRXpjDPOiLjYEPKDH/xAnTt31qJFi3TXXXdp+fLl+t73vpd0nQCAxHzyySeS1O5d1omcjxP9XeS0007T6tWrddddd2nZsmWaO3euBg8eHLEk7MyZM/Wd73xHhx56qJ566in9+te/Vm1trY477jitXbu2wzYMHz5cW7Zs0Xe/+12tWLFC9fX1HT4+0b/ZN2zYoCuuuEJPPfWUnnnmGY0bN07XXnutfv7zn0c87uabb9Z3v/td9e7dWwsWLNCSJUs0YcKEiAsTieQrgCdYgI9MmDDB6tKlS9T7Xn31VUuSddppp0Xc/tRTT1mSrJUrV1qWZVm7d++2unfvbp1xxhkRj2tpabGOPPJI65hjjgnfdvfdd1uSrPXr13dY1549e6yvv/7aWrFihSXJeueddzp8fKjW3/72tzEfM3ToUKugoCD8/fz58yNqWbVqlSXJevbZZzvcVpcuXawJEya0uz30et///vdj3te63SNHjrQkWb///e8jHnv55ZdbnTp1sjZu3BjRtldffTXicevXr7ckWfPnzw/fdvXVV1uxTlMVFRURdd94442WJOtvf/tbxOOuuuoqKxAIWOvWrYvYzuGHH241NzeHH/f3v//dkmQ98cQTUbcHAH7z2GOPWZKsefPmWZZlWbW1tVbXrl2t4447LuJxiZ43W1parLKyMmvo0KERz9+4caPVuXNnq6KiInxbMj8Lbrnllpg/Cyyr45+xEyZMsCRZjz76aEJ9Yll7f5717NnTampqCt/2k5/8xJJk/fOf/7Qsy7KuuOIKq2vXruGfbSH33HOPJcn64IMPwrdJsm655Zbw9xdddJEVDAatTZs2RTy3srLSKiwstHbs2GFZlmXdeuutliRr2bJlHdbb9vV/+9vfRu1by9rbHyUlJVZjY2P4tjvvvNPq1KlT3N9lQvvh888/j3p/fX29JcmqrKyM2F7r/R7qn1Abo3nrrbfajYHWrxdrf7bdlmXt7ZuCggKruro6fFtzc7N18MEHWwceeGC7trUV7fedww47zBo5cmS7x0Ybu8OGDbNKSkqs2traiO0PGjTI6tOnj7Vnz56I7UyaNCniNe+66y5LkrVly5Z22wMAJC50nn3zzTetr7/+2qqtrbX+8Ic/WL169bKKiorCPydSPR/H+l3kiy++sCRZc+bMiVnbpk2brNzcXOvaa6+NuL22ttYqKyuzLrjggg7b1tDQYJ199tmWJEuSlZOTYw0ePNiaPn26tW3btojHJvo3e1stLS3W119/bd16661Wjx49wj+//v3vf1s5OTnWd7/73Zj1JZOvAKZjJjqyTmimVMgRRxwh6b9v4XrjjTf05ZdfasKECWpubg5/7dmzR6eeeqreeuuthJZk+fe//63x48errKxMOTk56ty5s0aOHClp76dnp8uKs7bZgQceqH333Vc//elPNW/evLhXsGM599xzE35sUVFRu/4dP3689uzZ0+HMNDu88sorOvTQQ3XMMcdE3D5x4kRZlqVXXnkl4vaxY8cqJycn/H3bcQAAfvfII4+ooKBAF110kSSpa9euOv/88/WXv/xFH3/8cbvHxztvrlu3TtXV1brgggsinrfffvvp2GOPtbX2ZH/GJvOz7LLLLtMXX3yh5557TpLU3Nysxx9/XMcdd5wGDBggSfrDH/6g0aNHq3fv3hG/K4SWelmxYkXM13/llVd04oknqm/fvhG3T5w4UXV1dVq5cqUk6Y9//KMGDhyok046KeHa4/nRj36kbdu2hd9tsGfPHs2dO1djx46NuRRKouL9XiIpvFTLBRdcoKeeekqffvppSttKZn+eeOKJER8Em5OTowsvvFCffPJJeHk7J+zevVt/+9vfdN5550W8xT4nJ0cXX3yx/vOf/7RbTjDe76gAgPQMGzZMnTt3VlFRkU4//XSVlZXpj3/8Y7sPDE/kfJzI7yLdu3fXAQccoLvvvluzZ8/W22+/HbHUqCS99NJLam5u1ve///2I3yny8/M1cuTIuO/mCwaDWrJkidauXat7771XF110kT7//HPdfvvtOuSQQ9r9rEn0b/ZXXnlFJ510koqLi8Ptu/nmm7V9+3Zt27ZNkrRs2TK1tLTo6quvjlmfXfkKYAJCdGSdHj16RHwf+vCr0Nuetm7dKmnvB3F07tw54uvOO++UZVlxP+F6165dOu644/S3v/1Nt912m5YvX6633npLzzzzTMS20rFp0yb1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W265Jbym+s0336zu3bvrN7/5jZYuXaq77rpLxcXFkqTrr79eixcv1llnnaUbb7xRxxxzjOrr67VixQqdfvrpMS8ODxo0SJL08MMPq6ioSPn5+erfv394dllOTo6uvvpq/fSnP1WXLl00ceLEpPpm9erVKi4u1tdff63PPvtMf/7zn/XrX/9aJSUlev755zv8Q/jmm2/Wf/7zH5144onq06ePduzYofvuuy9iHdkDDjhABQUF+s1vfqNDDjlEXbt2Ve/evTu8MNGRnj176oQTTtBNN92kLl266MEHH9RHH32kJ598MvyY0047Td27d9dll12mW2+9Vbm5uVqwYIE2b97c7vVC7/JbvHix9t9/f+Xn54d/X2lr1qxZOvnkkzV69GjdcMMNysvL04MPPqj3339fTzzxRNoXLwAA7kj0d5F3331X11xzjc4//3wNGDBAeXl5euWVV/Tuu++G3xner18/3XrrrZo+fbr+/e9/69RTT9W+++6rrVu36u9//7u6dOmiGTNmxKzlhz/8oXbs2KFzzz1XgwYNUk5Ojj766CPde++96tSpk376059GPD6Rv9nHjh2r2bNna/z48frhD3+o7du365577ml30aBfv36aNm2afv7zn6u+vl7f+c53VFxcrLVr1+qLL77QjBkzkspXAOO59IGmgCMmTJhgdenSJep9r776qiXJ+u1vfxtx+/r16y1J1vz58yNuX7FihTV27Fire/fuVufOna1vfOMb1tixY9s9f+rUqVbv3r2tTp06WZKsV1991bIsy3rjjTes4cOHW4WFhVavXr2sH/zgB9Y//vGPqNuKVWvoKzc31+rRo4c1fPhwa9q0adaGDRvaPSf0SeLr16+3LMuyPvroI+s73/mOdcABB1gFBQVWcXGxdcwxx1gLFiyIeN6aNWusY4891iosLLQkWSNHjox4vbfeeivutixr7yd9H3bYYdby5cutIUOGWMFg0CovL7emTZtmff311xHP37Jli3XeeedZ3bt3t4qLi63vfe971qpVq9r1TWNjo/WDH/zA6tWrlxUIBCK2WVFRYU2YMCHidTdu3GiNHz/e6tGjh9W5c2froIMOsu6++26rpaUl/JjQ/r777rvbtUuSdcstt7S7HQD85Oyzz7by8vKsbdu2xXzMRRddZOXm5lrV1dVJnzcffvhh68ADD7Ty8vKsgQMHWo8++qh11llnWYMHD454XKI/C2655Rar7a+sif6M7ej3gnjuu+8+S5J16KGHRr3/888/t6677jqrf//+VufOna3u3btbRx11lDV9+nRr165dHfbRe++9Z51xxhlWcXGxlZeXZx155JFRfzf46quvrB/96EfWfvvtZ3Xu3NkqKSmxxo4da3300Ucdvv6cOXOs/v37Wzk5OVF/79iwYYMlybryyisT7o/Qfgh9hX7Ojxkzxrrvvvusmpqads+ZMGGCVVFREf7+D3/4g1VZWWl94xvfsPLy8qySkhLrtNNOs/7yl79EPO+JJ56wDj74YKtz584R7etof7bdlmXt7Zurr77aevDBB60DDjjA6ty5s3XwwQdbv/nNb9o9/+9//7s1YsQIq0uXLtY3vvEN65ZbbrH+7//+r93vOxs2bLDGjBljFRUVWZLC24z1++Rf/vIX64QTTrC6dOliFRQUWMOGDbOef/75iMfE+p0r9Ptg6HdLAEBqOvrbNpHHRTsfJ/K7yNatW62JEydaBx98sNWlSxera9eu1hFHHGHde++9VnNzc8Q2nn32WWv06NFWt27drGAwaFVUVFjnnXee9ac//anDml966SXr0ksvtQ499FCruLjYys3NtcrLy61x48ZZK1eujHhsMn+zP/roo9ZBBx1kBYNBa//997dmzZplPfLII+1+LlqWZT322GPW0UcfbeXn51tdu3a1Bg8enHK+ApgsYFmW5VxEDwAAgGy2Y8cODRw4UGeffbYefvhht8uB9q5Let111+n999/v8F1tAADAP0aNGqUvvvii3TrpABLDci4AAACwRXV1tW6//XaNHj1aPXr00MaNG3XvvfeqtrZWP/rRj9wuL+u9/fbbWr9+vW699VadddZZBOgAAABAggjRAQAAYItgMKgNGzZo0qRJ+vLLL8MflDlv3jwCWwOcc845qq6u1nHHHad58+a5XQ4AAADgGSznAgAAAAAAAABADJ3cLgAAAAAAAAAAAFMRogMAAAAAAAAAEAMhOgAAAAAAAAAAMRj3waJ79uzRZ599pqKiIgUCAbfLAQAgJsuyVFtbq969e6tTJ65LJ4qf9QAAr+BnfWr4WQ8A8IpEf9YbF6J/9tln6tu3r9tlAACQsM2bN6tPnz5ul+EZ/KwHAHgNP+uTw896AIDXxPtZb1yIXlRUJGlv4d26dXO5GgAAYqupqVHfvn3DP7uQGH7WAwC8gp/1qeFnPQDAKxL9WW9ciB56q1e3bt34YQsA8AQ/vU157ty5mjt3rjZs2CBJOuyww3TzzTersrJS0t63us2YMUMPP/ywvvrqKw0dOlS//OUvddhhhyW8DX7WAwC8xk8/6zOBn/UAAK+J97OeRd0AAEBYnz59dMcdd2jVqlVatWqVTjjhBJ111ln64IMPJEl33XWXZs+erQceeEBvvfWWysrKdPLJJ6u2ttblygEAAAAAcAYhOgAACDvjjDN02mmnaeDAgRo4cKBuv/12de3aVW+++aYsy9KcOXM0ffp0jRs3ToMGDdLChQtVV1enRYsWuV06AAAAAACOIEQHAABRtbS06Mknn9Tu3bs1fPhwrV+/XtXV1RozZkz4McFgUCNHjtQbb7wR83UaGxtVU1MT8QUAAAAAgFcQogMAgAjvvfeeunbtqmAwqCuvvFJLlizRoYcequrqaklSaWlpxONLS0vD90Uza9YsFRcXh7/69u3raP0AAAAAANiJEB0AAEQ46KCDtGbNGr355pu66qqrNGHCBK1duzZ8f9sPXLEsq8MPYZk6dap27twZ/tq8ebNjtQMAAAAAYLdctwsAAABmycvL04EHHihJGjJkiN566y3dd999+ulPfypJqq6uVnl5efjx27Ztazc7vbVgMKhgMOhs0QAAAAAAOISZ6AAAoEOWZamxsVH9+/dXWVmZli1bFr6vqalJK1as0IgRI1ysEAAAAAAA5zATHQAAhE2bNk2VlZXq27evamtr9eSTT2r58uV68cUXFQgEdP3112vmzJkaMGCABgwYoJkzZ6qwsFDjx493u3QAAAAAABxBiA4AAMK2bt2qiy++WFu2bFFxcbGOOOIIvfjiizr55JMlSVOmTFF9fb0mTZqkr776SkOHDtXLL7+soqIilysHAAAAAMAZAcuyLLeLaK2mpkbFxcXauXOnunXr5nY5AADExM+s1NBvAACv4GdWaug3AIBXJPozizXRAQAAAAAAAACIgRAdAAAAAAAfmjVrlo4++mgVFRWppKREZ599ttatWxf3eStWrNBRRx2l/Px87b///po3b14GqgUAwFyE6AAAAAAA+NCKFSt09dVX680339SyZcvU3NysMWPGaPfu3TGfs379ep122mk67rjj9Pbbb2vatGm67rrr9Lvf/S6DlQMAYBY+WBQAAAAA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1qxZ7X43AAAAzgv9/F2wYEFSzwsGg55ati1blhKIJpm21zY0a1djs3oVBVPqp8K8XO3fq0t4e4AJvHohJlZY7sWLAnatp286R1rHh40BAJA9unTposMPP1wff/xx1PunTp0aEa7X1NSob9++mSoPAAAkafny5SopKdE+++yjkSNH6vbbb1dJSUnMx7v9rjNvzty0RzJttyOccyKwzMaLINnYZkSKdTx68aKAF2tOhSMt5MPGAADIHo2Njfrwww913HHHRb0/GAwqGAxmuCoAAJCKyspKnX/++aqoqND69et100036YQTTtDq1atj/jx3+11nXpy5aZdk2m5q0JWNF0Gysc2IZOrxiNgc+WDRZD5srLGxUTU1NRFfAADAXDfccINWrFih9evX629/+5vOO+881dTUaMKECW6XBgCA71VVVbX74M+2X6tWrUr59S+88EKNHTtWgwYN0hlnnKE//vGP+uc//6mlS5fGfM7UqVO1c+fO8NfmzZtT3n4qUv0QSac+wC+THwxoxwdo2llvKq/l1IeLmiwb2wx4ne1Ha7IfNub2FWsAAJCc//znP/rOd76jL774Qr169dKwYcP05ptvqqKiwu3SAADwvWuuuUYXXXRRh4/p16+fbdsrLy9XRUVFzGXbJO++68yp2cAmzDJOdq30WPUmu+xIKm3Pxhm5brXZrWVkWL4mu/h1f9vekmQ/bIx1UgEA8JYnn3zS7RIAAMhaPXv2VM+ePTO2ve3bt2vz5s0qLy/P2DYzxallYExYXsautdLbvk68cMyEtiM2ty7wmHBhCZnj1/3teEvifdiYV69YAwAAAABgsk2bNunLL7/Upk2b1NLSojVr1kiSDjzwQHXt2lWSdPDBB2vWrFk655xztGvXLlVVVencc89VeXm5NmzYoGnTpqlnz54655xzXGyJM2LNBk53FqUJM6vtWiu97evEC8dMaHs6/DqDNqSjceFk27m4YoZMjW+/7m/HWxPvw8YAAAAAAID9br75Zi1cuDD8/eDBgyVJr776qkaNGiVJWrdunXbu3ClJysnJ0XvvvafHHntMO3bsUHl5uUaPHq3FixerqKgo4/W7xY5ZlG6HsXaF2W1fx6/hWIhfZ9CGdDQunGx7ouPR7ePG7zI1vr1+MS0W21t0ww036IwzztB+++2nbdu26bbbbuPDxgAAAAAAyLAFCxZowYIF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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Generated 1000 samples and saved to pendulum_test.csv\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAHqCAYAAADrpwd3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACsWklEQVR4nOzdeXxU1cH/8e+QkEkCSZQlW0XADRfUUlABi4AoCogLrrX1B4pWxKWKloo+KtgqbkW0VtE+CFo3bF1axY1WwVpBgeKuVCtblYCgkEBIQsL9/cEzYyaZmcxy79xz73zer1demNnu2e7c+D1nzgQsy7IEAAAAAAAAAABaaed2AQAAAAAAAAAAMBUhOgAAAAAAAAAAMRCiAwAAAAAAAAAQAyE6AAAAAAAAAAAxEKIDAAAAAAAAABADIToAAAAAAAAAADEQogMAAAAAAAAAEAMhOgAAAAAAAAAAMRCiAwAAAAAAAAAQAyE6PC0QCCT0s3DhQi1cuFCBQEB//vOfbTv+22+/ralTp2rLli22vaakcFlDP3l5eeratauOPvpoXX/99VqzZk2r58ydO1eBQECrV69O6li33nqrnn/++aSeE+1YQ4YMUe/evZN6nba89NJLmjp1atT7evTooXHjxtl6PADIRvfee68CgYDt7+F2mTp1qgKBgOPHOe2001RQUBD3mv7Tn/5U7du314YNGxJ+3UAgEPNaZoeWr//JJ59o6tSpSf890JZQP4R+CgsLtddee+mEE07Q7373O9XU1LR6zrhx49SjR4+kjvP1119r6tSpeu+995J6XrRjBQIBXXbZZUm9Tlvuv/9+zZ07t9Xtq1evViAQiHofAMBdof9/Df3k5uZqr7320vnnn6+vvvqq1eOWLVvmYmmT9+mnn+q8887TPvvso/z8fHXp0kU/+tGPdNlll6m6utrt4gG+QYgOT1u8eHHEz8iRI1VQUNDq9h/96EeOHP/tt9/WtGnTbA/RQ2699VYtXrxYb7zxhmbPnq0hQ4bo4Ycf1kEHHaTHH3884rGjRo3S4sWLVVFRkfQxkg3RUz1Wsl566SVNmzYt6n3PPfecbrjhBkePDwDZ4OGHH5Ykffzxx3rnnXdcLo17xo8fr7q6Oj3xxBNR79+6dauee+45nXTSSSorK8tw6WJbvHixLrzwwvDvn3zyiaZNm2Z7iB7yyiuvaPHixXrllVd01113ae+999bkyZN1yCGH6P3334947A033KDnnnsuqdf/+uuvNW3atKRD9FSOlYpYIXpFRYUWL16sUaNGOV4GAEBq5syZo8WLF2vBggW66KKL9OSTT2rQoEHavn2720VL2YoVK9S3b1998sknuvHGG/XKK69o1qxZGjVqlF599VV9++23bhcR8I1ctwsApKN///4Rv3ft2lXt2rVrdbtX7b///hF1Ofnkk3X11VfruOOO07hx43TYYYfp0EMPlbS77l27dnW0PDt27FB+fn5GjtWWPn36uHp8APCDZcuW6f3339eoUaM0f/58zZ49W0cddZTbxXJUbW2tCgsLW90+YsQIVVZW6uGHH9bEiRNb3f/kk09qx44dGj9+fCaKmbBM/83Tt29fdenSJfz7Oeeco8suu0yDBw/WySefrH//+98KBoOSpH333dfx8oT6MxPHiicYDPrm708A8KvevXurX79+kqShQ4eqqalJv/71r/X888/rpz/9qculS83MmTPVrl07LVy4UEVFReHbzzjjDP3617+WZVkulg7wF1aiI+vs3LlT119/vSorK1VcXKzjjjtOK1eubPW4v/3tbxo2bJiKi4tVWFioo48+Wn//+9/D90+dOlW//OUvJUk9e/aM2DpGkubNm6fhw4eroqJCBQUFOuigg3TttdemPcvdqVMnPfjgg2psbNTdd98dvj3aFisrVqzQSSedpNLSUgWDQVVWVmrUqFH673//K2n3x5y3b9+uRx55JFz+IUOGRLzea6+9pgsuuEBdu3ZVYWGh6uvr424d849//EP9+/dXQUGBfvCDH+iGG25QU1NT+P7QVjWhdgpp+THocePG6fe//324nKGf0DGjbeeydu1a/exnPwvX96CDDtJvf/tb7dq1q9Vx7rrrLs2YMUM9e/ZUx44dNWDAAC1ZsiSJngAA75s9e7Yk6bbbbtPAgQP11FNPqba2NuIxyb5v/uEPf9ABBxygYDCogw8+WE888USrrTYSvRbEkug1dty4cerYsaM+/PBDDR8+XEVFRRo2bFjU18zJydHYsWO1fPlyffjhh63unzNnjioqKjRixAhJUlVVlS6++GLttddeysvLU8+ePTVt2jQ1NjbGLbskffTRRzrllFO05557Kj8/Xz/84Q/1yCOPtHrcli1bdPXVV2ufffZRMBhUaWmpRo4cqc8++yz8mObbucydO1dnnnmmpN3hQOjaOXfuXP36179Wbm6u1q1b1+o4F1xwgTp37qy6uro2yx7N4Ycfruuvv15r167VvHnzwrdH22LlT3/6k4466iiVlJSosLBQ++yzjy644AJJu8fFEUccIUk6//zzw+UP1S9ef8bbOubBBx+MGJNPPfVUxP2xtgxq+fdOjx499PHHH2vRokXhsoWOGWvsvvXWWxo2bJiKiopUWFiogQMHav78+VGP88Ybb+iSSy5Rly5d1LlzZ40ZM0Zff/111DoBANIXmvxsuV1qTU1Nm+/Hif4t8uWXX+qcc85RZWWlgsGgysrKNGzYsFafuJo3b54GDBigDh06qGPHjjrhhBO0YsWKNuuwefNmFRcXq2PHjlHvb359C23B2tb/s0vStGnTdNRRR6lTp04qLi7Wj370I82ePTtqKP/EE09owIAB6tixozp27Kgf/vCH4b8xQ9rKVwAvIERH1rnuuuu0Zs0a/e///q8eeughff755xo9enTEReOxxx7T8OHDVVxcrEceeURPP/20OnXqpBNOOCH8Rn/hhRfq8ssvlyQ9++yzrbaO+fzzzzVy5EjNnj1br7zyiq688ko9/fTTGj16dNp1OOKII1RRUaE333wz5mO2b9+u448/Xhs2bNDvf/97LViwQDNnztTee+8d3rd08eLFKigo0MiRI8Plv//++yNe54ILLlD79u31xz/+UX/+85/Vvn37mMesqqrSOeeco5/+9Kf6y1/+ojPOOEO/+c1v9Itf/CLpOt5www0644wzwuUM/cTaQuabb77RwIED9dprr+nXv/61/vrXv+q4447TNddcE3U/1OZt8vjjj2v79u0aOXKktm7dmnRZAcCLduzYoSeffFJHHHGEevfurQsuuEA1NTX605/+FPXxibxvPvTQQ/r5z3+uww47TM8++6z+53/+R9OmTWsVlqcrmWtsQ0ODTj75ZB177LH6y1/+EnObMGn3NS8QCIS3uAn55JNP9O6772rs2LHKyclRVVWVjjzySL366qu68cYb9fLLL2v8+PGaPn26LrroorhlX7lypQYOHKiPP/5Y9957r5599lkdfPDBGjdunO64447w42pqavTjH/9YDz74oM4//3y98MILmjVrlg444ACtX78+6muPGjVKt956q6Td/RW6do4aNUoXX3yxcnNz9eCDD0Y859tvv9VTTz2l8ePHKz8/P27Z4zn55JMlKe7fJosXL9bZZ5+tffbZR0899ZTmz5+vG2+8MTzx8KMf/Uhz5syRJP3P//xPuPzNt6tJpj8l6a9//avuvfde3Xzzzfrzn/+s7t276yc/+UlK35Hz3HPPaZ999lGfPn3CZYu3hcyiRYt07LHHauvWrZo9e7aefPJJFRUVafTo0RGTDSEXXnih2rdvryeeeEJ33HGHFi5cqJ/97GdJlxMAkJgvvvhCklp9yjqR9+NE/xYZOXKkli9frjvuuEMLFizQAw88oD59+kRsCXvrrbfqJz/5iQ4++GA9/fTT+uMf/6iamhoNGjRIn3zySdw6DBgwQOvXr9dPf/pTLVq0SDt27Ij7+ET/n3316tW6+OKL9fTTT+vZZ5/VmDFjdPnll+vXv/51xONuvPFG/fSnP1VlZaXmzp2r5557TmPHjo2YmEgkXwE8wQJ8ZOzYsVaHDh2i3vfGG29YkqyRI0dG3P70009bkqzFixdblmVZ27dvtzp16mSNHj064nFNTU3W4Ycfbh155JHh2+68805LkrVq1aq45dq1a5e1c+dOa9GiRZYk6/3334/7+FBZ//SnP8V8zFFHHWUVFBSEf58zZ05EWZYtW2ZJsp5//vm4x+rQoYM1duzYVreHXu///b//F/O+5vUePHiwJcn6y1/+EvHYiy66yGrXrp21Zs2aiLq98cYbEY9btWqVJcmaM2dO+LZLL73UivU21b1794hyX3vttZYk65133ol43CWXXGIFAgFr5cqVEcc59NBDrcbGxvDj3n33XUuS9eSTT0Y9HgD4zaOPPmpJsmbNmmVZlmXV1NRYHTt2tAYNGhTxuETfN5uamqzy8nLrqKOOinj+mjVrrPbt21vdu3cP35bMteCmm26KeS2wrPjX2LFjx1qSrIcffjihNrGs3dezLl26WA0NDeHbrr76akuS9e9//9uyLMu6+OKLrY4dO4avbSF33XWXJcn6+OOPw7dJsm666abw7+ecc44VDAattWvXRjx3xIgRVmFhobVlyxbLsizr5ptvtiRZCxYsiFvelq//pz/9KWrbWtbu9igtLbXq6+vDt91+++1Wu3bt2vxbJtQP33zzTdT7d+zYYUmyRowYEXG85v0eap9QHaNZunRpqzHQ/PVi9WfLY1nW7rYpKCiwqqqqwrc1NjZaBx54oLXffvu1qltL0f7eOeSQQ6zBgwe3emy0sdu/f3+rtLTUqqmpiTh+7969rb322svatWtXxHEmTpwY8Zp33HGHJclav359q+MBABIXep9dsmSJtXPnTqumpsZ68cUXra5du1pFRUXh60Sq78ex/hbZtGmTJcmaOXNmzLKtXbvWys3NtS6//PKI22tqaqzy8nLrrLPOilu3uro669RTT7UkWZKsnJwcq0+fPtb1119vbdy4MeKxif4/e0tNTU3Wzp07rZtvvtnq3Llz+Pr15ZdfWjk5OdZPf/rTmOVLJl8BTMdKdGSd0EqpkMMOO0zS9x/hevvtt/Xtt99q7NixamxsDP/s2rVLJ554opYuXZrQlixffvmlzj33XJWXlysnJ0ft27fX4MGDJe3+9ux0WW3sbbbffvtpzz331K9+9SvNmjWrzRnsWE4//fSEH1tUVNSqfc8991zt2rUr7so0O7z++us6+OCDdeSRR0bcPm7cOFmWpddffz3i9lGjRiknJyf8e8txAAB+N3v2bBUUFOicc86RJHXs2FFnnnmm/vGPf+jzzz9v9fi23jdXrlypqqoqnXXWWRHP23vvvXX00UfbWvZkr7HJXMvGjx+vTZs26a9//askqbGxUY899pgGDRqk/fffX5L04osvaujQoaqsrIz4WyG01cuiRYtivv7rr7+uYcOGqVu3bhG3jxs3TrW1tVq8eLEk6eWXX9YBBxyg4447LuGyt+UXv/iFNm7cGP60wa5du/TAAw9o1KhRMbdCSVRbf5dICm/VctZZZ+npp5/WV199ldKxkunPYcOGRXwRbE5Ojs4++2x98cUX4e3tnLB9+3a98847OuOMMyI+Yp+Tk6PzzjtP//3vf1ttJ9jW36gAgPT0799f7du3V1FRkU466SSVl5fr5ZdfbvWF4Ym8Hyfyt0inTp2077776s4779SMGTO0YsWKiK1GJenVV19VY2Oj/t//+38Rf1Pk5+dr8ODBbX6aLxgM6rnnntMnn3yiu+++W+ecc46++eYb3XLLLTrooINaXWsS/X/2119/Xccdd5xKSkrC9bvxxhu1efNmbdy4UZK0YMECNTU16dJLL41ZPrvyFcAEhOjIOp07d474PfTlV6GPPW3YsEHS7i/iaN++fcTP7bffLsuy2vyG623btmnQoEF655139Jvf/EYLFy7U0qVL9eyzz0YcKx1r165VZWVlzPtLSkq0aNEi/fCHP9R1112nQw45RJWVlbrpppu0c+fOhI8Ta/uUaFr+8SFJ5eXlknbv1eakzZs3Ry1rqI1aHr+tcQAAfvbFF1/ozTff1KhRo2RZlrZs2aItW7aEt9FquZ2J1Pb7Zuh9Ntq1INptqUr2GltYWKji4uKEX/+MM85QSUlJeFuRl156SRs2bIj4QtENGzbohRdeaPV3wiGHHCJJ2rRpU8zXT/R69c0332ivvfZKuNyJ6NOnjwYNGhT+zpEXX3xRq1evjrrtWbJCwUK8v02OOeYYPf/88+GwYK+99lLv3r315JNPJnycZPsz9HdItNuc/Nvku+++k2VZ/G0CAAZ59NFHtXTpUq1YsUJff/21Pvjgg6gT/W29Hyf6t0ggENDf//53nXDCCbrjjjv0ox/9SF27dtUVV1wR3mI1lD8cccQRrf6umDdvXty/KZo76KCDdOWVV+qxxx7T2rVrNWPGDG3evFk33HBDxOMS+X/2d999V8OHD5e0+7tu/vnPf2rp0qW6/vrrI+r3zTffSFLcv1fsyFcAU+S6XQDANF26dJEk/e53vwt/0UhLbYUBr7/+ur7++mstXLgwPBstKWLfs3S8++67qqqqivgf+mgOPfRQPfXUU7IsSx988IHmzp2rm2++WQUFBbr22msTOla0L9qKJXSBbK6qqkrS93+IhPZbra+vj3hcon8cxNK5c+eoe8SGvgAm1K8AgN0huWVZ+vOf/xx1b+hHHnlEv/nNbyJWnrcl9D4f71oQks61INlrbDLXMUkqKCjQT37yE/3hD3/Q+vXr9fDDD6uoqCj8hZ3S7mvKYYcdpltuuSXqa8QLkhO9XnXt2tWRldJXXHGFzjzzTP3rX//SfffdpwMOOEDHH3982q8bWrkf+oLyWE455RSdcsopqq+v15IlSzR9+nSde+656tGjhwYMGNDmcZLtz5Zjr/lt0f42CQUlUnp/m+y5555q164df5sAgEEOOugg9evXL+3XSeZvke7du4e/ZPPf//63nn76aU2dOlUNDQ2aNWtW+FoQ+t4OOwQCAV111VW6+eab9dFHH0Xcl8j/sz/11FNq3769XnzxxYjvS3n++ecjnhfaS/6///1vq0/YhdiRrwCmYCU60MLRRx+tPfbYQ5988on69esX9ScvL09S7BVCof/Ba/4/YpJafZlXKr799ltNmDBB7du311VXXZXQcwKBgA4//HDdfffd2mOPPfSvf/0rfF8wGLRthVNNTU34f6JDnnjiCbVr107HHHOMJIU/Lv7BBx9EPK7l80JlkxJbgTVs2DB98sknEXWTdq82CAQCGjp0aML1AAA/a2pq0iOPPKJ9991Xb7zxRqufq6++WuvXr9fLL7+c1Ov26tVL5eXlevrppyNuX7t2rd5+++2I25K5FrTk5DU2ZPz48WpqatKdd96pl156Seecc44KCwvD95900kn66KOPtO+++0b9OyFeiD5s2LDw/3w39+ijj6qwsDD8P5gjRozQv//971bbkbWlrWvnaaedpr333ltXX321/va3v2nixIlJB9Mtvf/++7r11lvVo0ePVtv5xCvn4MGDdfvtt0uSVqxYkVD5k/X3v/89IjBoamrSvHnztO+++4ZXzsUajy+88ELUcidStg4dOuioo47Ss88+G/H4Xbt26bHHHtNee+2lAw44IJUqAQBclurfIgcccID+53/+R4ceemj4/1tPOOEE5ebm6j//+U/M/CGeWF82/vXXX6u6urrV3ySJ/D97IBBQbm5uxGKKHTt26I9//GPE84YPH66cnBw98MADMcuXTL4CmI6V6EALHTt21O9+9zuNHTtW3377rc444wyVlpbqm2++0fvvv69vvvkmfJE49NBDJUn33HOPxo4dq/bt26tXr14aOHCg9txzT02YMEE33XST2rdvr8cff1zvv/9+UmX5/PPPtWTJEu3atUubN2/WO++8o9mzZ6u6ulqPPvpo+GPj0bz44ou6//77deqpp2qfffaRZVl69tlntWXLlogVZ4ceeqgWLlyoF154QRUVFSoqKlKvXr1SaLndM9eXXHKJ1q5dqwMOOEAvvfSS/vCHP+iSSy7R3nvvLWn3R8WOO+44TZ8+XXvuuae6d++uv//97+GPvjUXat/bb79dI0aMUE5Ojg477LCoF9mrrrpKjz76qEaNGqWbb75Z3bt31/z583X//ffrkksu4X9UAeD/vPzyy/r66691++23R1013Lt3b913332aPXu2TjrppIRft127dpo2bZouvvhinXHGGbrgggu0ZcsWTZs2TRUVFWrX7vu1G8lcC1qy6xobT79+/XTYYYdp5syZsiyr1Se/br75Zi1YsEADBw7UFVdcoV69eqmurk6rV6/WSy+9pFmzZsX8aPNNN90U3lP9xhtvVKdOnfT4449r/vz5uuOOO1RSUiJJuvLKKzVv3jydcsopuvbaa3XkkUdqx44dWrRokU466aSYk8O9e/eWJD300EMqKipSfn6+evbsGV5dlpOTo0svvVS/+tWv1KFDB40bNy6ptlm+fLlKSkq0c+dOff311/r73/+uP/7xjyotLdULL7wQ93+Eb7zxRv33v//VsGHDtNdee2nLli265557IvaR3XfffVVQUKDHH39cBx10kDp27KjKysq4ExPxdOnSRccee6xuuOEGdejQQffff78+++wzPfXUU+HHjBw5Up06ddL48eN18803Kzc3V3PnztW6detavV7oU37z5s3TPvvso/z8/PDfKy1Nnz5dxx9/vIYOHaprrrlGeXl5uv/++/XRRx/pySefTHvyAgDgjkT/Fvnggw902WWX6cwzz9T++++vvLw8vf766/rggw/Cnwzv0aOHbr75Zl1//fX68ssvdeKJJ2rPPffUhg0b9O6776pDhw6aNm1azLL8/Oc/15YtW3T66aerd+/eysnJ0Weffaa7775b7dq1069+9auIxyfy/+yjRo3SjBkzdO655+rnP/+5Nm/erLvuuqvVpEGPHj103XXX6de//rV27Nihn/zkJyopKdEnn3yiTZs2adq0aUnlK4DxXPpCU8ARY8eOtTp06BD1vjfeeMOSZP3pT3+KuH3VqlWWJGvOnDkRty9atMgaNWqU1alTJ6t9+/bWD37wA2vUqFGtnj9lyhSrsrLSateunSXJeuONNyzLsqy3337bGjBggFVYWGh17drVuvDCC61//etfUY8Vq6yhn9zcXKtz587WgAEDrOuuu85avXp1q+eEvkl81apVlmVZ1meffWb95Cc/sfbdd1+roKDAKikpsY488khr7ty5Ec977733rKOPPtoqLCy0JFmDBw+OeL2lS5e2eSzL2v1N34cccoi1cOFCq1+/flYwGLQqKiqs6667ztq5c2fE89evX2+dccYZVqdOnaySkhLrZz/7mbVs2bJWbVNfX29deOGFVteuXa1AIBBxzO7du1tjx46NeN01a9ZY5557rtW5c2erffv2Vq9evaw777zTampqCj8m1N933nlnq3pJsm666aZWtwOAn5x66qlWXl6etXHjxpiPOeecc6zc3Fyrqqoq6ffNhx56yNpvv/2svLw864ADDrAefvhh65RTTrH69OkT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LV68WIsWLXK7eL5m5wpP07cjSKeuTq2Ebf66klxfbWvHimgvrWbP1Jgw/dywkxPnSja1n5exEh0AAAAAAAcNHDhQ//znP1VbW6t9991Xr732msrKyrR48WL17dvXseN+9913Ou+881RSUqKSkhKdd9552rJlS9znjBs3ToFAIOKnf//+jpXRaams8DRhxXQq0lnNmshzU2mX5q+bidW2bZXRjhXRXlrN7vSYyEbptItX31uwG2cCAAAAAAAOO/TQQ/XII49k9Jjnnnuu/vvf/+qVV16RJP385z/XeeedpxdeeCHu80488UTNmTMn/HteXp6j5XRSKis8vbo/cTqrWRN5birt0vJ1kylfKiu+0+27RI7ppf29nR4T2ah5uyQ7RhMdn176tIMTTK2/OSUBAAAAAMCndu3apS+++EIbN27Url27Iu475phjbD/ep59+qldeeUVLlizRUUcdJen7PdhXrlypXr16xXxuMBhUeXm57WWym1NBi5dC0kzKdLukEojHKmOiYyWRYxIuI6St8dJy3CV6Dtk1kWdqGN0WUycyzSkJAAAAAAA+tGTJEp177rlas2aNLMuKuC8QCKipqcn2Yy5evFglJSXhAF2S+vfvr5KSEr399ttxQ/SFCxeqtLRUe+yxhwYPHqxbbrlFpaWlMR9fX1+v+vrv972urq62pxJtiBa02BEaEZK25kYYl0poH6vvEg3lmEBBMtoaLy3HXVvvLaHzLKedbNlKx9Qwui2mnodmlQYAAAAAAJ+ZMGGC+vXrp/nz56uiokKBQMDxY1ZVVUUNvktLS1VVVRXzeSNGjNCZZ56p7t27a9WqVbrhhht07LHHavny5QoGg1GfM336dE2bNs22sicqWtDi1dDIdG60q52TGYmGckygIBltjZdkw+DQeda1KKiy4vy0y5fM8U1atW7qeWheiQAAAAAA8JHPP/9cf/7zn7Xffvul/VpTp05tM7BeunSpJEUN6y3Lihvin3322eH/7t27t/r166fu3btr/vz5GjNmTNTnTJkyRZMmTQr/Xl1drW7dusUtox2iBS2mrmD0OtPaNdnAz9RQLsSkABP2SXbc2X2eJXP85hNlod8Zj5FoCR/rce18t4sAADHxHgUAsIvJ1xSTy4bMOeqoo/TFF1/YEqJfdtllOuecc+I+pkePHvrggw+0YcOGVvd98803KisrS/h4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XJxVAzIkW+stZW/dqXd2od7Oy8ZVaSZe6/0w1qmDGbxeB6+XX6IOpqAO38vGa3262rVrp7322ivjx/XDuE0F9c4+2Vp36p1dTPv/eke2c5k0aZLOO+889evXTwMGDNBDDz2ktWvXasKECU4cDgAAZBjXegAAAABAtnAkRD/77LO1efNm3XzzzVq/fr169+6tl156Sd27d3ficAAAIMO41gMAAAAAsoVjXyw6ceJETZw40amXt00wGNRNN93U6mPmfpet9Zayt+7Um3png2ytt1tMuNb7oc+pgxm8Xgevl1+iDqagDvCibO1z6p1d9Zayt+7Um3qbIGBZluV2IQAAAAAAAAAAMFE7twsAAAAAAAAAAICpCNEBAAAAAAAAAIiBEB0AAAAAAAAAgBiyMkS/5ZZbNHDgQBUWFmqPPfZI6Dnjxo1TIBCI+Onfv7+zBbVZKvW2LEtTp05VZWWlCgoKNGTIEH388cfOFtRm3333nc477zyVlJSopKRE5513nrZs2RL3OV7t7/vvv189e/ZUfn6++vbtq3/84x9xH79o0SL17dtX+fn52meffTRr1qwMldReydR74cKFrfo2EAjos88+y2CJ0/Pmm29q9OjRqqysVCAQ0PPPP9/mc/zS18nW3Q/9jUirV6/W+PHj1bNnTxUUFGjffffVTTfdpIaGhrjPM+165vW/RfzwN4UX/z7ww3Xey9dsP1x//XAdnT59uo444ggVFRWptLRUp556qlauXNnm80zrC6TP69fSVPnhGpwKL163U+GHa30qvPz3Qar88HdFKrz8t0hWhugNDQ0688wzdckllyT1vBNPPFHr168P/7z00ksOldAZqdT7jjvu0IwZM3Tfffdp6dKlKi8v1/HHH6+amhoHS2qvc889V++9955eeeUVvfLKK3rvvfd03nnntfk8r/X3vHnzdOWVV+r666/XihUrNGjQII0YMUJr166N+vhVq1Zp5MiRGjRokFasWKHrrrtOV1xxhZ555pkMlzw9ydY7ZOXKlRH9u//++2eoxOnbvn27Dj/8cN13330JPd4vfS0lX/cQL/c3In322WfatWuXHnzwQX388ce6++67NWvWLF133XVxn2fa9czrf4v44W8Kr/194IfrvNev2X64/vrhOrpo0SJdeumlWrJkiRYsWKDGxkYNHz5c27dvj/kcE/sC6fP6tTRVfrgG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PXo0Kioq0LlzZwAv8wdvb+8mx0NERNSWuKYnkhfezoXISK1YsQJZWVn45JNPcOnSJRQVFSElJQWRkZFSHaVSiTNnzuD27dt48OABgJcL7tTUVJw7dw4FBQWYP38+7t6922hbLi4u8PHxQWZmZpP98vDwQFlZGRITE3H9+nXs2rVL40vCFi9ejL1792Lv3r0oLCzE2rVrcfXqVY1rZWRkYPDgwdK7z4iIiIxVa8blevb29pg9ezaWLVuGU6dO4cqVKwgPD4e5+f+n8G+//TZUKhXmzp2LzMxM/PHHH5g2bRq6desGlUol9evff//FqVOn8ODBA60fufb29oaLiwvOnj3bZN88PDxw8eJF/PbbbygsLERsbCwuXLigVmfx4sXYvHkzkpOTce3aNSxatAgPHz6Ujt+/fx8XLlzA2LFjpbK4uDgkJibi2rVrKCwsxMGDB+Hq6oqOHTtKdTIyMjBy5Mjm/AqJiIjaDNf0RPLCTXQiI+Xl5YXTp0+jqKgIgwcPhre3N2JjY6X7rAHAhg0bUFJSgl69ekkfn4qNjYWPjw9GjRqFoUOHwtXVFePGjWuyvXnz5uGHH35osp5KpUJ0dDQiIiIwcOBAnDt3TvqG73qhoaFYs2YNVqxYAV9fX5SWlmLhwoUa1zpw4ADmzp3bZJtERERtrbXjcr1t27YhODgYISEhGDFiBIKCguDr66tWJyEhAb6+vhg7diwCAgIghMDx48elj5gHBgZiwYIFCA0NhYuLC7Zu3dpgW+3atcOsWbOalQ8sWLAAEyZMQGhoKPz9/VFeXq72rnQAWLp0KaZPn47w8HDpli/jx4+Xjh85cgT+/v7o0qWLVGZvb48tW7bAz88P7777LkpKSnD8+HHphYOsrCxUVVVh4sSJzfsFEhERtRGu6YnkxUw052aLRCR7T548gaenJxITExv8whB9O3bsGJYtW4a8vDxYWPDOUkRERMbg3r176NevH3JycuDu7m7QtkJCQhAUFITly5c3+5xJkybB29sbq1atMmDPiIiITA/X9ESGxXeiExEAwNraGt9//730ETJDe/ToERISEhhsiYiIjEjXrl0RHx+PsrIyg7cVFBSEKVOmNLv+06dPMWDAAERHRxuwV0RERKaJa3oiw+I70YmIiIiI2kBZWRn69u2r9Xh+fr70haJERERERNR2uIlORERERNQGnj9/jpKSEq3HlUol391FRERERGQEuIlORERERERERERERKQF74lORERERERERERERKQFN9GJiIiIiIiIiIiIiLTgJjoRERERERERERERkRbcRCciIiIiIiIiIiIi0oKb6EREREREREREREREWnATnYiIiIiIiIiIiIhIC26iExERERERERERERFpwU10IiIiIiIiIiIiIiIt/ge7xQTm3+Bi+gAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Training Dataset Statistics:\n", - " theta theta_dot control theta_next theta_dot_next\n", - "count 1000.000000 1000.000000 1000.000000 1000.000000 1000.000000\n", - "mean -0.007058 0.001817 -0.007890 -0.007011 0.002818\n", - "std 0.903213 1.168873 0.579310 0.902824 1.168373\n", - "min -1.564972 -1.997035 -0.998503 -1.590624 -2.178955\n", - "25% -0.774411 -1.026903 -0.499810 -0.773115 -0.993660\n", - "50% -0.021604 -0.039369 -0.004702 -0.019158 -0.051045\n", - "75% 0.761550 1.008526 0.497493 0.760362 0.984905\n", - "max 1.567001 1.992192 0.999452 1.602169 2.180556\n", - "\n", - "Average change in one timestep:\n", - "Theta: 0.000046 ± 0.023336\n", - "Theta_dot: 0.001000 ± 0.139575\n" - ] - } - ], - "source": [ - "import torch\n", - "import numpy as np\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "\n", - "def generate_pendulum_dataset(n_samples=200, t_span=[0, 1], dt=0.02, output_file=\"pendulum_dataset.csv\"):\n", - " \"\"\"\n", - " Generate pendulum data and save to CSV.\n", - " CSV format: theta, theta_dot, control, theta_next, theta_dot_next\n", - " \"\"\"\n", - " t = np.arange(t_span[0], t_span[1], dt)\n", - " \n", - " def pendulum_dynamics(state, control=0.0, L=1.0, g=9.81, m=1.0, b=0.1):\n", - " theta, omega = state\n", - " dtheta = omega\n", - " domega = (-b*omega - m*g*L*np.sin(theta) + control)/(m*L**2)\n", - " return np.array([dtheta, domega])\n", - " \n", - " # Lists to store data\n", - " data = []\n", - " \n", - " # Generate multiple trajectories\n", - " for _ in range(n_samples):\n", - " # Random initial conditions\n", - " theta0 = np.random.uniform(-np.pi/2, np.pi/2)\n", - " omega0 = np.random.uniform(-2, 2)\n", - " state = np.array([theta0, omega0])\n", - " \n", - " # Random control input (optional)\n", - " control = np.random.uniform(-1, 1)\n", - " \n", - " # Generate one step data using RK4\n", - " k1 = pendulum_dynamics(state, control)\n", - " k2 = pendulum_dynamics(state + dt*k1/2, control)\n", - " k3 = pendulum_dynamics(state + dt*k2/2, control)\n", - " k4 = pendulum_dynamics(state + dt*k3, control)\n", - " \n", - " # Update state\n", - " next_state = state + (dt/6)*(k1 + 2*k2 + 2*k3 + k4)\n", - " \n", - " # Store the data point\n", - " data.append([state[0], state[1], control, next_state[0], next_state[1]])\n", - " \n", - " # Convert to DataFrame and save\n", - " df = pd.DataFrame(data, columns=['theta', 'theta_dot', 'control', 'theta_next', 'theta_dot_next'])\n", - " df.to_csv(output_file, index=False)\n", - " \n", - " print(f\"Generated {n_samples} samples and saved to {output_file}\")\n", - " \n", - " # Visualize some examples\n", - " plt.figure(figsize=(15, 5))\n", - " \n", - " # Plot theta distribution\n", - " plt.subplot(131)\n", - " plt.hist(df['theta'], bins=50)\n", - " plt.title('Theta Distribution')\n", - " plt.xlabel('Theta (rad)')\n", - " \n", - " # Plot theta_dot distribution\n", - " plt.subplot(132)\n", - " plt.hist(df['theta_dot'], bins=50)\n", - " plt.title('Angular Velocity Distribution')\n", - " plt.xlabel('Theta_dot (rad/s)')\n", - " \n", - " # Plot phase space\n", - " plt.subplot(133)\n", - " plt.scatter(df['theta'], df['theta_dot'], alpha=0.1, s=1)\n", - " plt.title('Phase Space')\n", - " plt.xlabel('Theta (rad)')\n", - " plt.ylabel('Theta_dot (rad/s)')\n", - " \n", - " plt.tight_layout()\n", - " plt.show()\n", - " \n", - " return df\n", - "\n", - "if __name__ == \"__main__\":\n", - " # Generate training dataset\n", - " train_df = generate_pendulum_dataset(n_samples=1000, dt=0.02, output_file=\"pendulum_train.csv\")\n", - " \n", - " # Generate test dataset with different initial conditions\n", - " test_df = generate_pendulum_dataset(n_samples=1000, dt=0.02, output_file=\"pendulum_test.csv\")\n", - " \n", - " # Print some statistics\n", - " print(\"\\nTraining Dataset Statistics:\")\n", - " print(train_df.describe())\n", - " \n", - " # Verify data consistency\n", - " theta_diff = train_df['theta_next'] - train_df['theta']\n", - " theta_dot_diff = train_df['theta_dot_next'] - train_df['theta_dot']\n", - " \n", - " print(\"\\nAverage change in one timestep:\")\n", - " print(f\"Theta: {theta_diff.mean():.6f} ± {theta_diff.std():.6f}\")\n", - " print(f\"Theta_dot: {theta_dot_diff.mean():.6f} ± {theta_dot_diff.std():.6f}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "ename": "KeyboardInterrupt", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[33], line 212\u001b[0m\n\u001b[1;32m 209\u001b[0m t \u001b[38;5;241m=\u001b[39m dataset\u001b[38;5;241m.\u001b[39mt\n\u001b[1;32m 211\u001b[0m \u001b[38;5;66;03m# Train model\u001b[39;00m\n\u001b[0;32m--> 212\u001b[0m losses \u001b[38;5;241m=\u001b[39m train_model(model, train_loader, t, dt, num_epochs, device)\n\u001b[1;32m 214\u001b[0m \u001b[38;5;66;03m# Test on a sample\u001b[39;00m\n\u001b[1;32m 215\u001b[0m model\u001b[38;5;241m.\u001b[39meval()\n", - "Cell \u001b[0;32mIn[33], line 152\u001b[0m, in \u001b[0;36mtrain_model\u001b[0;34m(model, train_loader, t, dt, epochs, device)\u001b[0m\n\u001b[1;32m 149\u001b[0m pred \u001b[38;5;241m=\u001b[39m model(initial_states, t\u001b[38;5;241m.\u001b[39mto(device), dt, controls)\n\u001b[1;32m 151\u001b[0m loss \u001b[38;5;241m=\u001b[39m torch\u001b[38;5;241m.\u001b[39mmean((pred \u001b[38;5;241m-\u001b[39m trajectories) \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39m \u001b[38;5;241m2\u001b[39m)\n\u001b[0;32m--> 152\u001b[0m loss\u001b[38;5;241m.\u001b[39mbackward()\n\u001b[1;32m 153\u001b[0m optimizer\u001b[38;5;241m.\u001b[39mstep()\n\u001b[1;32m 155\u001b[0m epoch_loss \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m loss\u001b[38;5;241m.\u001b[39mitem()\n", - "File \u001b[0;32m~/anaconda3/lib/python3.12/site-packages/torch/_tensor.py:581\u001b[0m, in \u001b[0;36mTensor.backward\u001b[0;34m(self, gradient, retain_graph, create_graph, inputs)\u001b[0m\n\u001b[1;32m 571\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m has_torch_function_unary(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 572\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m handle_torch_function(\n\u001b[1;32m 573\u001b[0m Tensor\u001b[38;5;241m.\u001b[39mbackward,\n\u001b[1;32m 574\u001b[0m (\u001b[38;5;28mself\u001b[39m,),\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 579\u001b[0m inputs\u001b[38;5;241m=\u001b[39minputs,\n\u001b[1;32m 580\u001b[0m )\n\u001b[0;32m--> 581\u001b[0m torch\u001b[38;5;241m.\u001b[39mautograd\u001b[38;5;241m.\u001b[39mbackward(\n\u001b[1;32m 582\u001b[0m \u001b[38;5;28mself\u001b[39m, gradient, retain_graph, create_graph, inputs\u001b[38;5;241m=\u001b[39minputs\n\u001b[1;32m 583\u001b[0m )\n", - "File \u001b[0;32m~/anaconda3/lib/python3.12/site-packages/torch/autograd/__init__.py:347\u001b[0m, in \u001b[0;36mbackward\u001b[0;34m(tensors, grad_tensors, retain_graph, create_graph, grad_variables, inputs)\u001b[0m\n\u001b[1;32m 342\u001b[0m retain_graph \u001b[38;5;241m=\u001b[39m create_graph\n\u001b[1;32m 344\u001b[0m \u001b[38;5;66;03m# The reason we repeat the same comment below is that\u001b[39;00m\n\u001b[1;32m 345\u001b[0m \u001b[38;5;66;03m# some Python versions print out the first line of a multi-line function\u001b[39;00m\n\u001b[1;32m 346\u001b[0m \u001b[38;5;66;03m# calls in the traceback and some print out the last line\u001b[39;00m\n\u001b[0;32m--> 347\u001b[0m _engine_run_backward(\n\u001b[1;32m 348\u001b[0m tensors,\n\u001b[1;32m 349\u001b[0m grad_tensors_,\n\u001b[1;32m 350\u001b[0m retain_graph,\n\u001b[1;32m 351\u001b[0m create_graph,\n\u001b[1;32m 352\u001b[0m inputs,\n\u001b[1;32m 353\u001b[0m allow_unreachable\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[1;32m 354\u001b[0m accumulate_grad\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[1;32m 355\u001b[0m )\n", - "File \u001b[0;32m~/anaconda3/lib/python3.12/site-packages/torch/autograd/graph.py:825\u001b[0m, in \u001b[0;36m_engine_run_backward\u001b[0;34m(t_outputs, *args, **kwargs)\u001b[0m\n\u001b[1;32m 823\u001b[0m unregister_hooks \u001b[38;5;241m=\u001b[39m _register_logging_hooks_on_whole_graph(t_outputs)\n\u001b[1;32m 824\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m--> 825\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m Variable\u001b[38;5;241m.\u001b[39m_execution_engine\u001b[38;5;241m.\u001b[39mrun_backward( \u001b[38;5;66;03m# Calls into the C++ engine to run the backward pass\u001b[39;00m\n\u001b[1;32m 826\u001b[0m t_outputs, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs\n\u001b[1;32m 827\u001b[0m ) \u001b[38;5;66;03m# Calls into the C++ engine to run the backward pass\u001b[39;00m\n\u001b[1;32m 828\u001b[0m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[1;32m 829\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m attach_logging_hooks:\n", - "\u001b[0;31mKeyboardInterrupt\u001b[0m: " - ] - } - ], - "source": [ - "import torch\n", - "import torch.nn as nn\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "from torch.utils.data import Dataset, DataLoader\n", - "\n", - "class ODEFunc(nn.Module):\n", - " def __init__(self, hidden_dim=32, control_dim=1):\n", - " super().__init__()\n", - " self.net = nn.Sequential(\n", - " nn.Linear(3, hidden_dim),\n", - " nn.Tanh(),\n", - " nn.Linear(hidden_dim, hidden_dim),\n", - " nn.Tanh(),\n", - " nn.Linear(hidden_dim, 2)\n", - " )\n", - " \n", - " def forward(self, t, y, u):\n", - "\n", - " if y.dim() == 1:\n", - " y = y.unsqueeze(0) # (1,2)\n", - " if u.dim() == 1:\n", - " u = u.unsqueeze(0) # (1,control_dim)\n", - "\n", - " # Concatenate y and u along last dim\n", - " inp = torch.cat([y, u], dim=-1) # shape (batch_size, 2+control_dim)\n", - " \n", - " # Pass through the network\n", - " out = self.net(inp) # shape (batch_size, 2)\n", - " \n", - " # If we started with unbatched data, squeeze back\n", - " if out.shape[0] == 1:\n", - " out = out.squeeze(0)\n", - " return out\n", - "\n", - "def rk4_step(func, t, y, dt, u):\n", - " k1 = func(t, y, u)\n", - " k2 = func(t + dt/2, y + dt*k1/2, u)\n", - " k3 = func(t + dt/2, y + dt*k2/2, u)\n", - " k4 = func(t + dt, y + dt*k3, u)\n", - " return y + (dt/6)*(k1 + 2*k2 + 2*k3 + k4)\n", - "\n", - "class NeuralODE(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - " self.func = ODEFunc()\n", - " \n", - " def forward(self, y0, t, dt):\n", - " y = y0\n", - " trajectory = [y]\n", - " \n", - " for i in range(len(t)-1):\n", - " y = rk4_step(self.func, t[i], y, dt)\n", - " trajectory.append(y)\n", - " \n", - " return torch.stack(trajectory, dim=1)\n", - " \n", - "class NeuralODE(nn.Module):\n", - " def __init__(self, hidden_dim=32, control_dim=1):\n", - " super().__init__()\n", - " self.func = ODEFunc(hidden_dim, control_dim)\n", - " \n", - " def forward(self, y0, t, dt, controls):\n", - " if y0.dim() == 1:\n", - " y0 = y0.unsqueeze(0) # -> (1,2)\n", - " \n", - " if controls.dim() == 2:\n", - " controls = controls.unsqueeze(0)\n", - " \n", - " y = y0\n", - " trajectory = [y]\n", - " \n", - " for i in range(len(t) - 1):\n", - " u_i = controls[:, i, :]\n", - " new_states = []\n", - " for batch_idx in range(y.shape[0]):\n", - " y_single = y[batch_idx]\n", - " u_single = u_i[batch_idx]\n", - " y_new = rk4_step(self.func, t[i], y_single, dt, u_single)\n", - " new_states.append(y_new.unsqueeze(0))\n", - " \n", - " y = torch.cat(new_states, dim=0) # (batch_size, 2)\n", - " trajectory.append(y)\n", - " \n", - " # stack trajectory along time dimension: (batch_size, T, 2)\n", - " trajectory = torch.stack(trajectory, dim=1)\n", - " return trajectory\n", - "\n", - "class PendulumDataset(Dataset):\n", - " def __init__(self, csv_file, seq_length=100):\n", - " df = pd.read_csv(csv_file)\n", - " \n", - " self.initial_states = torch.tensor(df[['theta', 'theta_dot']].values, dtype=torch.float32)\n", - " self.controls = torch.tensor(df[['control']].values, dtype=torch.float32)\n", - " \n", - " dt = 0.02\n", - " t = torch.arange(0, seq_length * dt, dt)\n", - " \n", - " self.trajectories = []\n", - " self.control_trajectories = []\n", - " \n", - " for i in range(len(self.initial_states)):\n", - " state = self.initial_states[i]\n", - " control = self.controls[i]\n", - " \n", - " control_seq = control.repeat(seq_length - 1, 1)\n", - " \n", - " trajectory = [state]\n", - " for j in range(seq_length - 1):\n", - " theta, theta_dot = state\n", - " # Add torque to the dynamics\n", - " theta_ddot = -9.81 * torch.sin(theta) - 0.1*theta_dot + control\n", - " theta_new = theta + theta_dot * dt\n", - " theta_dot_new = theta_dot + theta_ddot * dt\n", - " state = torch.tensor([theta_new, theta_dot_new])\n", - " trajectory.append(state)\n", - " \n", - " trajectory = torch.stack(trajectory)\n", - " self.trajectories.append(trajectory)\n", - " self.control_trajectories.append(control_seq) # shape (seq_length-1, 1)\n", - " \n", - " self.trajectories = torch.stack(self.trajectories) # (N, seq_length, 2)\n", - " self.control_trajectories = torch.stack(self.control_trajectories) # (N, seq_length-1, 1)\n", - " self.t = t\n", - " \n", - " def __len__(self):\n", - " return len(self.initial_states)\n", - " \n", - " def __getitem__(self, idx):\n", - " return (self.initial_states[idx], \n", - " self.trajectories[idx],\n", - " self.control_trajectories[idx])\n", - "\n", - "def train_model(model, train_loader, t, dt, epochs=100, device=\"cpu\"):\n", - " model = model.to(device)\n", - " optimizer = torch.optim.Adam(model.parameters(), lr=0.01)\n", - " losses = []\n", - " \n", - " for epoch in range(epochs):\n", - " epoch_loss = 0.0\n", - " \n", - " for initial_states, trajectories, controls in train_loader:\n", - " initial_states = initial_states.to(device)\n", - " trajectories = trajectories.to(device)\n", - " controls = controls.to(device)\n", - " \n", - " optimizer.zero_grad()\n", - " pred = model(initial_states, t.to(device), dt, controls)\n", - " \n", - " loss = torch.mean((pred - trajectories) ** 2)\n", - " loss.backward()\n", - " optimizer.step()\n", - " \n", - " epoch_loss += loss.item()\n", - " \n", - " epoch_loss /= len(train_loader)\n", - " losses.append(epoch_loss)\n", - " if (epoch + 1) % 10 == 0:\n", - " print(f\"Epoch {epoch+1}, Loss: {epoch_loss:.4f}\")\n", - " \n", - " return losses\n", - "\n", - "def plot_results(true_traj, pred_traj, t, losses=None):\n", - " plt.figure(figsize=(15, 5))\n", - " \n", - " plt.subplot(1, 3, 1)\n", - " plt.plot(t, true_traj[:, 0].cpu().detach(), label='True θ')\n", - " plt.plot(t, pred_traj[:, 0].cpu().detach(), '--', label='Predicted θ')\n", - " plt.xlabel('Time')\n", - " plt.ylabel('Angle (rad)')\n", - " plt.legend()\n", - " plt.title('Angle Comparison')\n", - " \n", - " plt.subplot(1, 3, 2)\n", - " plt.plot(t, true_traj[:, 1].cpu().detach(), label='True θ_dot')\n", - " plt.plot(t, pred_traj[:, 1].cpu().detach(), '--', label='Predicted θ_dot')\n", - " plt.xlabel('Time')\n", - " plt.ylabel('Angular velocity (rad/s)')\n", - " plt.legend()\n", - " plt.title('Angular Velocity Comparison')\n", - " \n", - " if losses is not None:\n", - " plt.subplot(1, 3, 3)\n", - " plt.plot(losses)\n", - " plt.xlabel('Epoch')\n", - " plt.ylabel('Loss')\n", - " plt.title('Training Loss')\n", - " \n", - " plt.tight_layout()\n", - " plt.show()\n", - "\n", - "# Parameters\n", - "csv_file = \"pendulum_train.csv\"\n", - "batch_size = 32\n", - "seq_length = 100\n", - "dt = 0.02\n", - "num_epochs = 100\n", - "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", - "\n", - "# Create dataset and dataloader\n", - "dataset = PendulumDataset(csv_file, seq_length=seq_length)\n", - "train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)\n", - "\n", - "# Initialize model\n", - "model = NeuralODE()\n", - "\n", - "# Time points\n", - "t = dataset.t\n", - "\n", - "# Train model\n", - "losses = train_model(model, train_loader, t, dt, num_epochs, device)\n", - "\n", - "# Test on a sample\n", - "model.eval()\n", - "initial_state, true_trajectory = dataset[0]\n", - "initial_state = initial_state.to(device)\n", - "\n", - "torch.save(model, \"neural_ode_model_complete.pth\")\n", - "\n", - "with torch.no_grad():\n", - " pred_trajectory = model(initial_state.unsqueeze(0), t.to(device), dt)\n", - " pred_trajectory = pred_trajectory.squeeze(0)\n", - "\n", - "# Plot results\n", - "plot_results(true_trajectory, pred_trajectory, t, losses)" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ipykernel_8133/3577289463.py:2: FutureWarning: You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.\n", - " model = torch.load(\"neural_ode_model_complete.pth\").to(device)\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0wAAAHUCAYAAAAJN6iwAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAADbN0lEQVR4nOzdd3hT1RvA8W/SpumedNDdAoWy994gQ5ANKiAbZKgo4OCnAiKKKAgoUxSQpaAM2VOWDJkKskdbyuiC0pbuJPf3RyRQodBCS9ryfp4nj73nnnvzJhbIm3POe1SKoigIIYQQQgghhHiA2twBCCGEEEIIIURBJQmTEEIIIYQQQmRDEiYhhBBCCCGEyIYkTEIIIYQQQgiRDUmYhBBCCCGEECIbkjAJIYQQQgghRDYkYRJCCCGEEEKIbEjCJIQQQgghhBDZkIRJCCGEEEIIIbIhCZMQQhQiCxcuRKVSmR6Wlpb4+vrSt29frl279kxiCAwMpE+fPqbjXbt2oVKp2LVrV67us3//fsaNG8ft27fzND6APn36EBgYmO35/76P2T0edY+c+u/7lddmzZrFwoUL8+3+QgjxvLM0dwBCCCFyb8GCBZQpU4bU1FT27NnDxIkT2b17NydPnsTOzu6ZxlK1alUOHDhA2bJlc3Xd/v37+eSTT+jTpw/Ozs75E1w22rRpw4EDB7K01alThy5dujBy5EhTm1arfernWr16NY6Ojk99n+zMmjWLYsWK5WtSJoQQzzNJmIQQohAqX7481atXB6BJkybo9Xo+/fRT1qxZQ48ePR56TUpKCra2tnkei6OjI7Vr187z++Ynd3d33N3dH2j39PR85GvR6/XodLpcJVJVqlR5ohjN6UlepxBCFFUyJU8IIYqAux/yIyIiAOOUNHt7e06ePEmLFi1wcHCgWbNmAGRkZDBhwgTKlCmDVqvF3d2dvn37Ehsbm+WemZmZvPfee3h5eWFra0v9+vU5dOjQA8+d3ZS8P//8k5deegk3Nzesra0pUaIEb7/9NgDjxo3j3XffBSAoKMg0Be7+eyxfvpw6depgZ2eHvb09LVu25Pjx4w88/8KFCyldujRarZbQ0FAWLVr0RO/hf4WHh6NSqfjyyy+ZMGECQUFBaLVadu7cSVpaGiNHjqRy5co4OTnh6upKnTp1+O233x64z8Om5CUmJjJq1CiCgoKwsrLCx8eHt99+m+Tk5Cz9DAYD3377LZUrV8bGxgZnZ2dq167N2rVrTfc+deoUu3fvfug0witXrtCzZ088PDxM78+UKVMwGAyPfZ3btm3D2dmZ119//aHvjYWFBV999dVTvMNCCFE4yAiTEEIUARcvXgTIMmqSkZFBu3bteP311/nggw/Q6XQYDAbat2/P3r17ee+996hbty4RERGMHTuWxo0bc+TIEWxsbAAYOHAgixYtYtSoUbzwwgv8888/dOrUiaSkpMfGs2XLFl566SVCQ0P5+uuv8ff3Jzw8nK1btwIwYMAAbt26xbfffsuqVasoXrw4gGla3+eff85HH31E3759+eijj8jIyOCrr76iQYMGHDp0yNRv4cKF9O3bl/bt2zNlyhQSEhIYN24c6enpqNV5853gN998Q0hICJMnT8bR0ZFSpUqRnp7OrVu3GDVqFD4+PmRkZLB9+3Y6derEggUL6NWrV7b3S0lJoVGjRly9epX//e9/VKxYkVOnTjFmzBhOnjzJ9u3bUalUgDHxXbJkCf3792f8+PFYWVlx7NgxwsPDAeN0vy5duuDk5MSsWbOAe9MIY2NjqVu3LhkZGXz66acEBgayfv16Ro0axaVLl0z9H/U6+/Xrx3fffceXX36Jk5OTqe+sWbOwsrKiX79+efIeCyFEgaYIIYQoNBYsWKAAysGDB5XMzEwlKSlJWb9+veLu7q44ODgoUVFRiqIoSu/evRVAmT9/fpbrf/rpJwVQVq5cmaX98OHDCqDMmjVLURRFOXPmjAIo77zzTpZ+S5cuVQCld+/epradO3cqgLJz505TW4kSJZQSJUooqamp2b6Wr776SgGUsLCwLO1XrlxRLC0tlTfffDNLe1JSkuLl5aV069ZNURRF0ev1ire3t1K1alXFYDCY+oWHhysajUYJCAjI9rkfBlCGDRtmOg4LC1MApUSJEkpGRsYjr9XpdEpmZqbSv39/pUqVKlnOBQQEZHm/Jk6cqKjVauXw4cNZ+v36668KoGzcuFFRFEXZs2ePAigffvjhI5+7XLlySqNGjR5o/+CDDxRA+fPPP7O0DxkyRFGpVMq5c+ce+zovXbqkqNVqZerUqaa21NRUxc3NTenbt+8j4xJCiKJCpuQJIUQhVLt2bTQaDQ4ODrRt2xYvLy82bdqEp6dnln6dO3fOcrx+/XqcnZ156aWX0Ol0pkflypXx8vIyTYnbuXMnwAProbp164al5aMnJ5w/f55Lly7Rv39/rK2tc/3atmzZgk6no1evXllitLa2plGjRqYYz507x/Xr1+nevbtpRAYgICCAunXr5vp5s9OuXTs0Gs0D7b/88gv16tXD3t4eS0tLNBoNP/zwA2fOnHnk/davX0/58uWpXLlyltfXsmXLLNMSN23aBMCwYcOeKO7ff/+dsmXLUrNmzSztffr0QVEUfv/998e+zuDgYNq2bcusWbNQFAWAZcuWcfPmTd54440niksIIQobmZInhBCF0KJFiwgNDcXS0hJPT0/TlLb72draPlCdLTo6mtu3b2NlZfXQ+8bFxQFw8+ZNALy8vLKct7S0xM3N7ZGx3V0L5evrm7MX8x/R0dEA1KhR46Hn7061yy7Gu213p609rYe9t6tWraJbt2507dqVd999Fy8vLywtLZk9ezbz589/5P2io6O5ePHiQ5MwuPf/IDY2FgsLi4e+vpy4efPmQ8uie3t7m87f72GvE2D48OE0a9aMbdu20aJFC2bOnEmdOnWoWrXqE8UlhBCFjSRMQghRCIWGhpqq5GXn/lGXu4oVK4abmxubN29+6DUODg4ApqQoKioKHx8f03mdTvfAB+3/uruO6urVq4/sl51ixYoB8OuvvxIQEJBtv/tj/K+HtT2ph72PS5YsISgoiOXLl2c5n56e/tj7FStWDBsbm2wTq7uv393dHb1eT1RUVLbJzKO4ublx48aNB9qvX7+e5XnuetjrBGjatCnly5dnxowZ2Nvbc+zYMZYsWZLreIQQorCSKXlCCPEcadu2LTdv3kSv11O9evUHHqVLlwagcePGACxdujTL9StWrECn0z3yOUJCQihRogTz589/ZAJxtzhBampqlvaWLVtiaWnJpUuXHhrj3USxdOnSFC9enJ9++sk0XQyMlQL379+fszfkCalUKqysrLIkGVFRUQ+tkvdfbdu25dKlS7i5uT30td0dFWrdujUAs2fPfuT9tFrtA+8hQLNmzTh9+jTHjh3L0r5o0SJUKhVNmjR5bKx3vfXWW2zYsIHRo0fj6elJ165dc3ytEEIUdjLCJIQQz5FXXnmFpUuX8uKLLzJ8+HBq1qyJRqPh6tWr7Ny5k/bt29OxY0dCQ0Pp2bMn06ZNQ6PR0Lx5c/755x9TBbXHmTlzJi+99BK1a9fmnXfewd/fnytXrrBlyxZTElahQgUApk+fTu/evdFoNJQuXZrAwEDGjx/Phx9+yOXLl2nVqhUuLi5ER0dz6NAh7Ozs+OSTT1Cr1Xz66acMGDCAjh07MnDgQG7fvs24ceOeeBpbTrVt25ZVq1YxdOhQunTpQmRkJJ9++inFixfnwoULj7z27bffZuXKlTRs2JB33nmHihUrYjAYuHLlClu3bmXkyJHUqlWLBg0a8NprrzFhwgSio6Np27YtWq2W48ePY2try5tvvml6H3/++WeWL19OcHAw1tbWVKhQgXfeeYdFixbRpk0bxo8fT0BAABs2bGDWrFkMGTKEkJCQHL/enj17Mnr0aPbs2cNHH32U7ZROIYQoksxcdEIIIUQu3K2S998Ka//Vu3dvxc7O7qHnMjMzlcmTJyuVKlVSrK2tFXt7e6VMmTLK66+/rly4cMHULz09XRk5cqTi4eGhWFtbK7Vr11YOHDjwQNW3h1XJUxRFOXDggNK6dWvFyclJ0Wq1SokSJR6oujd69GjF29tbUavVD9xjzZo1SpMmTRRHR0dFq9UqAQEBSpcuXZTt27dnucf333+vlCpVSrGyslJCQkKU+fPnK717986zKnlfffXVQ/t/8cUXSmBgoKLVapXQ0FBl3rx5ytixY5X//tMaEBCg9OnTJ0vbnTt3lI8++kgpXbq0YmVlpTg5OSkVKlRQ3nnnHVOlQ0UxVgKcOnWqUr58eVO/OnXqKOvWrTP1CQ8PV1q0aKE4ODgoQJbXHRERoXTv3l1xc3NTNBqNUrp0aeWrr75S9Hp9jl/nXX369FEsLS2Vq1evPrKfEEIUNSpFuW8egxBCCCHylKurK/369WPy5MnmDuWJZWRkEBgYSP369VmxYoW5wxFCiGdKpuQJIYQQ+eDEiRNs3LiR+Ph46tSpY+5wnkhsbCznzp1jwYIFREdH88EHH5g7JCGEeOYkYRJCCCHywfDhwzl79iyjRo2iU6dO5g7niWzYsIG+fftSvHhxZs2aJaXEhRDPJZmSJ4QQQgghhBDZkLLiQgghhBBCCJENSZiEEEIIIYQQIhuSMAkhhBBCCCFENp6rog8Gg4Hr16/j4OCQZXd2IYQQQgghxPNFURSSkpLw9vZGrc5+HOm5SpiuX7+On5+fucMQQgghhBBCFBCRkZH4+vpme/65SpgcHBwA45vi6Oho5miEEEIIIYQQ5pKYmIifn58pR8jOc5Uw3Z2G5+joKAmTEEIIIYQQ4rFLdaTogxBCCCGEEEJkQxImIYQQQgghhMiGJExCCCGEEEIIkY3nag1TTiiKgk6nQ6/XmzsUUcRZWFhgaWkpJe6FEEIIIQowSZjuk5GRwY0bN0hJSTF3KOI5YWtrS/HixbGysjJ3KEIIIYQQ4iEkYfqXwWAgLCwMCwsLvL29sbKykm/+Rb5RFIWMjAxiY2MJCwujVKlSj9wwTQghhBBCmIckTP/KyMjAYDDg5+eHra2tucMRzwEbGxs0Gg0RERFkZGRgbW1t7pCEEEIIIcR/yFfa/yHf8otnSX7fhBBCCCEKNvm0JoQQQgghhBDZkIRJCCGEEEIIIbIhCZPIsXHjxlG5cmXTcZ8+fejQocMzjyM8PByVSsVff/2VbR9FURg/fjyenp44OjoycOBAqX4ohBBCCCFyTRKmQq5Pnz6oVCpUKhUajYbg4GBGjRpFcnJyvj/39OnTWbhwYY765iTJyUvTp09n9uzZLF++nD179nD48GGGDRv2TJ5bCCGEEEIUHZIwFQGtWrXixo0bXL58mQkTJjBr1ixGjRr10L6ZmZl59rxOTk44Ozvn2f3yisFgYOLEiYwdO5bGjRtTuXJl5s6dy6JFi4iMjDR3eEIIIYQQohCRhCkbiqKQkqEzy0NRlFzFqtVq8fLyws/Pj+7du9OjRw/WrFkD3JtGN3/+fIKDg9FqtSiKQkJCAoMGDcLDwwNHR0eaNm3K33//neW+X3zxBZ6enjg4ONC/f3/S0tKynP/vlDyDwcCkSZMoWbIkWq0Wf39/PvvsMwCCgoIAqFKlCiqVisaNG5uuW7BgAaGhoVhbW1OmTBlmzZqV5XkOHTpElSpVsLa2pnr16hw/fvyR78fff/9NTEwML774oqmtZs2aODg4sGPHjhy9p0IIIYQQ2TLoIT4Cwv+Av5fD3q9h0wewejD89CoseBHmNYO5jWBOfZhd33i8oA0s6Qy/9oON78HuL+HIAri4HeIugi7d3K9MPITsw5SN1Ew9ZcdsMctznx7fElurJ/9fY2Njk2Uk6eLFi6xYsYKVK1diYWEBQJs2bXB1dWXjxo04OTkxd+5cmjVrxvnz53F1dWXFihWMHTuWmTNn0qBBAxYvXsw333xDcHBwts87evRo5s2bx9SpU6lfvz43btzg7NmzgDHpqVmzJtu3b6dcuXJYWVkBMG/ePMaOHcuMGTOoUqUKx48fZ+DAgdjZ2dG7d2+Sk5Np27YtTZs2ZcmSJYSFhTF8+PBHvv7Lly+j1Wrx8/MztalUKkqWLMnly5ef+H0VQgghxHMoNR6uH4drxyD6FMSdh5sXQZf2+GtzTQUuAeBZHjzLGR++NcDROx+eS+SUJExFzKFDh1i2bBnNmjUztWVkZLB48WLc3d0B+P333zl58iQxMTFotVoAJk+ezJo1a/j1118ZNGgQ06ZNo1+/fgwYMACACRMmsH379gdGme5KSkpi+vTpzJgxg969ewNQokQJ6tevD2B6bjc3N7y8vEzXffrpp0yZMoVOnToBxpGo06dPM3fuXHr37s3SpUvR6/XMnz8fW1tbypUrx9WrVxkyZEi270FKSgoZGRk4ODhkaU9LS8sysiWEEEII8YDE68aRo7A9ELEfbl16eD8LK3DyBUcf48PBC2ycwdrJ+LC0BrUlqI1fVqPLMCZZujRIvQ0pcZAcB3eijaNVtyMgMwXiw42Ps+vvPZeTH/jVBP86UKIpuJXI3/dAZCEJUzZsNBacHt/SbM+dG+vXr8fe3h6dTkdmZibt27fn22+/NZ0PCAgwJSwAR48e5c6dO7i5uWW5T2pqKpcuGf9SOHPmDIMHD85yvk6dOuzcufOhMZw5c4b09PQsidrjxMbGEhkZSf/+/Rk4cKCpXafT4eTkZLpvpUqVsLW1zRLHo9ja2mJra/tAgYkuXbpkuY8QQgghBHodRP4J5zfD+S0Qd+7BPi6B4F0VilcC9zJQrJSxTZ27z2yPpChwJ8b4/NGnIPofuPG38eeESOPjn5X34inRFEq/CEENwVKbd3GIB0jClA2VSvVU0+KepSZNmjB79mw0Gg3e3t5oNJos5+3s7LIcGwwGihcvzq5dux6415MWcbCxscn1NQaDATBOy6tVq1aWc3enDuZ2PRdAcHAwKSkp+Pn5mUbQwDgK9qgphUIIIYR4TugzIWw3/LPaOJKTdvveOZXamBgF1ofABsYpcbau+R+TSgUOnsZHUMN77el34NpRiDxkjPnKQeMI1JH5xofWEUJaQtn2UKqFJE/5oHBkBOKR7OzsKFmyZI77V61alaioKCwtLQkMDHxon9DQUA4ePEivXr1MbQcPHsz2nqVKlcLGxoYdO3aYpvHd7+6aJb1eb2rz9PTEx8eHy5cv06NHj4fet2zZsixevJjU1FRTUvaoOAAqVqyIu7s7f/zxh2nE69KlS4SHh+dqBEwIIYQQRYiiwI2/4PgSOLUaUm7eO2fjakw2QloaR25snM0V5YO09hDcyPho9K4xgQr/Ay5shbMb4E4UnPzF+LB2hvKdoNKrxkRPpTJ39EWCJEzPoebNm1OnTh06dOjApEmTKF26NNevX2fjxo106NCB6tWrM3z4cHr37k316tWpX78+S5cu5dSpU9mO0FhbW/P+++/z3nvvYWVlRb169YiNjeXUqVP0798fDw8PbGxs2Lx5M76+vlhbW+Pk5MS4ceN46623cHR0pHXr1qSnp3PkyBHi4+MZMWIE3bt358MPP6R///589NFHhIeHM3ny5Ee+PgsLC0aPHs3QoUNZuHAhjo6ODBs2jF69emUpBCGEEEKI50DqbTixHI4thuiT99pt3aBsB2OC4V8nb6fX5SetPZRuZXy8OBmuHYHTv8E/qyDp+r2RJ/cyUL0/VHrZuKZKPDFJmJ5DKpWKjRs38uGHH9KvXz9iY2Px8vKiYcOGeHp6AvDyyy9z6dIl3n//fdLS0ujcuTNDhgxhy5bsKwd+/PHHWFpaMmbMGK5fv07x4sVN66AsLS355ptvGD9+PGPGjKFBgwbs2rWLAQMGYGtry1dffcV7772HnZ0dFSpU4O233wbA3t6edevWMXjwYKpUqULZsmWZNGkSnTt3fuRrHD58OAkJCbRv3560tDS6devGN998kzdvoBBCCCEKvpizcOg7+PsnYzEFMBZqCH0JKnWH4MZgUcg/CqvVxmIQfjXhhfHGQhUnlhsTqNizsOld2D4OKnaF2sPAPcTcERdKKuVJFokUUomJiTg5OZGQkICjo2OWc2lpaYSFhREUFIS1tbWZIhTPG/m9E0IIIfKQohiThn3T4NLv99rdQ6F6X6jQ9dmsRzK3tATj/lBHfjAmTneFtIa6b0JAXZmux6Nzg/sV8rRaCCGEEEI89wwGY/GGP6bC9WPGNpXaWEWu1uvG4g3PU4Jg7QS1BkHNgRCxDw7MgnMb4fwm48O3JjR6H0o2e77elyckCZMQQgghhCicDAY4uw52ToTYM8Y2S2uo2gvqDDOW336eqVT/VvurD3EX4MBM4xTFq4dgaWfwqW5MnEq9IInTI0jCJIQQQgghChdFMe6ZtHMCRP1byEHrZBxRqTUY7N0fff3zqFgpeGkaNP4A9n8Lh38wFoxY1tVY9KL5OPCvbe4oCyRJmIQQQgghROFx9Qhs/Riu7DceWzlA7SHGEaWCVA68oHLwgpafQb3hsP8bODQPrhyA+S2Na5yajwOPMuaOskCRhEkIIYQQQhR8t8JgxyfGPZTAOPWu1mDjB//noZBDXrP3gBYToPZQ2PWFcX+q85uM+zvV6A+NR8v7+i+1uQMQQgghhBAiWxnJsONTmFnr32RJBZV7wpvH4IVP5EP903L0hnbfwLA/oUxbUPTGcuzfVIGDc0CvM3eEZicJkxBCCCGEKHgUBU7+CjNqwN7JoE837p00+A/oMBOcfMwdYdFSrBS8shR6rwPP8pB2Gza/D/Maw7Wj5o7OrCRhEkIIIYQQBUt8OCzpBCv7Q+I1cPaHl5fCa2vAq7y5oyvaghrC63ug7TSwdjYW1fi+OWx6H9KTzB2dWUjCJIQQQgghCga9DvbPgFl1jBvPWmih8f9g2CEIbSulr58VtYVxo983jkCFbqAY4M85MKMmnFlv7uieOUmYRI6NGzeOypUrm4779OlDhw4dnnkc4eHhqFQq/vrrr2z7KIrC+PHj8fT0xNHRkYEDB5KSkvLEz7lw4UKcnZ2f+HohhBBCPEbUSfihOWz9EDJTIKA+DNkPjd8HjU2+Pa2iKCgGg+lYfyeZ9IsXSb9wgbTz58mIiCAzOgZ9YiKK7jlbz2PvDp3nwWurwSUIkq7D8h7wU3dIuGru6J6ZQpswTZw4EZVKxdtvv23uUMyqT58+qFQqVCoVGo2G4OBgRo0aRXJycr4/9/Tp01m4cGGO+uYkyclL06dPZ/bs2Sxfvpw9e/Zw+PBhhg0b9kye+y6VSsWaNWue6XMKIYQQhU5mKmwfB3MbwfXjxv2UXvrGuJamWMm8eYroGBI3bSLuu3nc+PhjrgwaxOVOnbjQsBFnK1QkafNmU9+UPw9yue1LXH6pHWHt2nOpZSsuNmrE+Zq1OFu+AreWLL133+vXifl6KvHLV5C8fz+ZMTEoipInMRcoJZrC0APQYCSoLeHcBmMRjqMLjWvNirhCWVb88OHDfPfdd1SsWNHcoRQIrVq1YsGCBWRmZrJ3714GDBhAcnIys2fPfqBvZmYmGo0mT57XyckpT+6T1wwGAxMnTuSTTz6hcePGAMydO5e6desyfvx4/Pz8zBugEEIIIYyuHIQ1Q+DWZeNx2fbQ+kvjXkFPQJ+UROqJE6SdOIF906ZYly4NQMqRw1wfOSr76+77olllZYWFszOojeMKSno6htRU+HcUysLB3tQ37dw5bn73XZZ7qZ2c0JYsiXXZsji1a4dNhSKy5kpjA83GQIWusG44RP5p/O+5TcYE18HT3BHmm0I3wnTnzh169OjBvHnzcHFxyb8nUhRjGUtzPHKZqWu1Wry8vPDz86N79+706NHDNLJxdxrd/PnzCQ4ORqvVoigKCQkJDBo0CA8PDxwdHWnatCl///13lvt+8cUXeHp64uDgQP/+/UlLS8ty/r9T8gwGA5MmTaJkyZJotVr8/f357LPPAAgKCgKgSpUqqFQqUyIDsGDBAkJDQ7G2tqZMmTLMmjUry/McOnSIKlWqYG1tTfXq1Tl+/Pgj34+///6bmJgYXnzxRVNbzZo1cXBwYMeOHTl6TxcuXIi/vz+2trZ07NiRmzdvPtBn9uzZlChRAisrK0qXLs3ixYtN5wIDAwHo2LEjKpXKdCyEEEIIQJ8Jv0+ABa2NyZKDN7yyDLotylWypIuPJ3HLVm588gmXX3qJ8zVrEdl/ALHTvyHl4EFTP22JEthUqYJT+3YUGzaM4p9NwG/uHAJ//ZWSO3/HqV07U1/7Bg0IOXiAkP37CNm/j9JHj1Dm1D+UOfE3pfbvw6F5c1NfjacnLj16YN+oEVYBAaBWY0hIIPXoUeIXLyYjPMzUNz0sjFtLlpJ+8WLhHoXyCIW+m6HFZ2BhBec3w+w6RXptU6EbYRo2bBht2rShefPmTJgw4ZF909PTSU9PNx0nJibm/IkyU+Bz7ycN8+n87zpY2T3x5TY2NmRmZpqOL168yIoVK1i5ciUWFhYAtGnTBldXVzZu3IiTkxNz586lWbNmnD9/HldXV1asWMHYsWOZOXMmDRo0YPHixXzzzTcEBwdn+7yjR49m3rx5TJ06lfr163Pjxg3Onj0LGJOemjVrsn37dsqVK4eVlRUA8+bNY+zYscyYMYMqVapw/PhxBg4ciJ2dHb179yY5OZm2bdvStGlTlixZQlhYGMOHD3/k6798+TJarTbLSJJKpaJkyZJcvnz5se/fn3/+Sb9+/fj888/p1KkTmzdvZuzYsVn6rF69muHDhzNt2jSaN2/O+vXr6du3L76+vjRp0oTDhw/j4eHBggULaNWqlel9F0IIIZ57cRdh1QDj9DuASq9C60lgnbuZK6l//034K68+8EWzxtcXm8qVsSpxbzqfdZkyBP607IlDVqlUYGWFpWvWPZ+sy5bFq2xZ07EhPZ2My5dJv3CB1JP/YFu1quncnd9/J+aryQBYenpi37gx9o0bYVe7Nmqb/FujlS/Uaqj7BpRoAqteh+iTxrVNlXtCq4lg7WjuCPNUoUqYfv75Z44dO8bhw4dz1P/utKznyaFDh1i2bBnNmjUztWVkZLB48WLc3d0B+P333zl58iQxMTFotVoAJk+ezJo1a/j1118ZNGgQ06ZNo1+/fgwYMACACRMmsH379gdGme5KSkpi+vTpzJgxg969ewNQokQJ6tevD2B6bjc3N7y87n1z9OmnnzJlyhQ6deoEGEeiTp8+zdy5c+nduzdLly5Fr9czf/58bG1tKVeuHFevXmXIkCHZvgcpKSlkZGTg4OCQpT0tLS3LyFZ2pk+fTsuWLfnggw8ACAkJYf/+/Wy+b37z5MmT6dOnD0OHDgVgxIgRHDx4kMmTJ9OkSRPT63V2ds7yeoUQQojnlqLA0QWw5d+iDtbO8NI0KNfxMZcppJ08SeKGDVh6euHWry8A2jJlUFlZofHzxa52HWxr1cS2ShUsixXL/9eSDbVWi3VoKNahoVlGrQA03t7Y1qlN6rHj6KKjub18ObeXL0dlbY19o0Z4/m80Gs9CNq3NsxwM3AE7P4d90+GvJRC+BzrMgcB65o4uzxSahCkyMpLhw4ezdetWrK2tc3TN6NGjGTFihOk4MTEx5+tXNLbGkR5z0Njmqvv69euxt7dHp9ORmZlJ+/bt+fbbb03nAwICTB/gAY4ePcqdO3dwc3PLcp/U1FQuXboEwJkzZxg8eHCW83Xq1GHnzp0PjeHMmTOkp6dnSdQeJzY2lsjISPr378/AgQNN7TqdzrQ+6syZM1SqVAlb23vvSZ06dR55X1tbW2xtbR8oMNGlS5cs98nOmTNn6Ngx61/ederUyZIwnTlzhkGDBmXpU69ePaZPn/7Y+wshhBDPnTuxsPYN4/QtMG5A22E2OGY/mycjMpKE1atJWL+BzCtXALAKDjYlTGqtllJ7dmNRQNdU/5dj69Y4tm6NIT2dlEOHuLNzJ0m7dqG7foM7e/fi7TjR1DczOhpLd3dU6kKwesZSCy98AiEtYfXrcPsKLGwDDUdB49HGEuWFXKFJmI4ePUpMTAzVqlUzten1evbs2cOMGTNIT09/YNqTVqs1jaDkmkr1VNPinqUmTZowe/ZsNBoN3t7eDxR1sLPL+joMBgPFixdn165dD9zrSUtn2zzBULLh38WT8+bNo1atWlnO3f1/+SRzfIODg0lJScHPzy/L//+kpKRHTim8K6fPqfrPXhCKojzQJoQQQjz3Lm6H1YMhOda4r1LzcVBrsKmown8lbt1K/LKfsqxBUtnY4NC0KY5tXszy721hSZbup9ZqsW/QAPsGDfD8+GPSTp8m43KYaVqeoihEDhiAITkFp44dce7aBU1hmK0SUBcG74PNo40jTXu+Mhb16PxDoS8IUWgSpmbNmnHy5MksbX379qVMmTK8//77z/UaETs7O0qWzHnZzapVqxIVFYWlpWW2xQhCQ0M5ePAgvXr1MrUdvO8vrv8qVaoUNjY27NixwzSN73531yzp9XpTm6enJz4+Ply+fJkePXo89L5ly5Zl8eLFpKammpKyR8UBULFiRdzd3fnjjz9MI16XLl0iPDw8RyNgZcuWfeA5/nscGhrKH3/8keX92b9/P6GhoaZjjUaT5fUKIYQQzxWDHnZNhD2TAQU8y0OneeBZ9pGX3dm5y5gsqVTY1auHU/v2ODRrijoHs0QKG5VKhU25ctiUK2dq00VFkRkVjSEpibiZM4mbMweHZs1w6dED25o1CvaXs9aO0GGmcW3T2rcgfC/MqQ+dv4fgRuaO7okVmoTJwcGB8uWzlmW0s7PDzc3tgXbxaM2bN6dOnTp06NCBSZMmUbp0aa5fv87GjRvp0KED1atXZ/jw4fTu3Zvq1atTv359li5dyqlTp7IdobG2tub999/nvffew8rKinr16hEbG8upU6fo378/Hh4e2NjYsHnzZnx9fbG2tsbJyYlx48bx1ltv4ejoSOvWrUlPT+fIkSPEx8czYsQIunfvzocffkj//v356KOPCA8PZ/LkyY98fRYWFowePZqhQ4eycOFCHB0dGTZsGL169crRlMy33nqLunXr8uWXX9KhQwe2bt2aZToewLvvvku3bt2oWrUqzZo1Y926daxatYrt27eb+gQGBrJjxw7q1auHVqvN36qOQgghREGSFA0r+xs/MAPUGGCsqqbJuqwi9eQ/3FowH7fXB2NdOgQAl+6vovH2xrlTRzQ+Ps86crPTFC9Oqb17SNq2ndsrVpBy+DBJW7eStHUr2pAQPN57D/v6BXx9UIUu4FURfukNMadhcQfj9LwGIwvnFD2lEGvUqJEyfPjwHPdPSEhQACUhIeGBc6mpqcrp06eV1NTUPIww//Xu3Vtp3759tufHjh2rVKpU6YH2xMRE5c0331S8vb0VjUaj+Pn5KT169FCuXLli6vPZZ58pxYoVU+zt7ZXevXsr7733XpZ7/fe59Xq9MmHCBCUgIEDRaDSKv7+/8vnnn5vOz5s3T/Hz81PUarXSqFEjU/vSpUuVypUrK1ZWVoqLi4vSsGFDZdWqVabzBw4cUCpVqqRYWVkplStXVlauXKkAyvHjx7N93QaDQRk3bpzi7u6uODg4KP3791eSk5Oz7f9fP/zwg+Lr66vY2NgoL730kjJ58mTFyckpS59Zs2YpwcHBikajUUJCQpRFixZlOb927VqlZMmSiqWlpRIQEPDQ5ymsv3dCCCFEtsL2KspXpRRlrKOiTCiuKCd+yXLaYDAod/btU8L79FFOly6jnC5dRrn+0cdmCrbgSz13Trk+dqxypkpV5XTpMkrSrl3mDinn0pMVZc0w4+/CWEdF+bG9oiTFmDsqk0flBvdTKUphLgSfO4mJiTg5OZGQkICjY9Zyh2lpaYSFhREUFJTjohJCPC35vRNCCFFkGAywb6pxfyXFAO6hxn2V3I0jR4qikLxnD7EzZpJ2d5mFhQVObdvg2rcv1mXKmDH4gk+fmEjihg04v/KKaVrercVLMKSm4tK9Oxb2BXjt/V8/wYYRxuqIDsWN65oKQBW9R+UG9ys0U/KEEEIIIUQBlXLLWCHtwlbjcaVXoc0UUwEtRVGIHDiI5D/+AEBlbY1z16649en9XE67exIWjo64vPqq6Vh/5w6xM2ZgSEjg1vz5uPbrh2vPHgVzrVflV8G7CqzoBXHnYO9kY5GIgrwe6z6FoFahEHmrdevW2NvbP/Tx+eefmzs8IYQQonCJPg3zmhiTJUtraPetsWT4fdWGVSoVNlWroLK2xrVfP0ru2I7Xh/+TZOkpqK2t8frwf1gFBKC/fZvYr7/mUstWxP+8HCUz09zhPcijDAzaaayQ2GFOoUmWAGRK3r9katTz49q1a6Smpj70nKurK67/2cU7P8nvnRBCiELt9FpjyfDMZHAOgJeXQPGKZF67Rsy06Th37IBd3boA6O8ko6SlmnVj2aJI0elI3LCB2G++JfPaNQCsAgIo/tkEbKtXN3N0BZtMyRMiGz7ybZYQQgjxdAwG2P0F7J5kPA5qCF1/RK/XcHPK19z68UeUjAzSL10kaOVKVCqVcY1NQV5nU0ipLC2Npddbt+b2z8uJmzOHjKtXC+UeVQWVJExCCCGEECLn0pNg1etwboPxuPZQlMZjiF+5irgZM9HHxwNgW6MGHu+/X7D3DSpC1FZWuPZ6DadOnUg5chhtqVKmc0nbt2NbuzYW9vZmjLDwkoRJCCGEEELkzM1L8HMPiD0DFlbQdhqp6vLcePlV0s+dA8AqKAiPd9/FvkljSZbMwMLeDofGjU3HaefOc/Wt4Vi4ueIxciRO7dqhUksZg9yQd0sIIYQQQjzepZ0wr6kxWbL3gr6boEoPMq5Ekn7uHBZOTniO+Zjgtb/h0LSJJEsFhCE5GY2fL/rYOG58MJqI7j1IO3PG3GEVKjLCJIQQQgghHu3oQlg/AhQ9ik91dA0no/GtAoBj2zboYmJw6tQRSxcX88YpHmBbtQrB69Zx68cfiZs9h9S//iKsazfcBvSn2NChqK2szB1igScjTEIIIYQQ4uEMBtj6MawbDoqedO+XuPKHL2H93kSfkAAYS4a79e8nyVIBprayotjAgZTYtBGHFi1Ap+PmnLlE9HwNxWAwd3gFniRMIs/MmTMHf39/7Ozs6Ny5M3FxceYOSQghhBBPKiMFVrwG+79BMUBcShvCvj1Byp+HMNy5Q+qJE+aOUOSSxtMT32+m4zN9OhZubjh1aC/rmXJA3qFCTKVSPfLRp0+fZxbLmjVrePfdd/n22285cuQIiYmJdO3a9Zk9vxBCCCHyUFI0LGwDZ9eTfseG8KM1iF17HCUzE7uGDQhevw77Bg3MHaV4Qo4tW1Bi4wZcXnnF1JZy9CjJhw6ZMaqCS9YwFWI3btww/bx8+XLGjBnDuX8r1ADY2Nhk6Z+ZmYlGo8mXWCZMmMCwYcNo3749AD/++CN+fn7s27ePevXq5ctzCiGEECIfRJ+CZS+j3I7k1iV3Yv+2Qcm8htrREa+PPsTxpZekoEMRcP8+TYbkZK6//wGZV6/i8tpreIwaiVqrNWN0BYuMMD1GSmZKto90fXqO+6bp0nLUNze8vLxMDycnJ1Qqlek4LS0NZ2dnVqxYQePGjbG2tmbJkiWMGzeOypUrZ7nPtGnTCAwMzNK2YMECQkNDsba2pkyZMsyaNSvbOOLj4zl69Cgvvviiqc3b25vy5cuzbdu2XL0mIYQQQpjRxe3wQ0tIiERVrCRpbi1QMnXGUaV1a40lqSVZKnIUBezq1AEgfvFiwrt0Je38eTNHVXDICNNj1FpWK9tzDXwaMKv5vUSi8YrGpOpSH9q3umd1FrRaYDputbIV8enxD/Q72fvkU0T7oPfff58pU6awYMECtFot33333WOvmTdvHmPHjmXGjBlUqVKF48ePM3DgQOzs7Ojdu/cD/S9fvgxAqfs2SLt7fPecEEIIIQq4v5ah/PYmhgw9FqUaQLdFeGVaYNd4p3GtiyRKRZaFvR3FPx2PQ4sXuD76f6RfuEB4l654vPsuLj17PPf/7yVhKuLefvttOnXqlKtrPv30U6ZMmWK6LigoiNOnTzN37tyHJkwpKcaRsf8mTOnp6aYpekIIIYQooBQF/vga3foJXD/ojMq5OL7jVqLSaLEAnDt2MHeE4hmxb9CA4N/WcP1//yN59x6iP/uMO3/sxXfqVNS2tuYOz2wkYXqMP7v/me05C7VFluNd3XZl21etyjr7cXPnzU8VV05Vr149V/1jY2OJjIykf//+DBw40NSu0+lwum+u6/1s//0DtGvXLpydnU3tw4cPN50TQgghRAFk0MOm97mzdhHXD7qjT7dAlZBKRuQ1tMHB5o5OmIGlmxt+c+YQv2QpMV99hcpSg+o/6+KfN5IwPYatJucf+POr79Ows7PLcqxWq1EUJUtbZmam6WfDv7X4582bR61aWacjWlhkTRDvCv73L1RHR0dKlixpak9LSzOdE0IIIUQBk5mGsqI/Mb/s4dZZNwC0pUvj8/UUSZaecyqVCtfXemJbsyaWHu6mKXmG1FRUFhaonrPNbiVhes64u7sTFRWFoiimX/6//vrLdN7T0xMfHx8uX75Mjx49cnRPFxcXqlWrxt69ewkJCQHgzp07HDhwgPHjx+f5axBCCCHEU0qNJ2NWV66tDCPtlj0ALj164PHeu1IdTZhYlw4x/awoCjfGjCXz6lV8pn6NxsvLjJE9W5IwPWcaN25MbGwsX375JV26dGHz5s1s2rQJR0dHU59x48bx1ltv4ejoSOvWrUlPT+fIkSPEx8czYsSIh973o48+YvDgwfj5+REUFMSHH35IrVq1pKS4EEIIUdAkXEVZ3JmrK26SnmCFhYMdxb+YhEOzZuaOTBRgmdeuc2fXLgxJSYR16ozP5K+wq1vX3GE9E1JW/DkTGhrKrFmzmDlzJpUqVeLQoUOMGjUqS58BAwbw/fffs3DhQipUqECjRo1YuHAhQUFB2d63Q4cOjBs3jv79+1OpUiV0Oh0rVqzI75cjhBBCiNyIPQ8/tEAVdxavRpbYVqtA0Lr1kiyJx7Ly9SFo5a9oQ0PR37rFlf4DiJs9G+Xf5RxFmUr574KWIiwxMREnJycSEhKyjKiAcb1NWFgYQUFBWFtbmylC8byR3zshhBDPSsbhzaT/+AYOxWKhWAj0XIXi5Pvcl4wWuWNISyP6s8+4/cuvANg1aojPpElY3Ff4q7B4VG5wvycaYYqMjGTv3r1s2bKFY8eOkZ6e/viLhBBCCCGEWST9PIOw/m9zbaclaZbloe9mcPaTZEnkmtramuKffkrxzz9HpdWSvHsPV15//YGiYkVJjtcwRUREMGfOHH766SciIyOzvClWVlY0aNCAQYMG0blzZ9RqmeknhBBCCGFuil5P7Njh3Px1B6DCxluLRa8fwc7N3KGJQs65U0esy4Zy7Z0ReLwzokgn3znKbIYPH06FChW4cOEC48eP59SpUyQkJJCRkUFUVBQbN26kfv36fPzxx1SsWJHDhw/nd9xCCCGEEOIRdPHxRL7y0r/JErjUdCdg/V40/iUfc6UQOWNdpgzB69ZiV/veVjRp586j6HRmjCrv5WiEycrKikuXLuHu7v7AOQ8PD5o2bUrTpk0ZO3YsGzduJCIigho1auR5sEIIIYQQ4vFST5zg6uD+6G7dQWVhoHjX8jh9vBwspECyyFsqy3u/U+mXLhHRvTs2lSrhM/VrLJyczBhZ3snRn5qvvvoqxzd88cUXnzgYIYQQQgjxlBSFpPnj0d26g5WDDp+hLbHu8y0U4SlTomDIiIxEMRhI3r+fsG7d8Js5E23Jwj+imevFRqmpqaSkpJiOIyIimDZtGlu2bMnTwIQQQgghRC4pCmwejbvjNtwrJhI4oZ8kS+KZcWjcmMCflqHx9iYz4grh3V4m6fffzR3WU8t1wtS+fXsWLVoEwO3bt6lVqxZTpkyhQ4cOzJ49O88DFEIIIYQQj6ZPTCT6yy8xrH4T/pyNSg3FRo3DouVoSZbEM2VdpgyBv/6CbY0aGFJSuDrsDeLmzCnUVfRynTAdO3aMBg0aAPDrr7/i6elJREQEixYt4ptvvsnzAIUQQgghRPbSL4cR3u1lbs1fQPQPawEVtJsBNQeaOzTxnLJ0dcV//g+4dH8VFIXYadNJWLXK3GE9sVyv/EtJScHBwQGArVu30qlTJ9RqNbVr1yYiIiLPAxRCCCGEEA93Z+8fXBsxAkNSEpa2OlxKpkHn76FCF3OHJp5zKo0GrzFj0IaEkLRtO04vvWTukJ5YrkeYSpYsyZo1a4iMjGTLli20aNECgJiYmEfukCuKvjlz5uDv74+dnR2dO3cmLi7uie+1a9cuVCoVt2/fzrsAhRBCiCJCURRuLlhI5OuvY0hKwqZYOkEtb2M9+AdJlkSB4vLKK/jN+w6VlRVg3BtMycw0c1S5k+uEacyYMYwaNYrAwEBq1apFnTp1AONoU5UqVfI8QJE9lUr1yEefPn2eWSxr1qzh3Xff5dtvv+XIkSMkJibStWvXZ/b8AIGBgUybNu2ZPqcQQgjxrCk6HVHjPiFm0iQwGHAKTsb/hWQs+y2D0ML7Lb4oulTqeymHISkJlUZjxmhyL9dT8rp06UL9+vW5ceMGlSpVMrU3a9aMjh075mlw4tFu3Lhh+nn58uWMGTOGc+fOmdpsbGyy9M/MzESTT7+gEyZMYNiwYbRv3x6AH3/8ET8/P/bt20e9evXy5TmFEEKI51FmVBSJmzYB4FElAddyoOq+AoIbmTkyIR5N0elQF8IZaTkeYfL29mbIkCFs2rQJV1dXqlSpgvq+bLFmzZqUKVMmX4I0J0NKSvaP9PSc901Ly1Hf3PDy8jI9nJycUKlUpuO0tDScnZ1ZsWIFjRs3xtramiVLljBu3DgqV66c5T7Tpk0jMDAwS9uCBQsIDQ3F2tqaMmXKMGvWrGzjiI+P5+jRo1n24PL29qZ8+fJs27YtR69l48aNhISEYGNjQ5MmTQgPD3+gz8qVKylXrhxarZbAwECmTJliOte4cWMiIiJ45513TCNsQgghRFFk5WaHbysNvvVv4VbRAlWv1ZIsiUJBZWmZZbSpsMjxCNOyZctYt24db731FtHR0bRs2ZJ27drRpk0bXF1d8zNGszpXtVq25+waNcR/7lzT8fl69VFSUx/a17ZGDQIWLzIdX2zWHH18/AP9Qs+eeYpoH/T+++8zZcoUFixYgFar5bvvvnvsNfPmzWPs2LHMmDGDKlWqcPz4cQYOHIidnR29e/d+oP/ly5cBKFWqVJb2UqVKmc49SmRkJJ06dWLw4MEMGTKEI0eOMHLkyCx9jh49Srdu3Rg3bhwvv/wy+/fvZ+jQobi5udGnTx9WrVpFpUqVGDRoEAMHSlUgIYQQRUvqyX8wJN/BrkJJWNwBO/VJKOUCr60Gb1kSIUR+ynHC1LhxYxo3bsyUKVM4deoUa9euZebMmQwYMIA6derQvn172rVrR4kSJfIzXpFLb7/9Np06dcrVNZ9++ilTpkwxXRcUFMTp06eZO3fuQxOmuxsZ/zdhSk9PN03Re5TZs2cTHBzM1KlTUalUlC5dmpMnTzJp0iRTn6+//ppmzZrx8ccfAxASEsLp06f56quv6NOnD66urlhYWODg4ICXl1euXq8QQghRkCVu28b1d99DZWFBYAfQ6i+AnTu8tga8yps7PCGKvFyvYQIoV64c5cqVY/To0URHR7N27VrWrl3Lhx9+SHBwMJMmTaJNmzZ5HatZlD52NPuTFhZZDkP2/ZF93/8MP5bcsf1pwsqx6tWr56p/bGwskZGR9O/fP8tIjU6nw8nJ6aHX2NraAsbKds7Ozqb24cOHm849ypkzZ6hdu3aWaXR3i4nc3+e/yVe9evWYNm0aer0ei//8vxBCCCEKO0VRuLXwR2K+/BIUBVt/CyzTroJrcei1FtxDzB2iEM+FJ0qY7ufp6cnAgQMZOHAgycnJbN26Fa1WmxexFQjqHHzgz+++T8POzi7r86rVD+y0nHlfaUeDwQAYp+XVqlUrS7/skpLg4GAAHB0dKVmypKk9LS3NdO5RcrLzs6IoD6xLKsw7RgshhBCPohgMRH/xBfGLFgPgXN4Sr7JXUDn7Qp914Pr4f1+FEHkjRwlTYmJijm8olfIKNnd3d6KiorIkIH/99ZfpvKenJz4+Ply+fJkePXrk6J4uLi5Uq1aNvXv3EhJi/Lbrzp07HDhwgPHjxz/2+rJly7JmzZosbQcPHnygzx9/ZB3B279/PyEhIaZEzsrKCr1en6OYhRBCiILKkJ7O9fc/IGnzZgA86lji6n8FlbMf9FkPLoHmDVCI50yOEiZnZ+ccVx2TD6wFW+PGjYmNjeXLL7+kS5cubN68mU2bNmXZdHjcuHG89dZbODo60rp1a9LT0zly5Ajx8fGMGDHioff96KOPGDx4MH5+fgQFBfHhhx9Sq1atHJUUHzx4MFOmTGHEiBG8/vrrHD16lIULF2bpM3LkSGrUqMGnn37Kyy+/zIEDB5gxY0aW6n2BgYHs2bOHV155Ba1WS7FixZ7sTRJCCCHM6NbCH43JkqUl3k1UOLlFgJO/cWRJkiUhnrkc1fXbuXMnv//+O7///jvz58/Hw8OD9957j9WrV7N69Wree+89PD09mT9/fn7HK55SaGgos2bNYubMmVSqVIlDhw4xatSoLH0GDBjA999/z8KFC6lQoQKNGjVi4cKFBAUFZXvfDh06MG7cOPr370+lSpXQ6XSsWLEiRzH5+/uzcuVK1q1bR6VKlZgzZw6ff/55lj5Vq1ZlxYoV/Pzzz5QvX54xY8Ywfvz4LJvzjh8/nvDwcEqUKIG7u3vO3xQhhBCiAHHr2weHxvXxb/1vsuTsLyNLQpiRSsnlQpBmzZoxYMAAXn311Szty5Yt47vvvmPXrl15GV+eSkxMxMnJiYSEhCwjKmBcbxMWFkZQUBDW1tZmilA8b+T3TgghBEDG1atovL2Ne9QkXIWFbSE+7N9kaYPxv0KIPPWo3OB+ud456sCBAw+tvFa9enUOHTqU29sJIYQQQjzXkg8dIqxjJ6K/+AIl/gosbPNvshQAfTZKsiSEmeU6YfLz82POnDkPtM+dOxc/P788CUoULYMHD8be3v6hj8GDB5s7PCGEEMJskrZvJ3LAQAxJSaT9dQzlhzYQH26cftdnAzjLZyshzC3XZcWnTp1K586d2bJlC7Vr1waMFc0uXbrEypUr8zxAUfiNHz/+gXVSdz1q+FMIIYQoym6vXMWNjz8GgwH7RnXxKXUI9Z0r4BJkXLPk5GvuEIUQPEHC9OKLL3LhwgVmzZrF2bNnURSF9u3bmyqkCfFfHh4eeHh4mDsMIYQQosC4uWAhMZMmAeDUtgXFvXegSoz8N1naAE4+Zo5QCHHXE21c6+vr+0AVs6JCNkMVz5L8vgkhxPMn9tsZxM2cCYBrjy542K9GlRBp3Iy2zwZw9DZzhEKI+z1RwgSQkpLClStXyMjIyNJesWLFpw7KHDQaDWB8XTY2NmaORjwvUlJSgHu/f0IIIYo+bekQsLDA/fU+uBkWobodCa4ljNPwJFkSosDJdcIUGxtL37592bRp00PPF9aNay0sLHB2diYmJgYAW1vbHG/WK0RuKYpCSkoKMTExODs7Y2FhYe6QhBBCPCOOLVqg/Xk+2p2D4XaEscBD73WSLAlRQOU6YXr77beJj4/n4MGDNGnShNWrVxMdHc2ECROYMmVKfsT4zHh5eQGYkiYh8puzs7Pp904IIUTRZEhNJWrCBNyHDUPj7Q13YtDuHga3LoGTvzFZkjVLQhRYuU6Yfv/9d3777Tdq1KiBWq0mICCAF154AUdHRyZOnEibNm3yI85nQqVSUbx4cTw8PMjMzDR3OKKI02g0MrIkhBBFnD4hgcjBQ0g9fpy0M2cI+nEuqsXtIe48OPpAn3Wyz5IQBVyuE6bk5GRTxTNXV1diY2MJCQmhQoUKHDt2LM8DNAcLCwv5ICuEEEKIp6KLjeXKgIGknzuH2tERr1FvoVraEWJOg72XcWTJJdDcYQohHiPXG9eWLl2ac+fOAVC5cmXmzp3LtWvXmDNnDsWLF8/zAIUQQgghCpuMq1cJ79GT9HPnsChWjIB5M7E9ORaiToKdhzFZcith7jCFEDnwRGuYbty4AcDYsWNp2bIlS5cuxcrKioULF+Z1fEIIIYQQhUr65TCu9O2LLjoaja8v/rOnY7XrTbjxF9i6Qe+14B5i7jCFEDmkUp5yI5iUlBTOnj2Lv78/xYoVy6u48kViYiJOTk4kJCTg6Oho7nCEEEIIUQRF9O1LyoGDWJUsgf/sb9FsHQyRB8HGxTiy5FXB3CEKIch5bpCrKXmZmZkEBwdz+vRpU5utrS1Vq1Yt8MmSEEIIIcSz4PPllzi0akXA93PRbH/DmCxZO8FrayRZEqIQylXCpNFoSE9Pl/2JhBBCCCHuo4uPN/1s6e6O71efY7l1KET8AVpH6LkavCubL0AhxBPLddGHN998k0mTJqHT6fIjHiGEEEKIQiXl8GEuvdCC26vXGBt06bC8J1zeBVb20ONX8K1mzhCFEE8h10Uf/vzzT3bs2MHWrVupUKECdnZ2Wc6vWrUqz4ITQgghhCjI7uzbx9Vhb6CkpZG4fj1ObVuj+qU3XNwOGlvovgL8a5k7TCHEU8h1wuTs7Eznzp3zIxYhhBBCiEIjaedOrr01HCUzE7tGDfH9ejKqlf3g/CawtIZXf4bAeuYOUwjxlHKdMC1YsCA/4hBCCCGEKDQSN2/h2qhRoNPh8EJzfL78EtX6IXB2PVhYwSvLILiRucMUQuSBXK9hEkIIIYR4niWsXcu1ESNAp8OxTRt8Jk9GtfkdOLUa1Bp4eQmUbGbuMIUQeSRHCVOrVq3Yv3//Y/slJSUxadIkZs6c+dSBCSGEEEIUROmXLoPBgFOnTnhP+gLVttHw90+gsoCuCyGkpblDFELkoRxNyevatSvdunXDwcGBdu3aUb16dby9vbG2tiY+Pp7Tp0/zxx9/sHHjRtq2bctXX32V33ELIYQQQpiF+9vDsS5bFofmzVDtGAdHfgBU0HEuhLY1d3hCiDymUhRFyUnHjIwMfv31V5YvX87evXu5ffu28QYqFWXLlqVly5YMHDiQ0qVL52e8TyWnu/kKIYQQQtwvaft27OrXR21tfa9x95ew8zPjzy99A9V6myc4IcQTyWlukOOE6b8SEhJITU3Fzc0NjUbzxIE+S5IwCSGEECK3bi1eQvRnn2FXvz5+s2eh0mhg/wzY+qGxQ8uJUGeoeYMUQuRaTnODXFfJu8vJyQknJ6cnvVwIIYQQosC7tWgR0Z9PBMA6NBQsLeHIgnvJUpOPJFkSoogrNFXyJk6cSI0aNXBwcMDDw4MOHTpw7tw5c4clhBBCiCLq5sKFpmTJ7fXXcR/xDqoTK2D9O8YO9d6GhqPMF6AQ4pkoNAnT7t27GTZsGAcPHmTbtm3odDpatGhBcnKyuUMTQgghRBFzc8FCYr6YBIDb4Ndxf3s4qjPrYM0QQIEaA6H5OFCpzBqnECL/PfEaJnOLjY3Fw8OD3bt307BhwxxdI2uYhBBCCPE4txYtJvrzzwEoNnQIxd58E9XFHfDTK2DIhMo9oN0MUBea752FEA+R72uYzC0hIQEAV1fXbPukp6eTnp5uOk5MTMz3uIQQQghRuNlUqoja3h7X3r1xf/MNCP8DlvcwJkvlOkK7byVZEuI5kus/7X369GHPnj35EUuOKYrCiBEjqF+/PuXLl8+238SJE03FKZycnPDz83uGUQohhBCiMLKpVIng9euMydLVI7DsZdClQUgr6PgdqC3MHaIQ4hnKdcKUlJREixYtKFWqFJ9//jnXrl3Lj7ge6Y033uDEiRP89NNPj+w3evRoEhISTI/IyMhnFKEQQgghCpOb8xeQevIf07HGywuiTsKSTpBxB4IaQtcfwdLKjFEKIcwh1wnTypUruXbtGm+88Qa//PILgYGBtG7dml9//ZXMzMz8iDGLN998k7Vr17Jz5058fX0f2Ver1eLo6JjlIYQQQghxv7g5c4j58kuu9O+PLjbW2Bh7HhZ1gLQE8KsFr/wEGutH3kcIUTQ90QRcNzc3hg8fzvHjxzl06BAlS5bktddew9vbm3feeYcLFy7kdZwoisIbb7zBqlWr+P333wkKCsrz5xBCCCHE8yVu7nfETpsOgFv//li6u0N8OCxqDylxULwSdF8BWnvzBiqEMJunWrF448YNtm7dytatW7GwsODFF1/k1KlTlC1blqlTp+ZVjAAMGzaMJUuWsGzZMhwcHIiKiiIqKorU1NQ8fR4hhBBCPB9u/jCf2H8/r7iPGEGx1wdB4nX4sR0kXQf3MtBzNdg4mzdQIYRZ5bqseGZmJmvXrmXBggVs3bqVihUrMmDAAHr06IGDgwMAP//8M0OGDCE+Pj7vAs1mn4MFCxbQp0+fHN1DyooLIYQQAuDWjz8SPfELAIq99SbuQ4fCnVhY+CLEnQeXIOi7CRyLmzlSIUR+ybey4sWLF8dgMPDqq69y6NAhKleu/ECfli1b4uzsnNtbP1Ih3S5KCCGEEAVM4tat95KloUONyVJqPCzuaEyWHH2h91pJloQQwBMkTFOnTqVr165YW2e/8NHFxYWwsLCnCkwIIYQQIj/YN2iAXd06WJevQLE334D0JFjSBaJPgp0H9PoNnP3NHaYQooDI9RqmnTt3PrQaXnJyMv369cuToIQQQggh8ovaxga/OXNwf+dtVLo0+OlVuHYEbFyg1xooVtLcIQohCpBcJ0w//vjjQwstpKamsmjRojwJSgghhBAiL91evYaY6dNNU/xVVlaoDDr4pQ+E7wUrB+i5CjzLmTdQIUSBk+MpeYmJiSiKgqIoJCUlZZmSp9fr2bhxIx4eHvkSpBBCCCHEk0pYu5Yb//sfKAo2FSrg0LQpGAywZgic3wyW1tB9OfhUNXeoQogCKMcJk7OzMyqVCpVKRUhIyAPnVSoVn3zySZ4GJ4QQQgjxNBI2bOD6B6NBUXB+5WXsmzQBRYGNo+DkL6C2hG6LILCeuUMVQhRQOU6Ydu7ciaIoNG3alJUrV+Lq6mo6Z2VlRUBAAN7e3vkSpBBCCCFEbiVu3sL1994HgwHnrl3wGjPGuE3JjvFw5AdABR3nQkhLc4cqhCjAcpwwNWrUCICwsDD8/f2z3RdJCCGEEMLckrZv59qoUaDX49SxI16ffIJKrYZ938DeKcZObb+GCl3MG6gQosDLUcJ04sQJypcvj1qtJiEhgZMnT2bbt2LFinkWnBBCCCFEbmXeuMG1d0aATodju5coPuFTY7J0dCFs+9jYqfk4qC7VfYUQj6dScrAjrFqtJioqCg8PD9RqNSqV6qEbyapUKvR6fb4EmhdyuptvfsvQGRi//hR/pywAVKhUalSK2vgzakCNvYUXQdrGWKhVaCxUXM08gIVKjZXaCmtLaxys7HDU2uOodcBZ64izjT02GgtsrCxwtNbgaKPBQWuJWi0jgUIIIZ4/8b/8Qsqfh/D+YiIqS0v4ZxX82g9QoN7b8IKsuxbieZfT3CBHI0xhYWG4u7ubfhZPR29QWHIwAofQbdn20cWH8EdkkOnYvvQsVOqMh98vJYCUiCGmYxu/+aDSo+ht0WCPlcoBGwsH7CydcNMWJ9ChHG52VrjZWeFqrzX+bG+Fl6M1TjYamW4phBCiUFIUxfRvmEvXrjh36WI8vrAdVg0CFKjW1zi6JIQQOZSjhCkgIOChP4snY2mh4q2mpTie1BVFZQBFQcEAGFAwoKDg7OxLSEgpdAYDOr3CztvlyVTS0CkZ6Axp//6cip5UHKzs8fdyIDVTT3K6njTbMFTqe5sLZ/z7SAAiU/zYf3qY6ZxNwBxUKj0GnQNKpjMWBhecrTwoZu2Jr4MPAc5eFHeyxtPRmuJO1ng5WVPMTisjV0IIIQqU5D8PEfP1FPxmzcLSzQ0wznwh4gAs7wmGTCjXCdpMAfliUAiRCzmakne/iRMn4unpSb9+Wef9zp8/n9jYWN5///08DTAvFZQpeXlJURR0ig6NWmM63nd9H3Ept4hOjic2+Ra3Um8Tnx5PfFo8TpZ+VLbtw63kDOLupLFPNwhFlfnQe+tTfUkJf8N0bFVsO4pBi4XeHS8bP4Jd/Ah0dSKwmC3+rrYEutnh42KDxiLX+yELIYQQTyzl+HGu9B+AkpKCS6/X8Prf/4wnbvwNC9tCeiKUagEvLwVLK/MGK4QoMHKaG+Q6YQoMDGTZsmXUrVs3S/uff/7JK6+8UqCn7BXFhOlpKIrCybiTxKXGEZMSw9WkG4Tfvsr1OzeISY3GXRNCJe0bRCWkcT0xhTC7N0Glv+96FUqmC4YMN/QpJci4aVxz5eNsQ4CbMYEq4W5HiKcDIV4OFLPXmvHVCiGEKIpS/znFlT59MNy5g13duvjOnoVaq4W4CzC/FaTEgX9d6LkSrGzNHa4QogDJ0zVM94uKiqJ48eIPtLu7u3Pjxo3c3k6YkUqloqJ79lUN758LnqZLY+6JvoQnRHD5djjX7kSSbkhDZXULtdUt7K3sSExQk64zcOVWMjddxnDkhjOGMC8M6caHo4UfIe7ulPZyoJSnAyEe9oR4OuBiJ9/2CSGEyL208+eJ7N8fw5072Favju/MGcZk6XYkLOpgTJaKV4LuP0uyJIR4YrlOmPz8/Ni3bx9BQUFZ2vft2ycb1xYx9xd/sLa0ZnjV4aZjRVGIS40jIjGCyKRIPGw9qFO8LjFJ6Ry5dpHRh2+jtroNtuGma3TAP5lOHL9Qg4wDzU3txRysKOPpSDkfR8p7O1HBx4kAN1spPiGEECJb6ZfDuNKvP/qEBKwrVcR3zhzUNjZwJxYWd4DEq+BWCnquAmsnc4crhCjEcp0wDRgwgLfffpvMzEyaNm0KwI4dO3jvvfcYOXJkngcoCiaVSoW7rTvutu5U96puavdysqalQ2lKef3K+fjzXLh9gQvxFzgff4GYlGjUmgSqBNhhV8yD89FJXEuMI9VrPEfTfDl02hf9UV8Mab7YW7pSztuRCj5OlP/3EeRmJ8UmhBBCoCgKNz78EH1cHNqyofjPm4eFvR2k3oYlHeHmRXDyg15rwK6YucMVQhRyuV7DpCgKH3zwAd988w0ZGcYy19bW1rz//vuMGTMmX4LMK7KGybwS0hO4ePsibtZuBDoFArA9fDfv7H7jgb6GTEf0ab5kxtdCn1waADsrC8p5O1HJz4lqAS5UDXDBw8H6Wb4EIYQQBUTmtWtETfiM4p9/hqWLC2SkwJJOcOUA2LlDvy3gVsLcYQohCrB8K/pw1507dzhz5gw2NjaUKlUKrbbgL+iXhKngydBncD7+PP/E/cOpm6f4J+4fLidcxqAYAKhiM4ikuKqcuZFIhioKjfOf6FOD0KcEoujt8XO1oZq/iymBKuPliIWMQgkhRJGk6PWoLCwePKHLgJ9fhYvbQesEfTeAV4VnH6AQolDJ94QJ4OrVq6hUKnx8fJ70Fs+UJEyFQ0pmCmduneGfuH9o5t8MXwdfdHoD3x5ZyPyzU039DOnF0KUGok/5N4HKdMXOypLK/s5U8zcmUNUDXbHX5nrmqRBCiAJGFx/Plf79cR86FIfm99bBYtDDr/3g9BrQ2MJrq8G/ttniFEIUHvmWMBkMBiZMmMCUKVO4c+cOAA4ODowcOZIPP/wQtbrg7sEjCVPhdjzmOBsub+Bo9FEu3r74YIfrg0lKCPz3QA9YYKFWUcnXiTol3KhbohjVAlyw1jzk20khhBAFlj4xkYg+fUg/fQaNtzfBmzYaq+EpCqx7C44tArXGWA2vZPPH31AIIcjHsuIffvghP/zwA1988QX16tUzbpS6bx/jxo0jLS2Nzz777KkCFyI7VTyqUMWjCmBcD/VXzF8cjTnKsehjnI8/z+8j+nA1Xs/RiHh+ujiLyLQjpCWW4MStkhy7WoKZO62xslBTxd+ZuiWKUaeEG5X9nLGyLLhJvhBCPO/0d5KJHDiI9NNnsHB1xe/7efeSpW1jjMmSSg2dv5dkSQiRL3I9wuTt7c2cOXNo165dlvbffvuNoUOHcu3atTwNMC/JCFPRlanPRGOhMR13W9eNM7fOmI5VqFGl+5OaWAJ9cin0qf6AGhuNBdUDXahXshiNS7tT2tNBypkLIUQBYUhNJXLQ66QcPoyFkxP+i37EurSxEBB7p8CO8caf230LVXuZL1AhRKGUb1PyrK2tOXHiBCEhIVnaz507R+XKlUlNTX2yiJ8BSZieH4kZiRyOOsyB6wc4eOMgEYkRpnN2Fi5UVU3lz8u3uJmcAaoMUIyb5xZ3sqZxaXcal/agXslisv5JCCHMxJCRwdUhQ0netw+1vT3+CxZgU6G88eTh72HDv1uZtPgM6j5YbVUIIR4n3xKmWrVqUatWLb755pss7W+++SaHDx/m4MGDTxbxMyAJ0/Pr+p3rHLxxkAPXD+Bm48YHNT9AURTORiXSd0c70DmReKsUaQmlMaR7Ayo0FipqBLrSpLQHTcq4U8LdXkafhBDiGbm1ZCnREyagsrXF//vvsa1qnJLNiV9g1UBAgYbvQtOPzBqnEKLwyreEaffu3bRp0wZ/f3/q1KmDSqVi//79REZGsnHjRho0aPDUwecXSZjEf12Mv0jHtR2ztNmoXDGkhBIfVxJ9cgnT6JOPsw1NyrjTLNSTeiWKydonIYTIR4rBQPQXX+DQtCl2tf+tend+C/zcHQw6qDkIWn8J8kWWEOIJ5WtZ8evXrzNz5kzOnj2LoiiULVuWoUOH4u3t/VRB5zdJmMTDxKbEsvfaXnZH7ubAjQOk6u5NK63m3BHDzbYcvHyTDJ3B1O6gtaRZqAetynvRMMQdWyuZuieEEE9LMRhAUR6+11LEfljcEXRpUKEbdJwLBbgyrxCi4Hsm+zAVNpIwicdJ16dzOOowuyN3s/vqbj6r/xk1vGqQkqFj8fEd/HxhIQlxZYiPLY2idwDAWqOmUYg7rcp70bSMJ042msc8ixBCiP9SFIWocZ9gSErEe9IkVJr7/i6NOgkL2kB6ApRqCa8sBQv5u1YI8XTyNGE6ceJEjp+4YsWKOe77rEnCJHLj7h+Nu+uWPj3wKSvOrzC2oaKYpgx3boYSG10aRecEgKVaRd2SxWhVzosXynri7qA1T/BCCFGIKIpCzBdfcOvHRaBS4b9wIXa1ahpP3roMP7SE5BjwrwM9V4GVrXkDFkIUCXmaMKnValQqFY/rqlKp0Ov1uY/2GZGESTyNyKRItkVsY1v4Nv65+U+Wc+6aENQx/bgYda9NrYK6JYrRrrI3rcp74Wgt34YKIcTDxEybxs05cwEo/tlnOHfuZDyRFAU/tIDbEeBZHvpsABtn8wUqhChS8jRhioiIeFwXk4CAgBz3fdYkYRJ55fqd62yP2M72K9v5K+YvvO292dRpE5fjktlyKoo1Z/Zw4ao9it4OACtLNU1Le9C+sjdNynhgrXnI/HwhhHgOxc2bR+yUrwHwHPMxrt27G0+kxhun4cWcApcg6LcFHDzNGKkQoqiRNUwPIQmTyA+xKbFcu3ONyh6VAeMmuo1XNCY5MxkfbWUSYstz9VowKMbpeQ5aS1qV96J9ZR/qlHDDQi0VnoQQz6dby5YRPf5TADxGjcRtwADjiYwUWNwBIv8Eey/ovwVcAs0WpxCiaMrXhGnx4sXMmTOHsLAwDhw4QEBAANOmTSMoKIj27ds/VeD5SRIm8SxcTbrKyN0jOX3ztKnNSq2luKYaMdfLEhMTCBir6rk7aGlbsTgdKvtQ0ddJ9nkSQjw3MmNiuNSiJUpaGm5DBuMxfLjxhC7DWDr84jawdoK+m8CznHmDFUIUSTnNDXJdj3P27NmMGDGCF198kdu3b5vWLDk7OzNt2rQnDliIosLXwZflbZeztsNahlQagr+DPxmGdCLS95Pq9j09Wl6kRy1/nG01xCals2BfOO1n7qPVtL18v/cyN++km/slCCFEvtN4eOA3Zw5uAwfg/tZbxkaDAdYMMSZLljbQ/RdJloQQZpfrEaayZcvy+eef06FDBxwcHPj7778JDg7mn3/+oXHjxsTFxeVXrE9NRpiEOSiKwqmbp9hweQObwzfzfYvvKeFcggydgYXHfmf9+f2cvRBCRrqxTLnGQkWzMp50q+FLw1LuWFrIPiNCiKJD0elQWT5k7zpFgU3vwaHvQG0Jr/4MpV549gEKIZ4bOc0Ncr3bZlhYGFWqVHmgXavVkpycnNvbCVHkqVQqyhcrT/li5RlVfRQWamPBBytLNedTNxOm34JNsJoytpW5c7MKlyMC2Xwqis2novBw0NK5mi9dq/kS7G5v5lcihBBPJ+XoUa6P/h9+M2egLVUq68ndk4zJEirjprSSLAkhCohcf3UdFBTEX3/99UD7pk2bKFu2bF7EJESRdTdZuquRbyOqelTFgIGwlGPE2vyAV/kvqVx5B87ON4hJSmP2rks0nbKbrnP2s+JIJCkZOjNFL4QQTy71n1NEvj6YzCtXiPtuXtaTf34HuyYaf279JVTo8uwDFELku5TMFJIykswdRq7leoTp3XffZdiwYaSlpaEoCocOHeKnn35i4sSJfP/99/kRoxBF1kslXuKlEi9xJfEKv136jbWX1hKVHMUl3TZKlAljUPBMfjl6ld3nYzkcHs/h8Hg+XXeaTlV96Fk7gFKeDuZ+CUII8VjpFy8SOWAAhjt3sK1Rg+Kfjr938sQvsOld48+NR0OtQeYJUgiR73ps7EEz39b0Kd8Pe22u0xCzeaIqefPmzWPChAlERkYC4OPjw7hx4+jfv3+eB5iXZA2TKOgMioE/b/zJb5d+o5J7JV4t8yoA4TdvM3Lnx9y4Wp7rUd7cHRyuGeRKz9oBtCrnhZWlrHUSQhQ8GZGRRHTvgS42FusKFfBfMB8L+3+nGF/YBj+9AgYd1BxkHF2SaqFCFHqZ+kwO3DjA71d+58NaH2KptuTYlXjG7Z3MpYTLjKj4OQMaBJs7zGezD1NcXBwGgwEPD48nvcUzJQmTKKzWXFzDx/s+BsDd2gfb9LqcuVAGfaZxY9xi9lZ0re5H95r++LnamjNUIYQwyYyOJqJ7DzKvXUNbqhT+i37E0sXFePLKn7CoPehSoXwX6DQP1PLFjxCFld6g53D0YTaHbWZbxDYSMxIB6OwzhoP/eHI2KglUOlAseLFCcWb1qGbmiPMxYfrkk0/o2bMnJUqUeOognzVJmERhden2JX46+xPrL68nOdNYXMVSZYmvtgY3rlYiLtYfUKNSQeMQd3rWDqBxaQ/ZFFcIYVbXRr1L4vr1aAL8CVi8GM3dL1ijT8GC1pCWACVfgFeWgaWVeYMVQjyRq0lXWXR6EVvDt3Iz7aapXatyIi2+PCk3a2HI8EBrqealSt70rB1ApQKy92S+JUwVK1bk1KlT1KhRg549e/Lyyy/j7u7+1AE/C5IwicIuJTOFzeGb+fX8r5yMO2lqH11+EZv+ymDvhXtl/X2cbehVJ4BXavjjZKsxR7hCiOec/s4dosaOw2PEO2h8fIyNt8Jgfiu4EwV+teC1NWAlI+NCFCZ6g95UyOpC/AU6re0EgK2FA1bplbl+rQz6lCBATbC7HT1qBdClqm+B+zySr1PyTp06xdKlS/n555+5evUqzZs3p2fPnnTo0AFb24L7l54kTKIoOXvrLL+e/5Vbabf4uvHXAITFJTNu1xz+uuhIwu3igAobjQWdqvrQt14gJT2kSIQQIn8pej0qC4uHn0yKhvktIT4MPMpB3w1g4/JsAxRCPJHkzGS2R2xn3aV1uNu6M7GBsbLlzTvpvLPtc86EFSM2JgCwwFKtomU5L3rU9qdOsFuBGE16mGeyhglg3759LFu2jF9++YW0tDQSExOf5nb5ShImUdRFJUfRamUr9IoeL+uSpN2sRWRkGVCM3+g0KFWMfvWCaBTijlqm6wkh8pghPZ2rQ4ZiV68ubv8tBJV6Gxa2heiT4BwA/beCg5dZ4hRC5IzeoOfPqD9Zd2kdO67sIFWXCoCNpQ2zG6zjpz+jWPv3dTJ0BsC4prp7TX961A7A09HanKHnSL5tXPtfdnZ22NjYYGVlRVJS4aurLkRRYlAMtA1uy6awTUSlXQS7i3iVd8RRV5eLlyqw9wLsvRBHcDE7etcNpHM130JV1lMIUXApmZlcGzGS5P37Sf3rLxzbtEHj9W9ClJFirIYXfRLsPKDXGkmWhCjglp5ZyvyT84lJjTG1+Tv4U9q+CZfCQug864ipvYKPE33rBdKmYnG0ltmMMBdiTzTCFBYWxrJly1i6dCnnz5+nYcOGdO/ena5du+Lk5JQfceYJGWESz4v4tHhWX1zNinMruHbnGgAqVNS1H8G+k8VJSjNufuugtaRrdT/61guU6npCiCemGAxcf+99EtevR2Vlhd9332FXu5bxpD4Tfu4BF7aA1sk4Dc+rgnkDFkI8IFWXiqXKEo2FcVbK9ye/Z/qx6ThaOdLUryWqO9XZcsyK6MR0ACzVKl6sUJzedQOp6u9cYKfdPUq+TcmrU6cOhw4dokKFCvTo0YPu3bvjc3chZwEnCZN43ugNevZe28vPZ3/meMxxtnXdhoViy8pjV/n+4EGuxGrAYI1aBa0rFOf1hsFU9HU2d9hCiEJEURSixn3C7eXLwdIS32+/waFJE+NJgwHWDIYTy8HS2ljgIaCOWeMVQmR1+uZpVl1YxcbLG/mo9ke8GPwiAHGpcWy9dIBTF31ZdTSa1Ew9UPim3T1Kvk3Ja9KkCd9//z3lypV7qgCFEPnPQm1BY7/GNPZrzO202zhaGf8yeK12ABtvfkCqezgOmXUJu1yFDSdgw4kb1Apy5fVGwTQO8ZB1TkKIR1IUhZivJhuTJZUKny8n3UuWFAW2jDYmSyoL6LZIkiUhCoiE9AQ2XN7A6ourOXvrrKl9z7U9vBj8IkcjbjFvTwRbTqtRlOsAhBZ3ZED9INpWKprT7h7lqYs+FCYywiSEUXxaPL029SI8MRwAFWrc1VWJDK9ORnIAoKKUhz0DGwbTvrL3c/cXoxAiZ1KOHiWiR08Aik/4FOcuXe6d3P0V7Jxg/Lnjd1DpZTNEKIS4X6Y+k4/3f8y28G1kGDIA0Kg1NA9oTocSHbl9y5/v94Zz7Mpt0zWNS7szsEEwdUsU3Gp3T+qZVckrTCRhEuIeg2Jg37V9LDmzhP3X95vaXSyCuHX1Be7cDgbAw0FL33pBdK/lj5NNwdo/QQhhfrcWL0HR63Dr0+de4+HvYcNI48+tJkHtwWaJTQgBGfoMrCzubQzda1MvjsccJ8QlhE6lOtHUtxVbTyYxf18YETdTALCyUNOhijcDGgQT4ll0tySRhOkhJGES4uEuxl9kyZklrL+8nnR9OhPrTeHatRLM3xdmWtxpZ2XBKzX9GdAgiOJONmaOWAhhTorBgEqtfvjJf1bCr/0BBRq+B00/fKaxCSGMTt88zYpzK9h+ZTtrO6zF1doVgL9j/8ZCZYGXtiQ/Hohg0YEIElIzAXC21dCzVgC96gbg4VC41yflhCRMDyEJkxCPdivtFhsub6B7me5YqC3I0Bl4f/s37I84Q+y1WhgyPNFYqOhc1ZfBjUoQWMzO3CELIZ6xxE2buLVoMX5zZmPx38q4F7fDslfAkAnV+0ObKVDEpvAIUZCl6dLYHL6ZFedWcDLupKn949of0610NwCu3U5l3p7L/Hz4CmmZxv2TAtxs6V8/iC7VfLG1en62G8mXhEmn0/HZZ5/Rr18//Pz88iTQZ0kSJiFyJ1OfSYuVLYhLjQPATh9K7LU66JNLoVapaFvRm6FNSlDGS/48CfE8SNq1i6tvvAk6He5vD6fY4Pum2l09Aj++BJkpUK4TdP4e1LL+UYhnIS41jh9O/sBvl34jKcO4L6ql2pIX/F+gW+luVPOsxqXYO8zedZnf/rqGzmD8+F/Bx4khjUvQspwXFs9hoad8G2Gyt7fnn3/+ITAw8GljfOYkYRIidxRF4a/Yv1h8ejE7ruzAoBi/ibJRfLl1vS66xIqAJc1DPRjapCRV/V3MG7AQIt8kH/yTyEGDUDIycGzbFu9JX6Cy+Dchij0H81tCajyUaAqvLgdLq0ffUAiRZ26m3qT5r83RGXT42PvQJaQLHUt2xM3Gjb8jbzNr10W2no7m7qf+uiXcGNq4JPVKFr1CDrmRbwlThw4d6NChA33uX9xZSEjCJMSTu3bnGkvPLGXl+ZWk6IyLQr3VL3D+dLMsfwEPa1KySFbSEeJ5lvr331zp2w9DSgr2TZviO30aKs2/RWASrsIPLSHxKvhUg15rQWtv3oCFKMJiU2L55fwvXE64zORGk03tP576kWCnYOp610WtUrP/0k1m7brIvos3TX1alPVkSOMSVJEvOIF8TJjmzp3LuHHj6NGjB9WqVcPOLusahnbt2j1ZxM+AJExCPL2E9AR+Of8Ly84sY+4Lc7HQFWfO7kusPvEPOoMKRedEJT9n3mhSkuahHpI4CVHIpZ07R0Sv3hgSErCtUxu/OXNQa7XGkym3YH4riDsHbqWg3xawczNvwEIUUSdiT7D0zFK2RmxFZ9ABsLrdakq6lDT1URSF7WdimLHzIn9H3gbAQq2ifWVvBjcqUaQr3j2JfEuY1NlVxQFUKhV6vT43t3umJGESIu/oDDos1fcWhr7z+/vsiNyCLrEyaXENMKR7Uc7bkeHNSvFCWU9JnIQohBSDgbCOnUg/dw6bypXx/+F71He/KM1IhkXt4ephcPCG/lvBufCtbxaiIMvQZ7AlfAs/nf0pSxGHyu6V6R7aneb+zdFYaDAYFLaejuKbHRc5fSMRAK2lmldq+DGwYTC+LrbmegkFWk5zg1yXwTAYDE8VmBCiaLg/WTIoBpJ18SjosXA8ip3jUZSU0pyNbcCgxQmU83aSxEmIQkilVuMzdSoxX36J95eT7iVL+kxY0duYLFk7w2urJVkSIh9sj9jO//74H2DcYLZ1UGu6h3annFs5APQGhXV/X2fG7xc5F20s9mBrZUGvOoEMaBBEMXut2WIvSqSsuBAiz/wT9w8L/lnA9ivbTQUilHQfUmOaob9TlrLFHXm7uSROQhR0iqJk/2fUYIA1g+HEcrC0gd5rwa/msw1QiCJIURROxJ0gIT2Bhr4NAWO12p6betLMvxmdS3XGzcY45VWnN7D+xA2+/f0Cl2KTAXDQWtKnXiD96gXhYidFV3IiX/dhSk5OZvfu3Vy5coWMjIws5956663cR/uMSMIkxLMRmRTJ4tOLWX1hNWn6NCrbv8Kxv6uRnGGcslu2uCPDm5eihSROQhQ4+tu3iXx9MMXefBP7+vWynlQU2PIhHJwJKgt49WcIaWGeQIUoIu5Ou1t6Zimnbp7Cx96HDR03YPGQsvyZegO//XWdmTsvEhZnTJQcrS3pXz+YPvUCcbLRPOvwC7V8S5iOHz/Oiy++SEpKCsnJybi6uhIXF4etrS0eHh5cvnz5qYPPL5IwCfFs3U67zfJzy3mlzCsYdDZ8/8dlFh7bQqZlJBnxtSnr6SmJkxAFiCE5mYh+/Uj7+wQab2+CN29CbXXfN9V/TIXt44w/d5wLlV4xS5xCFAVxqXGsOLeC5eeWcyvtFgBWaitaB7XmvZrv4Wh177Nqpt7AqmNXmbHzIpG3UgFwsdUwoEEwveoE4GAtidKTyLeEqXHjxoSEhDB79mycnZ35+++/0Wg09OzZk+HDh9OpU6enDj6/SMIkhHkpisKr63tw6tZJFIOWzPhaZNyqT6i7ryROQpiZISODyNdfJ+XAQdROTgQsXoR1SMi9DscWw9o3jD+3+AzqvmGeQIUoAlacW8EXh74g05AJgIetB6+UfoXOIZ1xtXY19bu7Rmna9vOE3zRu6eFmZ8WghsH0rB2AnTbX5QjEffKt6MNff/3F3LlzsbCwwMLCgvT0dIKDg/nyyy/p3bt3gU6YhBDm17Ncd344+QMXb1/Eym0PGtd9XLpdjcE/N6RMsWCGNytFy3KSOAnxLCk6HddHjjQmS7a2+M/7LmuydHYjrPt3yn294ZIsCZFLBsVAmi4NW42xWl2ISwiZhkwqFqvIa2Vfo1lAMzTqe6NEBoPCllNRfL3tPBdi7gDGRGlI4xL0qBWAjdWD0/VE/sl1wqTRaEwfZDw9Pbly5QqhoaE4OTlx5cqVPA9QCFF0qFQq2ga3pU1QG/Ze28sPJ3/gWMwxrFwOoXE+zKWbjRm8pCXlvB0Z8UIITcvIPk5C5DfFYODGRx+TtG07KisrfGfNxKZixXsdIvbDr31BMUDlntD8E/MFK0Qhk6ZLY/3l9Sw+vZhaxWvxv1rGineV3Cuxou0KQt1Cs/RXFIVd52KZvPUcp64by4M7WlvyeqMS9KkbKCNKZpLrd71KlSocOXKEkJAQmjRpwpgxY4iLi2Px4sVUqFAhP2IUQhQxKpWKhr4NaejbkOMxx5l/cj67ru6iRYkq7Eyw4NT1RPr/+CcV/VwZ+UJpGpYqJomTEPkkYfUaEtasAQsLfKZ+jV3t2vdORv0Dy14BXRqEtIaXpoP8WRTiseJS4/j57M+sOLeC+PR4AJIykni3xrto1MbBh/8mS/svxjF56zmOXbkNgJ2VBf0bBNO/fpAUczCzXK9hOnLkCElJSTRp0oTY2Fh69+7NH3/8QcmSJVmwYAGVKlXKr1ifmqxhEqLguhh/kSCnIBJS9Xy35zKLTi8Eu7/JuNmYSq71GPlCGeqWLGbuMIUochSdjhsfj8Gudi2c2re/dyI+HH5oCXeiwL8O9FwFVrL5pRCPciH+AotOL2LD5Q2m9Unedt70CO1Bp1KdsLeyf+CaoxG3mLzlPAcu3wTAWqOmd91AXm9YAlcpD56v8rWseGElCZMQhYOiKLRa+SLXk68CoE93J+NmQ6q6NWPUC+WoGeT6mDsIIZ7KnViY3wJuXQaPstB3I9i4mDsqIQq8qUenMv+f+YBx2l2vsr1o6t80y2bvd/1zLYHJW8+x61wsAFYWarrX8mdo4xJ4OFo/07ifV0U2YZo1axZfffUVN27coFy5ckybNo0GDRrk6FpJmIQoPG6m3mTZ2WUsO/MTdzKNu5cbMh3JuNWAGm6tGfVCRar6ywc4IZ7E7VWrST1+DK9x41BZ/GfxeFoi/NgWbvwNTv7Qfys4FjdPoEIUYKm6VNZdWkeISwiVPSoDEJUcxZQjU+hZtieV3B8+6+pcVBJTt51n86koACzUKrpV9+WNpqXwcbZ5VuEL8jhhqlKlSo7XDxw7diznUebS8uXLee2115g1axb16tVj7ty5fP/995w+fRp/f//HXi8JkxCFT3JmMr+e/5UF//zIzTTjt3CZt6uRdqMrTUq7M+KF0lTwdTJzlEIUHolbt3Lt7XfAYMB70hdZp+Hp0mFpFwjbA7Zu0G8rFCtpvmCFKIBiU2L56exP/HL+F26n36a+T31mN5/92OvC4pKZtv08a/++jqIYlwN2qOzD8GalCCxm9wwiF/+VpwnTJ5/kvCLO2LFjc9w3t2rVqkXVqlWZPfveL2VoaCgdOnRg4sSJj72+QCVMBgMoerCQRXxC5ESGPoP1l9fz3d8/EKh/nW1/W6I3KKgsE6hfypXRL9SlrLd8ESLEo9zZt4+rg4egZGbi1KUzxT/99N4Xoga9sRre6d/Ayh56rwOfquYNWIgC5Oytsyw+vZiNYRvRGXQA+Nj78FrZ1+hepnu2gwtX41P4dsdFfj12Fb3B+LH7xQpevNM8hFKeDs8sfvGgIjclLyMjA1tbW3755Rc6duxoah8+fDh//fUXu3fvfuCa9PR00tPTTceJiYn4+fmZP2HSZ8Jvw0iPSsBq4GJUGlnQJ0ROKYqCSqUiPC6Zb3ZcYGPUN1g6HUWXWIlarp358IWmhMg/QEI8IOX4ca7064+SmopDq1b4TJl8bzqeosCGkXDkB1BroMcvUKKJeQMWogAZf2A8v5z/xXRc1aMqr5V9jSZ+TbBQP3xPpOjENGbuvMhPh66QqTd+3G5axoMRL4RQ3kdmRhQE+bZxrbnExcWh1+vx9PTM0u7p6UlUVNRDr5k4cWKuRseemagTJO9Yx9XdjjgefBGvhVtRqdXmjkqIQuHuN3iBxeyY0q0S8ZusOBJrQON0nGP647T/tQy1XLoy5oUXCXZ/sBqREM+jtHPniHx9MEpqKnYNGuDz5aSsa5d2TzImS6ig03eSLInnXkpmChZqC7QWWgDKupXFQmVBi4AWvFb2NSq4Z7+Vzs076czdc5kf94eTrjMAUK+kGyNeKE21AFl7WxjleoRJrVY/cj2TXq9/6qAe5vr16/j4+LB//37q1Kljav/ss89YvHgxZ8+efeCaAjvCBCTOG8+1r5eBosK1WSgeM1bKPjNCPKFTN08x7fBcDkbvAox/pelTAqnp/ArjW3TE301KIYvnlyE9nUstW6GLisKmalX8f/getc19C8sPf28cXQJ4cTLUHGieQIUoAGJSYkzrk96q8hbdSncDjBvQ3k6/jZedV7bXJqRm8v3ey8z/I4zkDOPn4WoBLoxsEULdErItRkGUbyNMq1evznKcmZnJ8ePH+fHHH/N1NKdYsWJYWFg8MJoUExPzwKjTXVqtFq1Wm28xPQ3HgWMwJMZzY95mbu04g8VH/Sj22QJzhyVEoVTOrRzzWn1DeEI4Xx/6jl3XN2JhG87+a4dpOsWFLtV8eaNpSXxdJHESzx+1VkvxT8YRN3sOfnNmZ02WTq2GDaOMPzd6X5Il8dw6ffM0i08vZnPYZnSKcX3SzsidpoTJ2tIaL8uHJ0vJ6ToW7g9n7u5LJKYZry3v48jIFqVpHOIuX4gXAXm2hmnZsmUsX76c3377LS9u91C1atWiWrVqzJo1y9RWtmxZ2rdvX/iKPvzr5se9ifnlEACe/Vrj+t7XZo5IiMIvOjmaqYd+IDKsFvvOpwFgZR9G9VLpfN68P4FuzuYNUAgzuLv+z+TyLljaFfQZUL0ftPnaWLZLiOfIrshd/HjqR45EHzG1VfWoSq9yvWjs2zjb9UkAaZl6lhyMYNauS9xKzgAgxNOeES+UpmU5T0mUCoFnXvTh0qVLVKxYkeTk5Ly43UPdLSs+Z84c6tSpw3fffce8efM4deoUAQEBj72+ICZMKAqxb3cibotxSqH3O91xev1jMwclRNFxNOIWX287z7GMz7G0C0PR2VHeoS2fNRtECTcPc4cnRL7QJyVx438f4jFqJFYP+/fx+nFY2BYy7kDZ9tBlATzig6EQRdUbO95g99XdWKosaRHYgl5le1GuWLlHXpOhM7D88BW+/f0iMUnGpR9Bxex4u3kp2lb0xkItiVJh8UwTptTUVEaPHs2mTZs4d+7c097ukWbNmsWXX37JjRs3KF++PFOnTqVhw4Y5urZAJkyAotcTPaAl8QeuYe+bge+8haiCcrYZrxDi8QyKgUn75vPLhSVkqm8CoBisCLVryYQmQynt7mvmCIXIO4bUVK4MHEjqkaNoS5cmaPWqrIWFbl6CH1pAShwENoCeK8GyYE5fFyIvRSdH89PZn3i59MsUtzduxnw0+ii7r+6me5nuj1yfBKDTG1h1/BrTt1/g2u1UAHycbRjerBSdqvpgaSEFvAqbfEuYXFxcsgwxKopCUlIStra2LFmyhHbt2j151PmsoCZMAEpmBrc/aIOz7SFUNg7QdwMUf/gO0UKIJ5Opz+TbP1ey9NxCMtTXAFAUC6o4dOPb1qNwtpUS/6JwUzIyiHzzTZJ370Ftb0/Aoh+xLlv2XofEGzC/Bdy+Al4Voc8GsC5Y/x4Kkdf+uz6pT7k+jKw+MsfXGwwK605cZ9r2C4TFGWdSeThoebNpSbrV8ENrKaOzhVW+JUwLFy7MkjCp1Wrc3d2pVasWLi4Fu1RiQU6YAMhMhSVdIOIPFJti6NouQVOuzuOvE0LkisFgYNahDSw8NZ90y4ukXnsZ6/Qa9KsfRL96gZI4iUJJ0eu5/u67JG7chMraGv8fvse2WrV7HVJvw4IXIeYUuARB/61gL9NSRdFkUAzsjtzNotOLsqxPquZZjf7l+9PA9/EzeRRFYcupaKZuO8+56CQAXO2sGNKoBD1rB2BjJYlSYVfkNq7NCwU+YQJIS0BZ0IboDVdIjLQjYOH3aKvUM3dUQhRJiqIw79BOVh5QcS4qBQAHjwP4eIfxXp0hNPGvJ4t2RaGgKApRY8dxe8UK0GjwmzUT+wb3fSDMTIXFHeHKAbD3hH5bwDXIfAELkY8UReHl9S9z5tYZACxVlrQMaslrZV+jnNuj1yfdvX73+VimbD3PyWsJADhYW/J6w2D61AvCXltotjEVj5FvCdOJEycefiOVCmtra/z9/QtsKe9CkTABhrhIIjq3JC1awdJORcBPy7EKyX6DNCHE0zEYFDafiuLrbWe54TAOtdUtANytSvBOjddpHfwClmr5B1IUXDcXLiTmi0mgUuHz9RQcW7e+d1KvgxWvwbmNoHWEvhvBS/5NEUVLXGocbtZupi+5Jh2axG+XfqNbSDdeKfPKY9cn3XXg0k2+3naOw+HxANhaWdCvXhADGwTjZKvJt/iFeeRbwnT/xrUPlCgFNBoNL7/8MnPnzsXa2voJQs8/hSVhAtBH/EPEK11JjweNkwUBv65D4yffBgqRn/QGhcVHjjPr6HxSrPejUmcC4GjpyaBKfehauhO2GtnLSRQ8uvh4Ige9jnO3rrh07XrvhKLA2jfg+BKw0MJrqyFQZi2IouPUzVMsPr2YLWFb+L7l91TzNE5DTUhPQKPW5Pjv7CPhxoqq+y8ZCwNpLdX0qhPA4EYlcLMvmAMB4unlW8L022+/8f777/Puu+9Ss2ZNFEXh8OHDTJkyhbFjx6LT6fjggw94+eWXmTx58lO/kLxUmBImgMzT+4no3ZfMJDXaYlb4r9mKZbGHb9IrhMg7Or2BZUdO8+2RH0mx3oPa0jhdr6xDUxa1+1oW+IoCSdHpUFn+ZyR0+zj4Yyqo1PDyEijTxiyxCZGXdAYduyJ3seTMEo5GHzW1D6o4iDervJmre/0VeZuvt51nz/lYADQWKl6p4c8bTUvi6ViwvvgXeS/fEqaaNWvy6aef0rJlyyztW7Zs4eOPP+bQoUOsWbOGkSNHcunSpSeLPp8UtoQJIOPwJiIGvY0uVY21jx3+a37HwqFwxC5EYZepN7D8yEWm//kzKTa/k3rtZTy1IbzRtCQNQ63IMKRQwrmEucMUz6mE9RswJCXi8uqrD+9wYCZs+Z/x53bfQtVezy44IfJBhj6DpWeW8tPZn7iRfAMwrk9qFdSK18q+Rlm3so+5wz3/XEtg6rbz7DgbY7yPWkXX6r680bQUPs42+RK/KHjyLWGysbHh+PHjlClTJkv72bNnqVKlCqmpqYSHh1O2bFlSUlKeLPp8UhgTJoD035cS8fZ49Jkq/PpVxX7UUtmNXYhnKENn4OfDEczaeZmoxDQA3PzXk2H3B/W8G9CvfB9qeNWQAhHimUnatYurb7wJOh1+8+Zh36B+1g5/L4fVg4w/NxsDDXJeQlmIgsqgGHhp9UtcSbqCi9aFLiFdeLn0y3ja5Xz2zdmoRKZuO8+WU9EAqFXQqaovbzUthb+bTLl+3uQ0N8j1KuYyZcrwxRdf8N1332FlZSy9m5mZyRdffGFKoq5du4anp0wdyyvapj3w+/QWmes+wz55A+wYD83HmjssIZ4bVpZqetUJolt1f346dIUZOy+SrEvGUlGx7/pe9l3fSxmXUPqW70OLwBZSIELkq+RDh7g2/G3Q6XBs2xa7enWzdriwDX4bavy51hCoP+KZxyjE09Ib9Oy+upt1l9bxRcMv0FpoUavUvFn1TVIzU2kd1Bpry5xPmbsYk8TU7RfYcMI4MqVSQftK3rzVrBTB7vb59TJEEZHrEab9+/fTrl071Go1FStWRKVSceLECfR6PevXr6d27dosXryYqKgo3n333fyK+4kU1hEmk6MLYd1wAPR1P0LdfGTW3duFEM9EaoaepX9GMHf/nyRa/Y7G+aipQISnrRdDKg2mc0hnM0cpiqLUk/9wpU8fDMnJ2Ddpgu8301Fp7qvcFXkYFrWDzBSo0BU6fgfy74QoRBIzEll9YTU/nf2Ja3eMG4xPqDeB9iXbP9H9wuOSmb7jAr/9dQ3Dv59421QoztvNS1HK0yGvwhaFVL7uw3Tnzh2WLFnC+fPnURSFMmXK0L17dxwcCvYvXqFPmAD2fk3m2glE7HTDvm4tPKcvlmlAQphJuk7Pr0evMmvP38SqdqFx2Y/aMpkKdp2Y0fpDXO1kA1yRd9IvXSKiR0/0t29jW7Mmft/NRX1/NdrYczC/JaTGQ4lm8OrPYCm/g6JwuJxwmWVnlrH20lpSdakAOFo50iWkC6+WeTXHZcHviryVwjc7LrDq+DX0/2ZKL5T15J3mIZT1LqSfAUWek41rH6JIJEyKQuJX/bg2/wCgwq1rMzw+nWHuqIR4run0BtaduM7MnWeJyNiL7k4ZrFUuvFrTnyqlo9l6dRU9Q3tS06umfMEhnoj+9m0ut++ALjoa6/Ll8V+4EAt7u3sdEq7CDy0g8Rr4VINea0Er04xE4XDjzg1armyJgvEjaUnnkvQI7UGb4DbYWOauAEN4XDKzdl1k1bFr6P5NlJqUdmfEC6Wp4OuU57GLwi1fE6bz58+za9cuYmJiMBgMWc6NGTMm99E+I0UiYQJQFOJHdyRqzTkAPAZ0wW3Up2YOSghhMChsPR3NzJ0XTbvD2/nPR213HjB+COgZ2pM2wW1yNfdeCEVRuPndPBLWrSVg8WIsXVzunUy5BQtaQ+xZcCsF/baAnZv5ghXiMeJS4/gr5i+aBzQ3tQ3eNhgrCyt6hPZ4oi+XLsbcYebOi1mm3jUoVYy3m4dQLcDl0ReL51a+JUzz5s1jyJAhFCtWDC8vryy/0CqVimPHjj151PmsyCRMAHodcW+1JHbHdQC8Rg7EZaAs7BWiIFAUhT0X4pi58yJHrp1F47IfjfMxVOoMAJy1zqbqTrmdZiKeb4a0tKzT8DJSYFF7uHoIHLyh/1Zw9jNfgEJkQ1EUjsUcY/nZ5Wy7sg0U2NplK+627oBxb6UnKZhzLiqJb3+/wIaTN7j7ibZxaXfebFpKEiXxWPmWMAUEBDB06FDef//9pw7yWStSCRNAZhoxA5tw8+BtQMFn/Ps4dutr7qiEEPc5HH6LObsuseN8BBrnw1i5HEBtFQ9AqGsoK15aYeYIRUFlSE4m9ptvKfbmm1mn392lz4Sfe8CFLWDtBH3/3959h0dV5X8cf09L75WEVDpIFRSwoyAKFmxYAMGOrn3Vta3i6k/XXdfelaKIDQHFDooURVCQIr2n996Tmbm/PwaDgQQTSDJJ+LyeZ55k7py58811vMxnzj3nfAORjV+HRqQ1lFaX8sWeL/ho+0fsKtxVu71/eH8eGfYIPUN6HtF+N6cX8dL3u/hmc2bttpG9I7n9rG70jwk62rLlGNFigSkgIID169fTpUuXoy6ytXW4wAQYFUVkXn0Ghb9X4hkMiV98gyk03t1lichBdmaV8NaKPXy6LhWnzyZswT8R5BzObSdM4OLjO+Okih9SfuDs+LOxWWx/vUPp0JzV1aROnUrZyp/xPfVU4t56s24Dw4BPb4EN74PVC67+DOKGuadYkQasyljFHUvuoNzuWpfT2+rNmMQxXN7zcnqH9j6ifW5IKeSlJTv5bmt27bYx/Tpx64jumsxBmqzFAtN1113HCSecwNSpU4+6yNbWEQMTgFGcRc4towiJScEa0831LaOuXxdpk7KLK5m1ch/vrUqiuLIGMBHm58HgfltZWfgWYd5hXNz9Yi7rcZku1ztGGXY7aXfdRcni7zD5+BA/cwbeAwbUbbT4EfjpBTBZ4Io50PNc9xQr8ic1jhqyK7Lp7NcZgKKqIkbOHUmUXxSX97yc87ueT4BH0z9/GYbBz7vzeG3ZblbszAVc6yid3z+aW8/sRg9NDy5HqMUC01NPPcWzzz7L2LFj6devHzZb3W9Cb7/99iOruBV01MAEQGGKazrZ4jSIPh7nZR9iDtbiwSJtVWmVnY9/TWH6j3tJK6zAFvQLnuHfYbIWA2A2mTk95nSu6HkFw6KHYTZpLZ1jgeF0kvHQwxQtWIDJw4PYN9/Ad9hBPUcrX4ZFD7l+v/AVGDSx9QsV+ZOk4iTm75zPp7s+JcY/hjlj5tQ+trdoLwkBCUc0Q6jDafDt5kxeX7abjamuiXQsZhMXDozmbyO60VULzspRarHAlJiY2PDOTCb27NnTlN21qg4dmGD/GhznULi5guzNYcR/OA/P7kd2bbCItA67w8mXv2fw5vI9bE4vwOq/GVvwKqy+B86lCQEJfHLBJ3haPN1YqbQ0wzDI/ve/yX/nXbBYiHnhefxHjqzbaMNHsOBG1+8jp8Epd7V6nSIAVY4qFictZv7O+fya+Wvt9gjvCD654BOCvY58woUqu4P5v6Xx5vI97M0tA8DTamb8kFhuOLULcaE+R12/CDQ+GzR5OpK9e/ceVWHSgsJ7Ylz5EQVXTsRR5iB54ngS5n2JLSbG3ZWJSAOsFjMXDuzMBQOi+Xl3HjN+iub7bf0x2bKxBa/CM2gdFkck1TVmPC2u5+wp2kNiQKLWdOpg8qdPd4UlIOr/njg0LO1cDJ/d4vp92C1w8p2tW6DIfnN3zOX5tc9TXH2gR/zk6JO5pPslnBZ7GjbzkY3DLK6sYc6qZGb8tJeckioAAr1tXD08nsknJRDmpy+NxD2abeHa33//nenTp/P88883x+5aRIfvYdrPvu5zkm66m+piKx5hPsQv+AZreLi7yxKRRkrOK+fdn/fx0ZoUSqrKMVnK8bWEcengGEYP9OCmHy6hV0gvLu1xKWMSx+DnoctSOoLK7dtJvu56wm68kZCrJ9V9MHUNvHM+1JRDv8vgojfBrMs0pXWU15TjMBz4e7jGCi1OWszdS+8myjeKi7pfxEXdLjqqMZfZxZXM+Gkfc1YlUVJlByAq0IvrTknkyhPj8PVs+nTjIo3RogvX/vlFPvjgA6ZPn86aNWvo378/69evP9LdtbhjJTAB1Cx/l6S7nqCmzIJn5yDiF3yLpYP/zSIdTVmVnfnr0pj1015257guS7EFbMQ7+mMMk+tDhbfVm7Pjz+aSHpcwMHygep3aOUdREZbAwLobc3a4xqhW5EPXs+DKD8Hq4Z4C5ZhhGAbrc9bz2a7P+Hrv10zpO4WbB9wMuCZ3+CXzF4ZFDcNithzxa2xOL2LmT/tYuD6daocTgO4Rftx0elcuGBCNh1VfCkjLatHAtGzZMqZPn868efOorKzk3nvv5frrr6dbt25HVXRLO5YCE0D1l8+z76FXcVRa8O4WSdzHX2H20XW/Iu2NYRj8uCuXWT/tY8n2bAxzGbbA3/ANXYPDmlXbLiEggefOeI5uwW37XCwHlCz5AUuAPz5DhtTfoCgNpp8NxakQfTxM/hw81aMoLSejNIOFuxfy+Z7PSSpOqt0+LGoYb5391lHv3+E0+H5rFjN+2suqPfm12wfHBzP19K6c1SsCs1lf/EjraPYxTBkZGcycOZMZM2ZQVlbGlVdeybJlyxg+fDhXX311mw9LxyKPsXcSV5JP0v99RMWuLIpefojg+55zd1ki0kQmk4lTu4dzavdw9uWW8e7PScxdE0hh/imYvZPxCv4VW+BGMstyiPKNqn1eRmkGET4RR/UNsLScslWrSbvzTjCZiH9/Dt7HHVe3QXk+vHeJKyyFdoMJcxWWpMUYhsGdP9zJDyk/YOD6Lt3b6s2o+FGM6zaOIZENhPpGKqmsYe6aVGat3EdyvmtdJovZxJh+UVxzcgLHxx35JBEiLa3RgSkxMZHLLruMV155hVGjRmHWtdPtgtcV/yK2OI+yRZ8SVD4LtoyAPhe4uywROUIJYb48cn4f7hndg883pPP+6iA2pMZD5nlYPDM5/6VfuerEOC45vjM3f3czZfYyxnUbxwVdLiA2INbd5ct+FRs3knrLLRjV1fiNPAuvngfNaFpdDh9cCTlbwT8KJi0A3zD3FCsdktNw8nvu7/QP64/JZMJkMhHoGYiBwYmdTuSCrhcwKn4UPrajuzIlOa+cWSv38fGaFEr3j08K9LZx1dA4Jg2LJzrIuzn+HJEW1ehL8nr27El1dTVXXXUVkyZNolevXgDYbDY2bNhAnz59WrTQ5nCsXZJXyzDg89vht3fB4oEx/n3oMVJjHUQ6iE1pRcxZnczC9WmUVTsA8PAqwjfxReyU1bYbFDGI87qcx+iE0QR6Bja0O2lhVbt2kTRhIo6iInyGDSP2jdcxe/5p9i+HHT6aADu+Aa9AuOZriDyu4R2KNMGewj18tfcrvtjzBWmlaXw49kOOC3O9v1JLUjGZTLULzx4pwzD4eU8es37ax+KtWfzxSbNruC/XnpLIRYM64+OhiRzE/VpkDNNPP/3E9OnTmTt3Lj169GDixIncd999bNy4kd69ezdL4S3pmA1MAE4HfHItzo2fkboyDM/h5xHxxHMKTSIdSGmVnc/Wp/H+6mQ2pxeDqQar/2YCw9dT47EDA9egapvZxt+H/J0JvSe4ueJjT3VqKklXTcCenY1X//7EzZiBxc/3QAPDgM9uhfXvgdXL1bMUf5L7CpYOIbMsk2/2fsNXe79ia/7W2u2+Nl/+OeyfjO0ytllep7C8mk/WpvL+L8nsyTnwZc3pPcK59pRETu0WpvFJ0qa06KQPpaWlfPDBB8yYMYPVq1dz+umnc9VVVzFu3DjC2/D01cd0YAKwV1Py2FhS56YCEHbNeML/8ZibixKR5mYYBhtSi3h/dRKfb8igosaByVqMR+B6AsM3UGFK4+UzX+P02FMASC9NJ78yn+NCj9OXKC3InpfHviuvoiY5Gc/u3Yh7912swQeN2/huGvz4HJjMcPkc6DXGLbVKx7EpdxNXfnll7X2rycpJnU9iTOIYzow7E2/r0V0SZxgGvyUXMmd1El9uzKDK7vpixtfDwrhBnbnm5AS6Rfgf1WuItJRWmVYcYOvWrUyfPp3Zs2eTn59PTU3N0eyuRR3zgQmguoz8O0eStaQQgMg7biDk5rvdW5OItJjiyhq+2JDBJ2tT+C25EDAwe2YQYIll3MBYLh0cwzfpb/HOlndIDExkbOJYzkk8h/iAeHeX3uE4q6tJv/c+KjdvJn7OHGyREXUb/PwqfPuA6/cLXoLjr279IqVdK68pZ2nKUqocVVzU/SIAHE4Hoz4ZRax/LGO7jGVU/CiCvY5+goWSyho+XZ/OnFVJbMssqd3eOyqAicPiuHBgZ/y0fpK0ca0WmP5gt9tZuHAhF198cXPsrkUoMO1XUUjOzWeSu6oCgKiH/k7QpOvdXJSItLRd2aXM+y2V+b+lklVcVbu9U+K3VHr/hMOort3WO6Q3oxNGMzphNDH+Me4ot0MyHA7seXnYIg4KSxs/hvk3uH4/6xE49e+tX5y0S+U15SxPW87ifYtZkbaCCnsF4d7hLL50ce0MmWU1ZfjafP9iT43zx5jJz9anUb5/zKSn1cz5A6KZMDSOgbFB6qmWdqPVA1N7oMB0gFGSRfb1I8nfYAcTdH7yMQIuGu/uskSkFTicBit25vDJ2lQWbcmi2u4EcyWeAZsI67SVUtNWnPvHO4V6hbJk/BLMJs2MeiSc1dUUzV9A0PjLMDU0u+yu7+D9y8Fph6FT4Zx/gz5wyl9YmrKUBTsX8FP6T1Q5DnwBEusfy7mJ53Jd3+uOeoa7P+SVVrFwQzqfrE11jY/cr2u4LxOGxnPJ8TEE+tia5bVEWlOzr8MkHYvJP5KI17/Eec1oCrdB5mPT8D31FCxh0e4uTURamMVs4oyeEZzRM4Ki8hoWbnR9ENqQ4kVa4RBMllK8g7YSGrmVPgE9sTvAw+qahvjOH+5kaNRQzo4/m3CftjtmtS0w7HbS77mXkkWLqNqxnU6PPHJoo9S18NHVrrDU91IY/ZTCktSrqKoIX5svVrPro9svmb+wJGUJAHH+cYyKH8Wo+FH0Ce3TLD081XYnP2zP5pO1qfywLRu70/X9us1i4py+UUwcGseJiSHqTZJjgnqYjnFG5lYyp15IUFw+3oNOhInzwKY1EUSORbuyS/hsfTqfrU+vXVgSDAK9PRjTL4peCTk88/sdAJgwMTBiICNiR3Bm3Jka83QQwzDIePhhiubNx2SzEfvmG/gOH163Ue5OmH42VORDlxFw1cdg9XBPwdImZZZlsjx1Od8lfcevmb/y6shXGR7teh9tydvCkuQljIofRY/gHs0SXAzDYHN6MZ+sTWXhhnTyyw5cpts/JpBLjo/hggHRBPvqfSodgy7Jq4cCUwMyNsCs86CqGLqPxrj0HUyeCk0ix6o/Ztn7bH0aX2zMIKfEdbmPyVJKSMQm/MM2k2ffWec5XQK7cO8J93JK51PcUXKbYhgG2U//h/xZs8BspvMLzxMwalTdRsXprrBUlALRg2Dy5+CpmcSOdU7Dyda8rSxNXcqylGV1pgAHuGXgLdw84OZmf93skko+W5fOvN9S60zgEO7vycWDOnPJ4Bh6ROr9KR1Pswem6OhoLrzwQi644ALOOussPDza37cLCkyHkbQSZl9ERZaDtN9iiZn+AV77FycWkWOXw2mwak8eC9en89WmDEoq7QCYrEWER+4iMHQ7OfbNOAwHs8+dzcCIgQBsz99OTkUOJ3Y6EQ9L+/v34mjkvvYaOS+8CEDUU08RdNG4ug0qCmDmGMjeAiFd4bpF4BvW+oVKm7OjYAeXLLyk9r4JE/3D+3NG7BmMih/VrD25BWXVfLM5ky82pvPz7jz2X3GHh9XMqD6RXDo4hlO7hWG1aPyidFzNHpiWLl3K559/zsKFC8nKymL06NFccMEFjB07lpCQkGYrvCUpMP2FnYtJvmEqZZkeWPw9SPj4UzwSE91dlYi0EVV2B8u25/DZhnS+35pFZY1rYgjMFYSF72Vsl3MY0y+aIQkhPL7qMebtnIevzZeTo0/mlM6ncHLnk4nwiTj8i7Rz+XPmkPX4EwBEPvgAIVcfNDV4TQXMvgiSfwa/Tq6wFKzLGY81GaUZ/Jj+I8tSlhHmHca0k6YBrt7JixdeTEJAAqfHns6pnU8l1Du02V63qKKGRZsz+WJjBj/tyq0dlwQwMDaISwfHcH7/aE3gIMeMFr0kb/PmzSxcuJDPPvuMdevWMXz48Nrep65dux5V4S1JgemvOVa9R9Kd06gqtGEN9iFh/hfYoqLcXZaItDHl1XaWbc/hq02ZLNmaRdn+6YUBwvw8iO26jGxjBcU1+XWe1yO4Byd3PpnbB91eO3i9Iyn+5hvS7r2PsBtvJPy2W+s+6LDDx5Ng+1fgGQjXfAWd+rqnUGlV5TXl/Jr5KyvTV7IyfSX7ivfVPubv4c+yy5dhM7tCimEYzTqRQmmVne+2ZPHFxnSW78il2uGsfax3VADn9Y/ivP5RxIc2z7TjIu1Jq41hysrKYuHChSxcuJDvv/+eLl268PTTTzN27Nij2W2LUGBqHPuSV0j6x/NUl1jxiAwkft4XWMN0uYiI1K+yxsGPO3P5elMmi7dkUrz/sj1wEhiUSUJsCjUeW0gt34GBQUJAAp9f9Hnt85enLqdLYJcOs95T1a5deHTtWvdDr2HAwlth3Xtg8YRJCyDhZPcVKS3q4NAz6atJrM9ZX3vfbDLTL6wfp8eczmkxpzXbpA1/KKqoYen2bL7+PZMftmdTZT8QkrpH+HFe/2jOGxBF13C/ZntNkfbILZM+lJeX8+233+Lv78/IkSOba7fNRoGp8WoWPsG+ae9gL7fiGRdB/NyFWAID3V2WiLRxNQ4nP+/O4+tNmSzanEnen2bZ8rBV0KNLBn2j/blt6OV0DvKm2lHNKR+eQoW9goSABIZGDeWETicwJHJIs16K1JLKf/sNW1TU4Xvjv/8XrPgfmMwwfjb0Pq/1CpQWZxgGKSUp/Jr5Kz9n/MzarLV8Pu5z/DxcgeSldS/x5Z4vOTn6ZE6KPokTok4gwKN5P4ekFpTz3ZYsFm/NYvWe/DqX2yWG+e7vSYqmZydN3iDyB82SVw8Fpqap/uAe9j29EEelhZCxJxH5v+nuLklE2hGH0+CXvfl8tzWL77ZmkZRXXufxXp38Gd7Dwu/Vr7G7eDN2w17n8a6BXbmi1xVc0euK1iy7SSp+30Ty5MmYgwJJmD0bW+fOhzZa9Tp88w/X7+e/CIMnt26R0iLSS9NZkbqCNVlrWJu1lpyKnDqPvzDiBc6MOxOAGmdN7SV3zeWPKcAXbcniuy1ZbMkorvN4twg/RvWJZGy/KI6LDtB6SSL10MK1ctQ8rvgvcaVF5M9bRLjvp7DzCug+6i+fJyICrgVyh3cNZXjXUB4e25vdOaV8tzWb77dmsTapgG2ZJWzLBLiSUH8nx3fNxicgicyazewq3MHuot2U1pTW7i+3IpfXN7zeZnqgqnbtIuWGG3CWl+PVvz+W+i5d/v2TA2HpzIcVltoph9PB9oLtRPpE1r7vfkr/iSdWP1Hbxma20S+sHydGncjJ0SfTN6xvnceaQ2WNg1/35bN4f0hKL6qsfcxsgiHxIYzsE8GoPp1IDNOYJJHmoh4mOTynE+bfAJs+Aas3xsR5EDsMk8Xi7spEpB3LL6tm6fZsvt+azbIdOZRWHehdsphN9Iu1khiTxTk9jmdk9z5YzCa+2vMV/1jxj9p2cf5x9A/vX3vrEdyj2b/Fb0h1ahpJEyZgz8rCq18/4mbOxOJ30AfU3Utgznhw1sCJN8K5/wF9y98uFFQW8Hvu765bzu9szNlISU0JDw19qLbHc1/RPp5Y/QRDIocwOHIw/cP742nxbNY6DMNgd04Zy3fksHxnDqv25B2YnRLwtlk4rUcYo/p0YkTPcEL9mvf1RTo6XZJXDwWmI+SogQ8nYOz4lpzNYdREnEH0S28qNIlIs6i2O/llbz7fb8ti2Y4c9uSU1Xk8yMfGyd3C6BFTRK7pJzYX/MbOgp2H7OeZ059hdMJoAPIq8nAYjhaZxtyek8O+CROpSU7Go1tX4mfPxhocXLdR2lqYdT7UlMFxF8ElM8Cs9Wzauj2Fe7h1ya2klKQc8pivzZfr+13P9f2ub9Eaispr+Gl3Lst35LBiZy5phRV1Ho/w9+Ss3hGM7B3Jyd3C8LLp32KRI6XAVA8FpqNQU0Hlcxewd2YKGCaCzj+bTv95XtdEi0izSy0oZ/kO1wfGn3bn1i6W+4cekX4M6+pNRHg2dts+thVuYmPORuZfMJ9Ovp0AeGvjW7y47kU6+Xaib2hfeob0pHdIb3qG9CTSJ/KIz12OoiKSrp5M1fbt2Dp3Jv79OdgiI+s2yt0FM86G8jxIPB0mzAWrvvlvCyrtlewq3MWOgh1szdvK77m/c2LUidw9+G4AymrKGP7+8NrZHPuH96dvWF/6h/enZ3DPFpkKv9ru5Pe0QlbsdL3n16cU8qf5GvCwmjkxIYTTeoRxWo9wekb6699ekWbSImOYioqKWLBgAStWrGDfvn2Ul5cTHh7OoEGDGD16NCeddNJRFy5tlM0br9vn0TlrFGlfFVH4+SLMfv8k4pHHdeIWkWYVE+zDVUPjuGpoHHaHkw2phSzbH6A2pBayI6uUHVmlgAmzKZF+nQdyXmIwW1PN+CXY8fO0kluRi9lkJrMsk8yyTL5L/q52/8GewXxw3gd09nNN0FBUVYSvzbdRH4aN6mpwOrGEhxE3c8ahYak4w7UwbXkeRA2EK+YoLLlZjaOGB398kO0F20kqTsJpOOs8/ufLOH1tvsw6ZxZdg7oS6NkyM8NW2R1sSCli9Z48Vu3N47ekQipqHHXadIvw47Tu4ZzWI4yhiaF4e6gXScSdGtXDlJGRwSOPPMKcOXPo1KkTJ554Ip07d8bb25v8/Hw2bdrE2rVriY+P59FHH+Xyyy9vjdqbTD1MzaAsj8J7R5KxxDXQNOymawi/6z43FyUix4rC8mp+3JXLT7ty+Xl3HvsOmnnPYjbRr3Mgw7qEcnyCD16+aewt2cG2/G1sy9/G3qK9WEwWVk1YVftB+aEfH+Kbvd/QLbgb3YK60SWwC12DutIlsAud/TpjMdf9sOooLMSel4fnwQu1VxTCzDGQvRlCusC1i8AvvCUPxzHPMAyyyrPYV7yPfUX72Fe8j50FOwnzDuPp056ubXfmx2fWzmIX4hVCj+Ae9AjuUdt79Ed4bgmVNQ5+Sy5g9Z58Vu/NY11yYZ11kQBCfD0Y1iWE07qHc2qPcDoHebdYPSJyQLP2MA0YMICrr76aX375hb5961+VvKKigk8//ZRnn32WlJQU7rnnniOrXNo231CCnvoC511nk7XSSe4bMzH7+RN6w83urkxEjgFBPh6uRTf7RwOQUVTBqj15/Lw7j1V78knOL2d9SiHrUwphGVjNJvp27sbg+BO4oUswfTv7UGPOrdOrkFScRLWzmi15W9iSt6XO63lbvflx/HJqftuI79AT+S3rN8wmMzHRMXj8eXHSmgr44EpXWPKLdC1Mq7DUbMpqysiryCMuIK52202Lb2Jd9joq7BWHtA/3rnvs7zvxPvxsfvQM7kmYd1iLXhmRXVLJ+uRC1qUUsnZfAetTCql21A1IYX4eDE0MZWiXEIZ1CaVbuB9ms67WEGmrGtXDlJOTQ3h440/8TW3fWtTD1IzydpN727nk/GYBE3T59BM8ex7n7qpE5BiXVljBqt15/Lw/RB08YB6gc5A3x8cHMzguiMHxIfTo5EtWeTrbC7azu3A3e4r2sLdoL3uL9tLJJ5I3fxtE0SfziHzkn9wW9CUbczYC4GP1IS4gjli/GDqnrSc2awfjq81wzVfQqV9r/+kdwsr0lewp3ENGWQYZZRmkl6aTUZZBfmU+nf06880l39S2nfz1ZH7L/g2LyUKMfwwJAQkkBCTQNahr7Zi1lr5kvLLGweb0YtYlu4LRuuTCet9zEf6eDO0SyrAuIQxNDKVruK8uZxdpAzTpQz0UmJqXkfE7ObdfhKdPKYFnnghXfQw2L3eXJSJSKyW/nLVJBbW3bZnFdQbUA3jZzAyICeL4+GAGxATSt3MgnYO8cTgdpP77/6iY/SGYzXR+/jmmeXzD5tzNZJRlYFB3RzE1dr4e+RYknALADYtuILcilwifCMK8wwjzDiPcO5wwnzA6+XRiYMTAVjoK7rWvaF9t6MmryCOvMo/8ynxyK3JxOB28efabtW2v/vpq1mWvq3c/Yd5hLLp0UW3v4Lb8bXhYPIj1i8Vmafnp5B1Og725pWxKK94fjgrYklFMjaPu+8Bkgh4R/gyMDWJQXBBDu4SSEOqjgCTSBjVrYFq4cGGjX/iCCy5odNvWpsDUAlLXwrsXQHUp9DgX47J3MNk0wFlE2qbSKjsbUwpdASq5gN+SCig+aBY+cE1lfmPyckb8OA8A24OP0nXS5bUfeqsd1aSWppKy4j8k7/iCdKsV394XcOuoF2v3MeLjEeRW5NZbR5x/HF9e/GXt/RsX3UhORQ6BnoEEewYT6BlIgGcAfjY/wr3Duaj7RbVt9xTuwcDA0+LpulldPz3MHs3yodxpOKl2VFPlqKq9OZwOEgITatt8n/Q9ySXJlFSXUFZTRmlNae3vJky8Pfrt2rZTvpnC2qy19b6WxWRhzcQ1tRNuvLzuZfYU7SHKN4pov2iifKNqf2+pSRjqU15tZ1tmCVvSi9mcXsyWjGK2ZxbXWQPpD6G+HgyKC2JQXDADY4PoHxOIv1frrAcmIkenWccwjRs3rs59k8nEn3PWn0/QDkfdmV6kg4sZDFd+CHMupWbDt6ROP5mIJ17Cd/hwd1cmInIIP08rJ3UL46RuYQA4nQZ7cktZm1TAb0mFbEovYkdWCSdvXsaIDfMBeKPvBXy6xR//xxZxXHQAfaMDOa5zAMOzFnD6mg9cOz7veRhyTZ3XmjF6BhllGeSU55BTkUNuRS455a6fkb51Z9fbU7SHrPKsemtODEysE5j+vuzv7CrcVW/bg4PY1MVT2VmwE5PJhMVkwWQy4TScOA0nwV7BzD1/bm3bGxfdyJqsNdQ4aw7Zb5BnECuuWFF7//1t7/NL5i/11mAxWXAaTswm17pTCQEJFFUVEeIVQqhXKKHerluIVwidfDrVee6tg26td58txek0SCusYFd2qSsgZRSzOb2Ivbll1Pd1so+HhV6d/Okf4+o9Oj4umJhgb/UeiXRwjQpMTueBb1S+++47/vGPf/Dkk08yfPhwTCYTK1eu5OGHH+bJJ59ssUKlDUs8FcbPJu+eG6lMKyP1phuIe/c9vAcOdHdlIiKHZTab6BbhT7cIfy4/wTWhQO78BeTMc4WljWdewt4+5+CRWUJJpZ1Ve/JZtSefi83LucjjdQDe9pjImq396VGwg56R/nSP9CM+1IfEwEQSAxMbVccrZ71CfmU+RVVFFFYVUlBVUNtjE+xZd1FcX5svgZ6BVDuqqbRX1rk08OAP7nmVeWRXZNf7mgcHoypH1SHbzCYznhZPrGYrxp8muRgePZwInwj8bH74e/jj5+GHn811C/QMdH2pur+UaSdNa9QxaEmVNQ725ZWxK7uUXdml7M5x/b4np/SQGev+EO7vSZ+oAPpEB3BcdAB9ogKID/XFoskZRI45TR7D1LdvX15//XVOOeWUOttXrFjBjTfeyNatW5u1wOakS/JalnPdXFLu/AflWZ6YfWzEv/8xXr16ubssEZEmyX7hBfJee53giROJfOhBTCYTNQ4nO7NK2ZReRM2WL7lizwNYcPK2/VyesE+kNh3sZza51pNKDPMlMcyXLuGunwmhvkQFemG1mJulVsMwsDvtVDmqqHRU4jScRPhE1D6+r2gfFfYKnIYTh+HAwKjtafIwe9A9uHtt26yyrDqX+nlYPFpkodaWUlplJyW/3HUrqCC1oJzkvHJ25ZSSkl9+yNi1P3hYzCSG+dIt0q82GPWJDiDCX2NyRTq6Fpv0wdvbm19++YV+/erOALRx40aGDh1KRcWhs8O0FQpMLc+5ahbJf3+cijwPLAFexH80H8/Exn3DKiLSVpQsXYrfaadhMh8UbJJWuhamtVdiDLiC7DOfY3tWGTuySvbfXD0YpVWHjov6g9VsIirIi5ggH2JDvIkJdv3sHORDVKAXEQGeeFq1UOmfOZ0G+eXVZBdXkV1SSWpBBSkF5aTmu36m5JdTUH7opYR/5u9lpVuEH93C/ei6/2e3CD9igr2bLcCKSPvSYoHptNNOw2az8d577xEVFQVAZmYmkyZNorq6mmXLlh1d5S1Igal1OJa8SNKDL1JVaMMa4kfCJ59hi452d1kiIg2q3LYNj4QEzF6H6VXI/N21MG1VMfQ4By5/D+qZnc0wDHJKq9ibU8beXNdtd04Ze3NLSc4vP2RWtfoE+9iIDPCiU6AXkf5eRAZ4EurnSYivB6G+HoT4eRDi60Gwjwe2dvph3+E0KK6oobCihoLyavJLq8kucQWirOIqckoqXfeLq8gtrcLeUBfRnwT52IjdH0Bjg32ICfGha7gv3SL8CPfz1FgjEamjxQLTrl27uOiii9i+fTtxca7rvZOTk+nRoweffvop3bp1O7rKW5ACU+uxf/kESdNmUV1iw7dvPHGffPPXTxIRcYOKTZtJnjIFr969iXntNSx+voc2yt8D00dDWTbEnQST5oPNu8mv5XAaB3pI8stJ3X/pWEp+BWmFFWQWV1LdwJiahgR4WQnwthHgZcPfy4q/l40AL2vt794eFrxtFrxsFrw9zHhZLXh5WPCyWvCwmrCYzVhMJixmE1aLCbPJhHX/OB2nYeA0XCHQafxx38DuMKh2OKmqcVLtcOz/6aTK7qSi2kFplZ2y/bfSKofr92o7xZV2CsurKSyvobiypt6JFQ4nzM+DcH8vogO9iA3xISbYm9gQn9qQpNnpRKQpmnWWvD/r1q0bGzduZPHixWzbtg3DMOjTpw8jR47UNzdSyzr2YeLKi8l4ZS5Rib/Aho9gwOXuLktEpI6qnTtJuf56nKWlAJjquxSuJBPeHecKS5H94MoPjigsAVjMJqICvYkK9OaEhJBDHjcMg6KKGjKLK8ksqiS7uIrM4kqyiivJL6smr6ya/P23gvJqDAOKK+37p0Zvu5fEH46/p5VAHxshvh5E+HsS7u9FhL8nEQGeRPzp9zA/z3bbmyYi7ZsWrpWWYxjw9T/glzfAZIHx72D0Ok/BWkTahOrkZJImTMSek4NXv37EzZyBxc+vbqOKApg5FrI3Q3AiXPst+EfWv8NW5nAaFJa7wpMrNNVQUmmn5KCfFdUOKvf3/FTZHfvvO6iscWJ3OLE7DRx/uv1x3wRgArPJhHn/T5PJhMkENrMJT5sFT6sZD6v5Tz9dvVm+nhZ8Pa34eVrx9bTi6+G67+9lI9jHRpCPjSAfDwK9bQpBIuI2LdbDBFBWVsayZctITk6murq6zmO33377kexSOiKTCc75t2tR2/VzKP7fTRSWDSVm5geHHycgItLCajIzSb7mWuw5OXh2707cW28eGpaqy+H9K1xhyS8SJi1oM2EJXL1VoX6usU0iItJymhyY1q1bx5gxYygvL6esrIyQkBByc3Px8fEhIiJCgUnqMpvh/BdxFBWSMW8tzpptpN04mZjp72Gy6VpzEWl99rw8kq+9jpq0NGzxccTNmI4lKKhuI0cNzJ0MKavAKxAmzocQzfgpInIsanI/+F133cX5559Pfn4+3t7erFq1iqSkJAYPHswzzzzTEjVKe2exYpkwi9gru2KyGJT+spH0227EcDjcXZmIHIPsWVnY8/KwRkURP2MG1vDwug2cTvjsb7BzEVi94aqPoVNf9xQrIiJu1+QxTEFBQaxevZqePXsSFBTEzz//TO/evVm9ejWTJ09m27ZtLVXrUdMYJjerqaD08bGkfJIOThOBY84k6pmXDl3nRESkhVVu34HJw3boOnGGAd/cD6tfB7MVrvgAepztniJFRKRFNTYbNPmTqs1mqx20HxkZSXJyMgCBgYG1v4vUy+aN30Of0XlsCJgMir5aQtbD93EMzTsiIm7irKqicseO2vtePXvUv6j28mdcYQlg3GsKSyIi0vTANGjQINasWQPAiBEjeOSRR5gzZw533nkn/fr1a/YCpYPx9CfgsS+IHuUHGBTM/5KSBR+4uyoR6cCMmhrS7rqbpCuupOyXXxpu+Ot0+OEJ1+/nPA39x7dOgSIi0qY1OTA9+eSTREVFAfD4448TGhrKzTffTHZ2Nm+++WazFygdkE8IgU9+RaczPAnuUYp/yn+hJMvdVYlIB2Q4HKQ/8CClS5a4xk06G+jR3jQfvvy76/fT7oNhU1uvSBERadOaNIbJMAySk5OJiIjA2/vIFu1zJ41hamOKUmHGuVCUDBF9MCZ/gck31N1ViUgHYRgGmY9Oo/Djj8FqJebll/A/44xDG+5eAnPGg7MGhlwHY//nWhZBREQ6tBYZw2QYBt27dyc1NfWoCxQhMAau/hT8InFmbCHt8rPIn6FeShE5eoZhkP2f/7rCktlM5//+p/6wlLoGPpzoCkvHXQxj/quwJCIidTQpMJnNZrp3705eXl5L1SPHmtCucPVCSrLCKdlVRdZ/nqNgzrvurkpE2rncV18lf+ZMAKIe/xcB5557aKPsbTDnUqgpg65nwkVvgNnSypWKiEhb1+QxTP/5z3+499572bRpU0vUU699+/Zx3XXXkZiYiLe3N127duXRRx+lurq61WqQFhTRi4Bp8wg5zvXfM/PxpyiaN9fNRYlIe2XY7VRs2ABA5IMPEHTJJYc2KkiC2RdBRQF0HgLjZ4PVo5UrFRGR9sDa1CdMnDiR8vJyBgwYgIeHxyFjmfLz85utuD9s27YNp9PJG2+8Qbdu3di0aRM33HADZWVlWiy3gzBFDyDihY8w/nY5Bds9SH/4EUyeXgScd767SxORdsZktRL78suU/LCUgNH1TAtekgnvXggl6RDeCybMBU+/1i9URETahSYvXPvOO+8c9vHJkycfVUGN9d///pfXXnuNPXv2NPo5mvSh7TP2rSTj1kkU7fIAM8S8+AL+I7UOioj8tYpNm/E6rk/tWoH1Ks+HWWMhewsEJ8A130BAVKvVKCIibUdjs0GTe5haKxD9laKiIkJCQg7bpqqqiqqqqtr7xcXFLV2WHCVTwklEPT8d49ZrKd7nSca9d+Oz9EcsgUHuLk1E2rDir78m7e/3EHzVVUQ+9GD9oamqBN67xBWW/KPg6s8UlkRE5C81agxTWVlZk3ba1PZNtXv3bl566SWmTj38OhlPPfUUgYGBtbfY2NgWrUuah6nbGUQ//xqBXSqJGZ6F5bt7wOlwd1ki0kaVfP89affeB04nzsoKqO/CiZoKeP8KSP8NvENg0qeuHiYREZG/0KjA1K1bN5588knS09MbbGMYBosXL+bcc8/lxRdfbNSLT5s2DZPJdNjbmjVr6jwnPT2dc845h8suu4zrr7/+sPt/4IEHKCoqqr2lpKQ0qi5xP1Ov0UQ/+wo+nZzw+1z4/HYMTfIhIgcpXbGCtDvvArudgAvOJ+qxxzCZD/qnzV4NH0+GpB/BMwAmzYeIXu4pWERE2p1GjWHavn07Dz/8MAsXLmTgwIEMGTKE6OhovLy8KCgoYMuWLfz888/YbDYeeOABbrzxRiyWv56aNTc3l9zc3MO2SUhIwMvLC3CFpREjRjB06FBmzZqF+eB/FP+CxjC1Q5sXwCfXUplvJu23RKJfewfvvse5uyoRaQPKVq0i5aapGFVV+J9zDp2f+S8m60FXmjsdMO962DwfrN6usBR/knsKFhGRNqWx2aBJkz6kpqYyd+5cli9fzr59+6ioqCAsLIxBgwYxevRoxowZ0+QQ01hpaWmMGDGCwYMH89577zUqkB1Mgamd2vARKXffT2maFxZfD+Len4tXzx7urkpE3Kh87VqSr78Bo6ICvzPPJOaF5zHZbHUbGQZ8fjv89i6YbXDlh9B9pHsKFhGRNqdFApO7pKenc/rppxMXF8e7775bJyx16tSp0ftRYGq/HCveJPkfT1OZ74HF34v4D+bi2a2bu8sSETcp+vwL0v/xD3xPOomYV1/B7HHQGkqGAYsehp9fBpMZLp0Jx41zS60iItI2tdgsee6waNEidu3axa5du4iJianzWDvIe9IMLKfeSNwTlSQ99AJVBZB01XjiP/wEzy5d3F2aiLhB4PnnYQ0NwXvQoEPDEsDy/7rCEsAFLyksiYjIEWsXPUzNRT1M7Z/92/+Q/OibVBXasAb6EPfhJ3gmJrq7LBFpBVW7dmEOCMAWEXH4hqteg2/ud/1+zr9h2M0tX5yIiLQ7jc0GLTPgSKSFWEffR9w/J+MZWIO9qJycR+90d0ki0gqq9uwlaco1JE2aRE1mZsMN1713ICyd8aDCkoiIHDUFJml3rGP/SdwD4wnqUkZ07DJY+467SxKRFlSdnEzylCk4cnMx+/hi9vauv+HmT2Hhba7fh98Kp9/XajWKiEjH1S7GMIkczHrh/xHlbXKNUfj8DjBbcHS7EIu/v7tLE5FmVJOeTtKUKdizs/Ho1pW46W9jCQw8tOHO71zThxtOOP5qOPsJMJlav2AREelwjqiHacWKFUycOJHhw4eTlpYGwOzZs/nxxx+btTiRBplMrg9EQ6cCBrlP3s+ec0dRnZrm7spEpJnUZGWTNOUa7OkZeCQkED9zJtaQkEMbJq2EjyaCswaOuxjOe15hSUREmk2TA9O8efMYPXo03t7erFu3jqqqKgBKSkp48sknm71AkQaZTHDOv3H2v4aifd7Yc4tIvvJSatIUmkTaO3tuLsnXXENNcjK2mBjiZs3EGh5+aMO0tfD+5WCvgO5nw0VvgLnp6/SJiIg0pMmB6YknnuD111/nrbfewvanRQJPOukkfvvtt2YtTuQvmUyYL3qOuDtHYvOzU5NTSNKVl1GTkeHuykTkKBh2OzidWKOiiJs1C1t9a+5lbITZF0NVMcSfAuPfBWs9U4yLiIgchSYHpu3bt3Paaacdsj0gIIDCwsLmqEmkaUwmbFe+QvztI1yhKbuApCsvPfxMWiLSptk6dSJ+9rvEz5qJR0znQxtkb4XZ46CyEGKHwlUfga2BySBERESOQpMDU1RUFLt27Tpk+48//kgXLSIq7mI2Y5v4OvG3nYbN105NZr7r8rysbHdXJiKNZC8ooGTJD7X3reHheMTHH9owdxe8cwGU50H0IJgwFzz9WrFSERE5ljQ5MN10003ccccdrF69GpPJRHp6OnPmzOGee+7hlltuaYkaRRrHbMY26S3i/3YKNl871Rl5lM97xd1ViUgj2AsKSJ5yDam33krx11833DB/L7xzPpRlQ2RfmDgfvOqZNU9ERKSZNHla8fvuu4+ioiJGjBhBZWUlp512Gp6entxzzz3ceuutLVGjSOOZzdimvE2ccS3ly74lMPdl2DoEep/v7spEpAH2ggKSr7mWqu3bsYSH4dmzZ/0NC1NcPUsl6RDeC67+DHzqmTVPRESkGZkMwzCO5Inl5eVs2bIFp9NJnz598PNr+5dDFBcXExgYSFFREQEBAe4uR1qS0wELboLf54LZiuPcN3BEDa9/LISIuI2jsJCka66lautWLGFhxL8zC8+uXQ9tWJwBM8+Fgr0Q0hWu+Qr865kIQkREpJEamw2OeOFaHx8fhgwZcqRPF2lZZguMex0MJ47f5pN8+/3YzeHEv/9h/WMiRKTVOQoLSbp2f1gKDSV+1sz6w1JpDrx7gSssBcXD5M8VlkREpNU0KjBdfPHFjd7h/Pnzj7gYkWZlscJFb+IsqsD53a/YCwpJuvJy4t77AM8uie6uTuSY5iwrI/na66jashVLSIgrLHXrdmjD8nx490LI3QEBMa6wFKieYhERaT2NCkyBgRpQK+2UxYptyrvEm64m+Y1fqcovImnC5cTPfr/+D2ci0ipMPj54DxlMTWYmcbNm4tm9+6GNKgpdU4dnbwa/TjB5IQSrh1hERFrXEY9hao80hukY5qjBPvsakl9fRVWhDUuAL3Gz38erZw93VyZyzDIMA3t2NrbIyEMfrCqBd8dB2hrwCXONWQpvYDIIERGRI9DYbNDkacVF2iWLDeukWcT97WQ8g6txFJeRPPFKKrdudXdlIscMR0kJ2f/7H87qagBMJlPDYWnOZa6w5B3smg1PYUlERNykyZM+DBo0CJPJdMh2k8mEl5cX3bp1Y8qUKYwYMaJZChRpNhYr1gkziDffQPIry7BXODCnLIfevd1dmUiH5ygpIfm666ncuBF7dg7RT/+7/oZ/hKXkn8EzECYtgE59W7dYERGRP2lyD9M555zDnj178PX1ZcSIEZxxxhn4+fmxe/duTjjhBDIyMhg5ciSfffZZS9QrcnTMFixXvkXc7SOJPzMXj5/vhw0fubsqkQ7NUVpK8vWusGQJCiLkmin1N6wqgfcuPRCWrv4Uoge1ZqkiIiKHaHIPU25uLn//+9/55z//WWf7E088QVJSEosWLeLRRx/l8ccf58ILL2y2QkWajdmCZfxrWHw84Ld3YcFNlKzZimXAefhoqnyRZuUoLSXluuup3LARS2AgcTNn4NWr16EN/whLKavAKxAmfQqdj2/1ekVERA7W5EkfAgMDWbt2Ld0OmmFs165dDB48mKKiIrZt28YJJ5xASUlJsxZ7tDTpg9ThdMJXf6fi29kkfR8GVhsxr7yG36mnuLsykQ7BUVJCyg03UrF+PebAQOJnzsCrT59DGyosiYiIG7TYpA9eXl6sXLnykO0rV67Ey8sLAKfTiaenZ1N3LdK6zGYY+yyeZ12NT2QVRrWdlKk3UbxokbsrE2n3DMMg9fbba8NS3IzphwlLlxwIS1d/prAkIiJtSpMvybvtttuYOnUqa9eu5YQTTsBkMvHLL7/w9ttv8+CDDwLw7bffMmiQrjuXdsBkwnzh/4i1WEl78WNKUrxJu/NOjKeeIlCXlIocMZPJRPhtt5GenELMyy/hVd/kKpXFMOdSSFl9ICxpzJKIiLQxR7QO05w5c3j55ZfZvn07AD179uS2227jqquuAqCioqJ21ry2RJfkSYMMA+Obh8h4cQ5Fe30AiHzkn4Tsf0+LSOMYhlFnJlWjuhqTh8ehDRWWRETEzRqbDbRwrcgfDANj8TSyXppBwU4/AGLffAO/005zc2Ei7UNNVhZpd9xJp0cfqb9H6Q+Vxa7L8FJ/Aa8gzYYnIiJu0dhs0ORL8v5QXV1NdnY2Tqezzva4uLgj3aWIe5lMmEZNI9Lmjfmll6kps+BbsRiMU6GetcdE5ICatDSSplxDTUoKGQ//k4RP5ta7Zt+hYekziB7Y2uWKiIg0WpMD086dO7n22msPmfjhj8swHA5HsxUn0upMJkwj7ifCyx/jmwcx/fwi1JRhjHoKrFZMFou7KxRpc6qTk0maMgV7ega22FhiXnxBYUlERDqMJgemKVOmYLVa+eKLL4iKiqr/H0WR9m743zB5+MLnd2L8Op30d9dgRA0i+r//xVzfeAyRY1TVnr0kX3MN9qwsPBISiJs1E1unToc2LM93haX03xSWRESkXWlyYFq/fj1r166lV30LD4p0JIOngIcflW/fQvGmbNi4iJSiImJefgWLn6+7qxNxu6qdO0m65locubl4dOtK3IwZ2CIiDm1YlgvvjoOs38E7xDVmKWpAa5crIiJyRJq8DlOfPn3Izc1tiVpE2p5+l+J98yxizyjGZHVSvmo1yZOvxp6f7+7KRNwu59VXceTm4tmzJ/Hvvlt/WCrJhJljXGHJNwKu+UphSURE2pUmB6ann36a++67j6VLl5KXl0dxcXGdm0iH02sMfn+fQ/yoMiweDio3byHpyiupSUtzd2UibhX9f/9H0JVXEDdrJtaQkEMbFKbAzHMhdzsEdIZrvoaIw8yeJyIi0gY1eVpxs9mVsQ4eu9QeJn3QtOJyVJJXU/XqeJIXeWAvt2KNCCdu+nQ8u3d3d2UiraZ63z5s8fF/PX41fw+8cyEUJUNQHEz+HIITWqVGERGRxmixacV/+OGHoypMpN2KG4rnHQtJ8LyI5K8NanJzsKftVmCSY0bJkiWk3XkXoVNvIvyWWxpumLMD3r0ASjIgtBtcvRACO7deoSIiIs2oyYHp9NNPb/Cx9evXH00tIm1f1ABst35NgseFVKbk4rvxARjQG4Lj3V2ZSIsqWriQ9AceBIfrslTD4ah/mv3MTTB7HJTlQHhv12x4/pGtXq+IiEhzafIYpoMVFRXx6quvcvzxxzN48ODmqEmkbQvvieXmr/HtFeW67Gj62VSu/Ir89+a4uzKRFpH/3hzS7/sHOBwEXnghMS88X39YSl4Ns8a4wlKn/jDlS4UlERFp9444MC1ZsoSJEycSFRXFSy+9xJgxY1izZk1z1ibSdoUkwrXfQkQfHPlZpNx6F1lPPEHWU//GcDrdXZ1IszAMg5xXXyXriScACJ40iainnsRkrefihF3fuXqWKosgdphrzJJvaOsWLCIi0gKadEleamoqs2bNYsaMGZSVlTF+/HhqamqYN28effr0aakaRdqmgCi45ivM719BcLdN5GwMIP+dd6jJyiL66X9j9vR0d4UiRyX7v8+QP2MGAGG33krY326pf7KHTfNh/o3grIFuI2H8bPDwaeVqRUREWkaje5jGjBlDnz592LJlCy+99BLp6em89NJLLVmbSNvnHYzp6k8JG3cq0cMKwGxQ8s03JF97HY7CQndXJ3JUPBLiwWQi8sEHCL/1b/WHpbWz4JNrXWHpuIvhig8UlkREpENp9LTiVquV22+/nZtvvpnuf5oVzGazsWHDhnbRw6RpxaXFOOzw+R2UffsxqT+G4Kwx45GYSOybb+ARG+vu6kSOWNXOnQ3PBPnjc/DdNNfvQ66FMc+AuZ6xTSIiIm1QY7NBo3uYVqxYQUlJCUOGDGHo0KG8/PLL5OTkNEuxIu2exQoXvozvxbcQf1YuVh871Xv3kvPcc+6uTKTR7Lm5pN51F/b8/Npt9YYlw4DFjxwIS6fcDWOfVVgSEZEOqdGBafjw4bz11ltkZGRw00038eGHH9K5c2ecTieLFy+mpKSkJesUaftMJhj1GF6X/4uEUbkExJfTaWAO1FS6uzKRv1S9bx/7rriSkq+/IePBhxpu6KiBhbfCTy+47o/6F4x81PX+FxER6YAafUlefbZv38706dOZPXs2hYWFjBo1ioULFzZnfc1Kl+RJq9nwEXx2CzjtEHcSxuXvUfrzOvzOPBOT+ahn8xdpVhUbN5Jy01QcBQXYYmOJe/stPOLrWVusugzmToGdi8BkhvOeh8GTW7tcERGRZtHsl+TVp2fPnvznP/8hNTWVDz744Gh2JdKxDLgcJs4DzwBIXknBnWeSeuttpN1xB87ycndXJ1KrZOlSkiZPwVFQgFffviR88H79Yak0B2ad5wpLVm+44n2FJREROSYcVQ9Te6MeJml1WZthzmUUbcwn45cgDKcJzz69iX3tNWyRWtBT3Kvg44/JfOxf4HDge+qpxDz/HGZf30Mb5u2G9y6Bgr3gHQJXfQyxJ7R+wSIiIs2oVXqYROQvRB4H139H4NCuxI3Iw+LppGrLVvZdehkVGze6uzo5hjkrK8mfMRMcDgLHjSP21VfqD0tpa2H62a6wFBQH1y1SWBIRkWOKephEWkNlMXx8NdUbl5O6IoSqIhsmm41O0x4l6JJL3F2dHKOq9+2jeNFiQm+4vv41lnYuho+vhppy6NQfJnwC/uoZFRGRjkE9TCJtiVcATJiLxylXEj8yF7/OFRg1NWT88xGq9uxxd3VyjKjJzKT4q69q73skJBB24w31h6Xf3oX3L3eFpa5nwjVfKSyJiMgxyeruAkSOGRYbXPgylqBYYqxPkbu5BnN0Lzw7R7i7MjkGVPy+idRbbsGen485IBC/U06uv6HTCd89CitfdN3vfwVc8BJYPVqvWBERkTZEgUmkNZlMcMb9mEK6Em79GzjWwIxz4MoPqCqw4ywqwnvgQHdXKR1M8Tffkn7//RiVlXh2745nYkL9DavLYP6NsO0L1/3T74cz7tcaSyIickxTYBJxh/6XQXACfHgVZP2O45UzSV0WR01GDpH/fJjg8ePdXaF0AIZhkPfGG+Q871pk1ve0U+n87LNY/PwObVycDh9cARkbwOIBF77qep+KiIgc4zSGScRdYk+AG5ZAZF8oz8HT2ItRU0PmI4+S/tBDOCsq3F2htGOO0lLSbr+9NiwFXz2J2FdfrT8spa+Ht850hSWfMJj8hcKSiIjIfgpMIu4UFAvXfoul77l0PimP8H7FYIKiefPZN/5yTQghR6xk0WJKFn/nmo3x8X/R6cEHMVnruahg25cw81woyYDwXnDD9xA3tPULFhERaaMUmETczdMPLn8P08m3E3ZcKXFn5GLxs1K1cyd7L72MooUL3V2htEOBF40j5LpriX9vNsGX1dNbZBjw43Pw4QTXTHhdRrjWWApOaPVaRURE2jIFJpG2wGyBsx+HC1/BNxq6nJWKT4wFo7ycwrmfYDid7q5Q2jjD4SBvxkwcxcUAmEwmIu+9F+8BAw5tXF0Gn1wD300DDBhyLUyYC16BrVqziIhIe6BJH0TakkETIawn1o8nEXdSCnm7Qgm8eSwms77bkIY5iopIu+deylasoPzXX4l59ZX611YCyN/r6lXK3gxmK5zzbzjhes2EJyIi0gB9ChNpa2JPgBuXYUoYTliPPGyLboSl/wank+z//Y/CefMxDMPdVUobUbF+PXsvvoSyFSsweXkRMObchsPSru/hzTNcYck3wjW5w4k3KCyJiIgchsk4hj55FRcXExgYSFFREQEBAe4uR+Tw7NWw6CH45U0Aymynkjx7NwD+o0bS6bHHsIaEuLNCcSPD6SRv+nRyXngR7HZsMTHEvPQiXr1719PYgJ9egO8fA8MJnQfD+NkQ2Ln1CxcREWkjGpsN1MMk0lZZPWDMf13r4Vg88alaQfhwG1itlCz+jj0XXEjpsmXurlLcwJ6fT8r1N5Dzv2fBbidgzLkkLphff1iqHa/0qCssDZoIU75SWBIREWkkjWESaesGTYCI3pg+mkSYOQm/EH/SNiRSnZpLyk1TCbriciLvuw+zj4+7K5VWYrJYqNq3F5OXF50efojASy6p/zK87G0wdwrkbHWNVzr3aRhynS7BExERaQJdkifSXpTlwrzrYM9SnHbIyTiR/J9SAfAeOJD4D95veOyKtHuG3Q4WS+1/44rff8fs7Y1nt271P2HdHPjqHteU4X6RcNk7ED+8FSsWERFp23RJnkhH4xsGE+fDiIcx28xExv5C3IU+WMNDCbn2GoWlDqxqzx72XTWBonnzard59+tXf1iqLoMFN8Nnt+xfX+kMmPqjwpKIiMgR0iV5Iu2J2QKn3wtxw2Dedfiyi65n+WAOz6ltUvLDD1gCAvAZPNiNhUpzMBwO8mfNIueFFzGqq8nJzCTg/PMxe3rW/4SsLa5L8HK3g8kMZzwIp97tet+IiIjIEVEPk0h7lHiqq9egywjMlLt6ExbcjD0rjYwHHyJpwkQyHnsMR2mpuyuVI1S1Zy9JEyaS/d9nMKqr8T31VBI+/qj+sGQY8Nu78NaZrrDk1wkmf+4K1wpLIiIiR0VjmETaM6cTfvwf/PAkGE4cfl3JSjuRoq9ds+dZIyPp9Ogj+J95ppsLlcYyHA7y33mXnBdewKiqwuznR+QD9xN48cX1X3ZZUegaq/T7XNf9rmfCRW+CX3ir1i0iItLeNDYbKDCJdAT7foR510NJBpgslHW6mox5W6lJTgbAf/RoIh96EFtEhJsLlb9SuX07ey+6GJxOfE85hajH/4UtKqr+xruXwKd/g5J01yV4Zz4MJ98FZl08ICIi8lcUmOqhwCQdWnk+fHk3bF4AgDNiMLn5J5P3wQJwODD7+9Nt8SIsQUHurVMOYTgcmCwHLp3Lff11LKGhBF16af29StXlrnWV9i9qTEhXuOgNiD2hlSoWERFp/zRLnsixxicELp0JF78FnoGYs9cSYXqbxH9NxKtfP/xHn62w1MYYhkHJ99+ze8wYKrfvqN0eNnUqwZddVn9YSl0Db5x6ICydcANMXaGwJCIi0kLaXWCqqqpi4MCBmEwm1q9f7+5yRNoWkwn6j4ebf4KEU6GmHK/fnyJhHHS644baZtX79pEy9Waqdu92X63HuOqkJFJuuonUv91KTVIyeW+8fvgn2KthyRMwfRTk7QL/aNc082OfAQ/f1ilaRETkGNTuAtN9991HdHS0u8sQaduCYuHqhXD2E2DxwLTzW8wzTof174NhkP2//1G6dCl7LhxH1lNP4SgqcnfFxwxnRQXZL7zAnvPOp2z5CrDZCL3xRqKeeKLhJ6WthbfPhOX/BcMJ/S6DW1ZCt7Nar3AREZFjVLsKTF9//TWLFi3imWeecXcpIm2f2Qwn3QY3LoXIflBRAJ/eDLPHEXHDFfiddRbY7eS/8y67Rp1N7uuv4ygtc3fVHVrxokXsHjuWvNdex6ipwffkk+ny2WdE3H0XZh+fQ59QWQxf3QdvnQWZv4N3sOuyy0vedv0uIiIiLa7dBKasrCxuuOEGZs+ejU99HyzqUVVVRXFxcZ2byDEn8ji48QcYOQ2sXrBnKR4LLyF2Qk9i33oDz+7dcRYXk/P8C+weNYqCDz9yd8Udlj07B3t6BtaoKDq/8AKxb7+FZ5fEQxsaBmxZCK+cCL+8ARjQbzz87Vfoe3Gr1y0iInIss7q7gMYwDIMpU6YwdepUhgwZwr59+xr1vKeeeorHHnusZYsTaQ8sNjjlLuh9AXx+B+xbAYsexi9qIL6vvUDxulRyX36Z6qQknGVa7La5lK9Z41p09qSTAAgefxkAQZdcjNnbu/4nFabAV/fCjq9d94MT4bxnXesriYiISKtz67Ti06ZN+8tA8+uvv7Jy5Uo++ugjli9fjsViYd++fSQmJrJu3ToGDhzY4HOrqqqoqqqqvV9cXExsbKymFZdjm2HAutmw6GGoLAKTBYbfgnHS3RR/vwL/s8+u/TBf+uNPVCcnEXTxxZi9vNxcePtR/ts6cl9+ibKVP2OLj6Prl19isv7F91P2aldv0g9PQU0ZmG1w8h1w2j1gayBciYiIyBFrF+sw5ebmkpube9g2CQkJXHHFFXz++ed1pth1OBxYLBYmTJjAO++806jX0zpMIn9SkgVf3wdbPnXd9w2Hsx6BgRPAbMFwOtk77iKqduzAEhJC8MQJBF95JdZgjZ2pj2EYlC1fTt5bb1O+Zo1ro9VK0CWXEHHvPVj8/Bp6ImxdCIsfhYK9rm2xw+D85yGid6vULiIicixqF4GpsZKTk+uMP0pPT2f06NF88sknDB06lJiYmEbtR4FJpB47FsG3D7imqgbo1B/OfRqj84kUfPgR+TNmUJOeDoDJ25vACy8g+Kqr8OrRw41Fty1lv/xC1v89SdX27a4NNhtB4y4k9KapeMR0bviJqWvg24cgZZXrvm8EnPVPGDjRNWmHiIiItJgOFZgO1thL8g6mwCTSAHu1ayHUZf+Bqv1TjB93EYz6F4ZfNMXffkve9OlUbdla+5TQ668j4p573FSw+xmGUdvrXf7rryRNuhqzjw9Bl19OyOSrsXXq1PCTC5Lg+8dg0zzXfas3nHw7nHQ7eDbQEyUiIiLNqrHZoF1M+iAiLczqASfdCgOucC2O+ts7sHkBbP8a0/C/EXjm7QSMGUP5L79SMGcOJd9/j/fxg2ufbs/JwVFYiGf37m78I1qeUVND6bJlFH4yD1tcLJ0efBAA7yFD6DRtGgHnnoMlMLDhHZTlwU/Pw+o3wFEFmGDgVXDmwxCg9eVERETaonbZw3Sk1MMk0kiZv8M3D7hm0wPwCnJNQDD0JvDwpSYjA2tEBCaLBYCcF18k99XX8OzTm8CxYwk491xsHWSBacMwqPz9d4q//JKiL77EkZcHgDkwkB4rlmPy8PjrnZTnw8oXYfWbrgkdABJPdy0sHNW/BasXERGRhnToS/KOlAKTSBMYBmz7EpY8DjnbXNt8I1zBacg14OFb2zTzX49T8PHHYLfXbvMeOBC/s87E/6yz8EhMrDNpS3uRP2cO+TNnUZOaWrvNEhZG0LgLCbz4kvrXUPqzkkxY9Rr8+jZU75+uPWoAjHgYuo+CdnhMREREOgoFpnooMIkcAacDfp8LPzwJhUmubT6hMOxmOPFG8HJdgmYvKKDk20UUf/UV5b/+6gpcgCUwkO4//Vg7rbZRXd24XplWZs/Pp/yXX/A77TTM+xfHzn7uefLeeAOTjw/+I0YQMHYMfqeeislmO/zO8na7epTWvw+Oate2Tv3hjAeg57kKSiIiIm2AAlM9FJhEjoK9GjZ8AD8+d2D6aw9/OP5q16V6wfG1TWuysild8j0l3y/BFtWJqMcfB1yXt+06YwTWyEh8Bg3Ee8AAvAcOxBoV1ao9UIZhUJOaSsXGjVSs30D56tVU7dgBQMyrr+B/pmuR2Ko9e6jasQO/009veKHZAzuF3Utck2fs+BbYf2qNHepaNLjHOQpKIiIibYgCUz0UmESagcPumhBixf8gZ/+seSYz9D4fTrgBEk6pEwz+PJtc1Z497Bkz9pBdWoKC8OzRg4CxYwm+fHzt87Db/7o35zAMpxN7Ti5mL8/ayRjK164l9dbbcBQUHNLes0cPwm6eSsC55zb+RcrzYeNHsGYG5O44sL37aFdQih9+xPWLiIhIy9EseSLSMixW6H8Z9L3E1aOy6hXXzy2fuW6h3WDwFBhwFfiG1uk58uzShW5Lvqd87W9UrF9Pxfr1VG7fjqOwkPJffsF7wIDatvbMTHaNHIU1NBRLSAjWkBAsQUGYvL0we3rhe+op+I8Y4Wqbk0Pua6/hKC7BUVKMs6gYe24uNVlZUFND+N13E3bjDa7yg4NxFBRgstnw7N0b73798DlhCD4nnog1JKRxx8Bhh73LYN1s1zivPy678/CHQRNcwTGsW7McbhEREXEvBSYROTJmM3Qf6bplbXFdivb7XNcCuIsehu8eg24jXeGqx7ng4RoXZIuOJjA6msDzzwPAWVlJ1e7dVO3YiWePA9OSV6ekgMOBPTsbe3Y2VQe/fGBAbWBylJRQ8P4H9ddpsWDPzam96xEXR8LHH+HZqxfmpoylMgxI+QU2feLqYSs7sE869XddmjjgCvD0b/w+RUREpM3TJXki0nyqSuD3T2DtLMhYf2C7hx/0GA09x7hmh/M6zFpF+/1xOZ09NwdHfgGO/DwchYU4K6twVlbge+KJ+A53Xe5mLyigYPZ7mP39sQQEYA7wxxoaii0qCmt4eO2EE01mr4K9y2H7V7D9GyhJP/CYd7Crl+34q10z34mIiEi7ojFM9VBgEmlF2dvg949dvU6FyQe2m22QcDJ0OcO1FlHUADBb3FZmHU4nZG1yXW63ZxkkrTywbhK4gl+vsdD3Uug6AixHPr5KRERE3EuBqR4KTCJuYBiQuga2f+ka7/PniRHAtShu7InQeQh0HgzRg8A3tHXqKk6HrM2QtgZSf4XUtVBVVLedf5RrKvCeYyDhVLB5tXxtIiIi0uIUmOqhwCTSBuTuhF3fu3px9v0IVcWHtvGNgPCerltwIgTGuG4B0a5L4axefz1Ft2FAZRGU57nGGxUmu9aRKkhyjbPK3uJ6/GA2X1cPWOLp0OV0iOyr6cBFREQ6IAWmeigwibQxDjtkbnD1QKWtdd3ydv318ywerp4pmzeYra6byQz2Ste4I3ulK4g57Yffj8nimtWv8/EQMwRiToCIPrrUTkRE5BigacVFpO2zWF2X4XUefGBbVanrsr2c7ZCzzdUzVJwGRalQkgmGwzWNd1l2417Dww98QiEo7sAtpIsrGIV1B6tny/xtIiIi0iEoMIlI2+Lp5+rx6Xz8oY8ZhmsmvsoiqCx09SY57eCocQUpq9eBm6e/KyhpzJGIiIgcBQUmEWk/TCbwCnDdiHV3NSIiInIMMLu7ABERERERkbZKgUlERERERKQBCkwiIiIiIiINUGASERERERFpgAKTiIiIiIhIAxSYREREREREGqDAJCIiIiIi0gAFJhERERERkQYoMImIiIiIiDRAgUlERERERKQBCkwiIiIiIiINUGASERERERFpgAKTiIiIiIhIAxSYREREREREGmB1dwGtyTAMAIqLi91ciYiIiIiIuNMfmeCPjNCQYyowlZSUABAbG+vmSkREREREpC0oKSkhMDCwwcdNxl9Fqg7E6XSSnp6Ov78/JpPJrbUUFxcTGxtLSkoKAQEBbq2lo9Ixblk6vi1Px7hl6fi2PB3jlqXj2/J0jFuWu4+vYRiUlJQQHR2N2dzwSKVjqofJbDYTExPj7jLqCAgI0P+ALUzHuGXp+LY8HeOWpePb8nSMW5aOb8vTMW5Z7jy+h+tZ+oMmfRAREREREWmAApOIiIiIiEgDFJjcxNPTk0cffRRPT093l9Jh6Ri3LB3flqdj3LJ0fFuejnHL0vFteTrGLau9HN9jatIHERERERGRplAPk4iIiIiISAMUmERERERERBqgwCQiIiIiItIABSYREREREZEGKDA1k1dffZXExES8vLwYPHgwK1asOGz7ZcuWMXjwYLy8vOjSpQuvv/76IW3mzZtHnz598PT0pE+fPixYsKClym8XmnKM58+fz6hRowgPDycgIIDhw4fz7bff1mkza9YsTCbTIbfKysqW/lPapKYc36VLl9Z77LZt21annd7DdTXlGE+ZMqXeY3zcccfVttF7+IDly5dz/vnnEx0djclk4tNPP/3L5+g83DRNPcY6DzdNU4+vzsNN19RjrPNw0zz11FOccMIJ+Pv7ExERwbhx49i+fftfPq89nIsVmJrBRx99xJ133slDDz3EunXrOPXUUzn33HNJTk6ut/3evXsZM2YMp556KuvWrePBBx/k9ttvZ968ebVtfv75Zy6//HImTZrEhg0bmDRpEuPHj2f16tWt9We1KU09xsuXL2fUqFF89dVXrF27lhEjRnD++eezbt26Ou0CAgLIyMioc/Py8mqNP6lNaerx/cP27dvrHLvu3bvXPqb3cF1NPcYvvPBCnWObkpJCSEgIl112WZ12eg+7lJWVMWDAAF5++eVGtdd5uOmaeox1Hm6aph7fP+g83HhNPcY6DzfNsmXL+Nvf/saqVatYvHgxdruds88+m7Kysgaf027OxYYctRNPPNGYOnVq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8vJyRowYgZ+fH8uXL+fHH3/Ez8+Pc845h+rqajf/NSIi0loUmERE5JgQEhLCiy++SM+ePbn22mvp2bMn5eXlPPjgg3Tv3p0HHngADw8PfvrpJwA+/PBDzGYzb7/9Nv369aN3797MnDmT5ORkli5d6t4/RkREWo3V3QWIiIi0huOOOw6z+cD3hJGRkfTt27f2vsViITQ0lOzsbADWrl3Lrl278Pf3r7OfyspKdu/e3TpFi4iI2ykwiYjIMcFms9W5bzKZ6t3mdDoBcDqdDB48mDlz5hyyr/Dw8JYrVERE2hQFJhERkXocf/zxfPTRR0RERBAQEODuckRExE00hklERKQeEyZMICwsjAsvvJAVK1awd+9eli1bxh133EFqaqq7yxMRkVaiwCQiIlIPHx8fli9fTlxcHBdffDG9e/fm2muvpaKiQj1OIiLHEC1cKyIiIiIi0gD1MImIiIiIiDRAgUlERERERKQBCkwiIiIiIiINUGASERERERFpgAKTiIiIiIhIAxSYREREREREGqDAJCIiIiIi0gAFJhERERERkQYoMImIiIiIiDRAgUlERERERKQBCkwiIiIiIiIN+H/fc8tjlJ2glQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Load the model and propagate a new trajectory with different initial conditions and longer time\n", - "model = torch.load(\"neural_ode_model_complete.pth\").to(device)\n", - "\n", - "# Generate a new trajectory\n", - "initial_state = torch.tensor([np.pi/2, 0.0], dtype=torch.float32).to(device)\n", - "t_new = torch.arange(0, 2, dt).to(device)\n", - "with torch.no_grad():\n", - " pred_trajectory = model(initial_state.unsqueeze(0), t_new, dt)\n", - " pred_trajectory = pred_trajectory.squeeze(0)\n", - "\n", - "from scipy.integrate import solve_ivp\n", - "# Generate true trajectory using analytical solution (rk4)\n", - "def pendulum_dynamics(t, state, L=1.0, g=9.81, m=1.0, b=0.1):\n", - " theta, theta_dot = state\n", - " theta_ddot = -g/L * np.sin(theta) - b*theta_dot\n", - " return [theta_dot, theta_ddot]\n", - "\n", - "true_trajectory = solve_ivp(pendulum_dynamics, [t_new[0].cpu().numpy(), t_new[-1].cpu().numpy()], initial_state.cpu().numpy(), t_eval=t_new.cpu().numpy(), method=\"RK45\").y.T\n", - "\n", - "\n", - "# Plot the new trajectory\n", - "plt.figure(figsize=(10, 5))\n", - "plt.plot(t_new.cpu().detach(), pred_trajectory[:, 0].cpu().detach(), label='Predicted θ')\n", - "plt.plot(t_new.cpu().detach(), pred_trajectory[:, 1].cpu().detach(), label='Predicted θ_dot')\n", - "plt.plot(t_new.cpu().detach(), true_trajectory[:, 0], '--', label='True θ')\n", - "plt.plot(t_new.cpu().detach(), true_trajectory[:, 1], '--', label='True θ_dot')\n", - "plt.xlabel('Time')\n", - "plt.ylabel('Angle (rad) / Angular velocity (rad/s)')\n", - "plt.legend()\n", - "plt.title('Predicted Trajectory')\n", - "plt.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/quadrotor_benchmark.cpp b/examples/quadrotor_benchmark.cpp deleted file mode 100644 index 28444c9d..00000000 --- a/examples/quadrotor_benchmark.cpp +++ /dev/null @@ -1,1577 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "cddp.hpp" -#include "matplot/matplot.h" - -// ACADOS includes (conditional compilation) -#ifdef CDDP_CPP_ACADOS_ENABLED -extern "C" { -#include "acados_solver_quadrotor.h" -#include "acados_c/ocp_nlp_interface.h" -} -#endif - -using namespace matplot; -namespace fs = std::filesystem; - -// Convert quaternion [qw, qx, qy, qz] to Euler angles (roll, pitch, yaw) -Eigen::Vector3d quaternionToEuler(double qw, double qx, double qy, double qz) -{ - // Roll (phi) - double sinr_cosp = 2.0 * (qw * qx + qy * qz); - double cosr_cosp = 1.0 - 2.0 * (qx * qx + qy * qy); - double phi = std::atan2(sinr_cosp, cosr_cosp); - - // Pitch (theta) - double sinp = 2.0 * (qw * qy - qz * qx); - double theta = (std::abs(sinp) >= 1.0) ? std::copysign(M_PI / 2.0, sinp) : std::asin(sinp); - - // Yaw (psi) - double siny_cosp = 2.0 * (qw * qz + qx * qy); - double cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz); - double psi = std::atan2(siny_cosp, cosy_cosp); - - return Eigen::Vector3d(phi, theta, psi); -} - -// Compute rotation matrix from a unit quaternion [qw, qx, qy, qz] -Eigen::Matrix3d getRotationMatrixFromQuaternion(double qw, double qx, double qy, double qz) -{ - Eigen::Matrix3d R; - R(0, 0) = 1 - 2 * (qy * qy + qz * qz); - R(0, 1) = 2 * (qx * qy - qz * qw); - R(0, 2) = 2 * (qx * qz + qy * qw); - - R(1, 0) = 2 * (qx * qy + qz * qw); - R(1, 1) = 1 - 2 * (qx * qx + qz * qz); - R(1, 2) = 2 * (qy * qz - qx * qw); - - R(2, 0) = 2 * (qx * qz - qy * qw); - R(2, 1) = 2 * (qy * qz + qx * qw); - R(2, 2) = 1 - 2 * (qx * qx + qy * qy); - return R; -} - -// Transform quadrotor frame points (motor positions) to world coordinates using quaternion -std::vector> transformQuadrotorFrame( - const Eigen::Vector3d &position, - const Eigen::Vector4d &quat, // [qw, qx, qy, qz] - double arm_length) -{ - // Motor positions in body frame - std::vector body_points = { - Eigen::Vector3d(arm_length, 0, 0), // Front - Eigen::Vector3d(0, arm_length, 0), // Right - Eigen::Vector3d(-arm_length, 0, 0), // Back - Eigen::Vector3d(0, -arm_length, 0) // Left - }; - - Eigen::Matrix3d R = getRotationMatrixFromQuaternion(quat[0], quat[1], quat[2], quat[3]); - - // Prepare return container - std::vector> world_points(3, std::vector()); - for (const auto &pt : body_points) - { - Eigen::Vector3d wp = position + R * pt; - world_points[0].push_back(wp.x()); - world_points[1].push_back(wp.y()); - world_points[2].push_back(wp.z()); - } - return world_points; -} - -int main() { - // -------------------------- - // 1. Shared problem setup - // -------------------------- - - // For quaternion-based quadrotor, state_dim = 13: - // [x, y, z, qw, qx, qy, qz, vx, vy, vz, omega_x, omega_y, omega_z] - int state_dim = 13; - int control_dim = 4; // [f1, f2, f3, f4] - int horizon = 400; - double timestep = 0.02; - - // Quadrotor parameters - double mass = 1.2; // 1.2 kg - double arm_length = 0.165; // 16.5 cm - Eigen::Matrix3d inertia_matrix = Eigen::Matrix3d::Zero(); - inertia_matrix(0, 0) = 7.782e-3; // Ixx - inertia_matrix(1, 1) = 7.782e-3; // Iyy - inertia_matrix(2, 2) = 1.439e-2; // Izz - - std::string integration_type = "rk4"; - - // Cost matrices - Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); - // penalize [x, y, z, qw, qx, qy, qz] more (the orientation/quaternion part) - Q(0, 0) = 1.0; - Q(1, 1) = 1.0; - Q(2, 2) = 1.0; - // Q(3, 3) = 1.0; - // Q(4, 4) = 1.0; - // Q(5, 5) = 1.0; - // Q(6, 6) = 1.0; - - Eigen::MatrixXd R = 0.01 * Eigen::MatrixXd::Identity(control_dim, control_dim); - - Eigen::MatrixXd Qf = Eigen::MatrixXd::Zero(state_dim, state_dim); - Qf(0, 0) = 1.0; - Qf(1, 1) = 1.0; - Qf(2, 2) = 1.0; - Qf(3, 3) = 1.0; - Qf(4, 4) = 1.0; - Qf(5, 5) = 1.0; - Qf(6, 6) = 1.0; - - // Figure-8 trajectory parameters - double figure8_scale = 3.0; // 3m - double constant_altitude = 2.0; // 2m - double total_time = horizon * timestep; - double omega = 2.0 * M_PI / total_time; // completes 1 cycle over the horizon - - std::vector figure8_reference_states; - figure8_reference_states.reserve(horizon + 1); - - for (int i = 0; i <= horizon; ++i) - { - double t = i * timestep; - double angle = omega * t; - - // Lemniscate of Gerono for (x, y) - // x = A cos(angle) - // y = A sin(angle)*cos(angle) - Eigen::VectorXd ref_state = Eigen::VectorXd::Zero(state_dim); - ref_state(0) = figure8_scale * std::cos(angle); - ref_state(1) = figure8_scale * std::sin(angle) * std::cos(angle); - ref_state(2) = constant_altitude; - - // Identity quaternion: [1, 0, 0, 0] - ref_state(3) = 1.0; - ref_state(4) = 0.0; - ref_state(5) = 0.0; - ref_state(6) = 0.0; - - figure8_reference_states.push_back(ref_state); - } - - // Hover at the starting point of the figure-8 - Eigen::VectorXd goal_state = Eigen::VectorXd::Zero(state_dim); - goal_state(0) = figure8_scale; // x - goal_state(2) = constant_altitude; - goal_state(3) = 1.0; // qw - - // Start the same figure-8 starting point - Eigen::VectorXd initial_state = Eigen::VectorXd::Zero(state_dim); - initial_state(0) = figure8_scale; - initial_state(2) = constant_altitude; - initial_state(3) = 1.0; - - // Control constraints - double min_force = 0.0; - double max_force = 4.0; - Eigen::VectorXd control_upper_bound = max_force * Eigen::VectorXd::Ones(control_dim); - Eigen::VectorXd control_lower_bound = min_force * Eigen::VectorXd::Ones(control_dim); - - // Initial trajectory guess (hover thrust) - double hover_thrust = mass * 9.81 / 4.0; - - // Create a directory for saving plots - const std::string plotDirectory = "../results/benchmark"; - if (!fs::exists(plotDirectory)) { - fs::create_directories(plotDirectory); - } - - // Helper function to create initial trajectory - auto createInitialTrajectory = [&]() { - std::vector X_init = figure8_reference_states; // Use reference as initial guess - std::vector U_init(horizon, hover_thrust * Eigen::VectorXd::Ones(control_dim)); - return std::make_pair(X_init, U_init); - }; - - auto [X_init, U_init] = createInitialTrajectory(); - - // -------------------------------------------------------- - // 2. Baseline #1: ASDDP - // -------------------------------------------------------- - std::cout << "Solving with ASDDP..." << std::endl; - - cddp::CDDPOptions options_asddp; - options_asddp.max_iterations = 1000; - options_asddp.verbose = true; - options_asddp.debug = false; - options_asddp.enable_parallel = false; - options_asddp.num_threads = 1; - options_asddp.tolerance = 1e-5; - options_asddp.acceptable_tolerance = 1e-4; - options_asddp.regularization.initial_value = 1e-1; - options_asddp.use_ilqr = true; - - cddp::CDDP solver_asddp( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, mass, inertia_matrix, arm_length, integration_type), - std::make_unique(Q, R, Qf, goal_state, figure8_reference_states, timestep), - options_asddp - ); - - solver_asddp.setInitialTrajectory(X_init, U_init); - - // Add constraints - solver_asddp.addPathConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, control_upper_bound)); - - // Solve for baseline #1 - auto start_time_asddp = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution sol_asddp = solver_asddp.solve(cddp::SolverType::ASDDP); - auto end_time_asddp = std::chrono::high_resolution_clock::now(); - auto solve_time_asddp = std::chrono::duration_cast(end_time_asddp - start_time_asddp).count(); - - auto X_asddp_sol = std::any_cast>(sol_asddp.at("state_trajectory")); - auto U_asddp_sol = std::any_cast>(sol_asddp.at("control_trajectory")); - double cost_asddp = std::any_cast(sol_asddp.at("final_objective")); - std::cout << "ASDDP Optimal Cost: " << cost_asddp << std::endl; - - // Extract data for plotting - std::vector x_asddp, y_asddp, z_asddp; - std::vector phi_asddp, theta_asddp, psi_asddp; - - for (size_t i = 0; i < X_asddp_sol.size(); ++i) - { - x_asddp.push_back(X_asddp_sol[i](0)); - y_asddp.push_back(X_asddp_sol[i](1)); - z_asddp.push_back(X_asddp_sol[i](2)); - - double qw = X_asddp_sol[i](3); - double qx = X_asddp_sol[i](4); - double qy = X_asddp_sol[i](5); - double qz = X_asddp_sol[i](6); - - Eigen::Vector3d euler = quaternionToEuler(qw, qx, qy, qz); - phi_asddp.push_back(euler(0)); - theta_asddp.push_back(euler(1)); - psi_asddp.push_back(euler(2)); - } - - // -------------------------------------------------------- - // 3. Baseline #2: LogDDP - // -------------------------------------------------------- - std::cout << "Solving with LogDDP..." << std::endl; - - cddp::CDDPOptions options_logddp; - options_logddp.max_iterations = 1000; - options_logddp.verbose = true; - options_logddp.debug = false; - options_logddp.tolerance = 1e-5; - options_logddp.acceptable_tolerance = 1e-6; - options_logddp.regularization.initial_value = 1e-4; - options_logddp.log_barrier.barrier.mu_initial = 1e-0; - options_logddp.log_barrier.barrier.mu_update_factor = 0.2; - options_logddp.log_barrier.relaxed_log_barrier_delta = 1e-5; - options_logddp.use_ilqr = true; - options_logddp.enable_parallel = false; - options_logddp.num_threads = 1; - - cddp::CDDP solver_logddp( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, mass, inertia_matrix, arm_length, integration_type), - std::make_unique(Q, R, Qf, goal_state, figure8_reference_states, timestep), - options_logddp - ); - - solver_logddp.setInitialTrajectory(X_init, U_init); - - // Add constraints for LogDDP - solver_logddp.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound, control_lower_bound)); - - // Solve for baseline #2: LogDDP - auto start_time_logddp = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution sol_logddp = solver_logddp.solve("LogDDP"); - auto end_time_logddp = std::chrono::high_resolution_clock::now(); - auto solve_time_logddp = std::chrono::duration_cast(end_time_logddp - start_time_logddp).count(); - - auto X_logddp_sol = std::any_cast>(sol_logddp.at("state_trajectory")); - auto U_logddp_sol = std::any_cast>(sol_logddp.at("control_trajectory")); - double cost_logddp = std::any_cast(sol_logddp.at("final_objective")); - std::cout << "LogDDP Optimal Cost: " << cost_logddp << std::endl; - - // Extract data for plotting - std::vector x_logddp, y_logddp, z_logddp; - std::vector phi_logddp, theta_logddp, psi_logddp; - - for (size_t i = 0; i < X_logddp_sol.size(); ++i) - { - x_logddp.push_back(X_logddp_sol[i](0)); - y_logddp.push_back(X_logddp_sol[i](1)); - z_logddp.push_back(X_logddp_sol[i](2)); - - double qw = X_logddp_sol[i](3); - double qx = X_logddp_sol[i](4); - double qy = X_logddp_sol[i](5); - double qz = X_logddp_sol[i](6); - - Eigen::Vector3d euler = quaternionToEuler(qw, qx, qy, qz); - phi_logddp.push_back(euler(0)); - theta_logddp.push_back(euler(1)); - psi_logddp.push_back(euler(2)); - } - - // -------------------------------------------------------- - // 4. Baseline #3: IPDDP - // -------------------------------------------------------- - std::cout << "Solving with IPDDP..." << std::endl; - - cddp::CDDPOptions options_ipddp; - options_ipddp.max_iterations = 100; - options_ipddp.verbose = true; - options_ipddp.debug = false; - options_ipddp.tolerance = 1e-6; - options_ipddp.acceptable_tolerance = 1e-7; - options_ipddp.regularization.initial_value = 1e-3; - options_ipddp.ipddp.barrier.mu_initial = 1e-1; - options_ipddp.ipddp.barrier.mu_update_factor = 0.5; - options_ipddp.ipddp.barrier.mu_update_power = 1.2; - options_ipddp.filter.merit_acceptance_threshold = 1e-4; - options_ipddp.filter.violation_acceptance_threshold = 1e-6; - options_ipddp.filter.max_violation_threshold = 1e+7; - options_ipddp.filter.min_violation_for_armijo_check = 1e-7; - options_ipddp.filter.armijo_constant = 1e-4; - options_ipddp.use_ilqr = true; - options_ipddp.enable_parallel = false; - options_ipddp.num_threads = 1; - - cddp::CDDP solver_ipddp( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, mass, inertia_matrix, arm_length, integration_type), - std::make_unique(Q, R, Qf, goal_state, figure8_reference_states, timestep), - options_ipddp - ); - - solver_ipddp.setInitialTrajectory(X_init, U_init); - - // Add constraints for IPDDP - solver_ipddp.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound, control_lower_bound)); - - // Solve for baseline #3: IPDDP - auto start_time_ipddp = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution sol_ipddp = solver_ipddp.solve(cddp::SolverType::IPDDP); - auto end_time_ipddp = std::chrono::high_resolution_clock::now(); - auto solve_time_ipddp = std::chrono::duration_cast(end_time_ipddp - start_time_ipddp).count(); - - auto X_ipddp_sol = std::any_cast>(sol_ipddp.at("state_trajectory")); - auto U_ipddp_sol = std::any_cast>(sol_ipddp.at("control_trajectory")); - double cost_ipddp = std::any_cast(sol_ipddp.at("final_objective")); - std::cout << "IPDDP Optimal Cost: " << cost_ipddp << std::endl; - - // Extract data for plotting - std::vector x_ipddp, y_ipddp, z_ipddp; - std::vector phi_ipddp, theta_ipddp, psi_ipddp; - - for (size_t i = 0; i < X_ipddp_sol.size(); ++i) - { - x_ipddp.push_back(X_ipddp_sol[i](0)); - y_ipddp.push_back(X_ipddp_sol[i](1)); - z_ipddp.push_back(X_ipddp_sol[i](2)); - - double qw = X_ipddp_sol[i](3); - double qx = X_ipddp_sol[i](4); - double qy = X_ipddp_sol[i](5); - double qz = X_ipddp_sol[i](6); - - Eigen::Vector3d euler = quaternionToEuler(qw, qx, qy, qz); - phi_ipddp.push_back(euler(0)); - theta_ipddp.push_back(euler(1)); - psi_ipddp.push_back(euler(2)); - } - - // -------------------------------------------------------- - // 5. Baseline #4: MSIPDDP - // -------------------------------------------------------- - std::cout << "Solving with MSIPDDP..." << std::endl; - - cddp::CDDPOptions options_msipddp; - options_msipddp.max_iterations = 100; - options_msipddp.verbose = true; - options_msipddp.debug = false; - options_msipddp.tolerance = 1e-6; - options_msipddp.acceptable_tolerance = 1e-7; - options_msipddp.regularization.initial_value = 1e-3; - options_msipddp.msipddp.barrier.mu_initial = 1e-1; - options_msipddp.msipddp.barrier.mu_update_factor = 0.5; - options_msipddp.msipddp.barrier.mu_update_power = 1.2; - options_msipddp.filter.merit_acceptance_threshold = 1e-4; - options_msipddp.filter.violation_acceptance_threshold = 1e-6; - options_msipddp.filter.max_violation_threshold = 1e+7; - options_msipddp.filter.min_violation_for_armijo_check = 1e-7; - options_msipddp.filter.armijo_constant = 1e-4; - options_msipddp.msipddp.segment_length = 2; - options_msipddp.msipddp.rollout_type = "nonlinear"; - options_msipddp.use_ilqr = true; - options_msipddp.enable_parallel = false; - options_msipddp.num_threads = 1; - options_msipddp.msipddp.use_controlled_rollout = false; - - cddp::CDDP solver_msipddp( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, mass, inertia_matrix, arm_length, integration_type), - std::make_unique(Q, R, Qf, goal_state, figure8_reference_states, timestep), - options_msipddp - ); - - solver_msipddp.setInitialTrajectory(X_init, U_init); - - // Add constraints for MSIPDDP - solver_msipddp.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound, control_lower_bound)); - - // Solve for baseline #4: MSIPDDP - auto start_time_msipddp = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution sol_msipddp = solver_msipddp.solve(cddp::SolverType::MSIPDDP); - auto end_time_msipddp = std::chrono::high_resolution_clock::now(); - auto solve_time_msipddp = std::chrono::duration_cast(end_time_msipddp - start_time_msipddp).count(); - - auto X_msipddp_sol = std::any_cast>(sol_msipddp.at("state_trajectory")); - auto U_msipddp_sol = std::any_cast>(sol_msipddp.at("control_trajectory")); - double cost_msipddp = std::any_cast(sol_msipddp.at("final_objective")); - std::cout << "MSIPDDP Optimal Cost: " << cost_msipddp << std::endl; - - // Extract data for plotting - std::vector x_msipddp, y_msipddp, z_msipddp; - std::vector phi_msipddp, theta_msipddp, psi_msipddp; - - for (size_t i = 0; i < X_msipddp_sol.size(); ++i) - { - x_msipddp.push_back(X_msipddp_sol[i](0)); - y_msipddp.push_back(X_msipddp_sol[i](1)); - z_msipddp.push_back(X_msipddp_sol[i](2)); - - double qw = X_msipddp_sol[i](3); - double qx = X_msipddp_sol[i](4); - double qy = X_msipddp_sol[i](5); - double qz = X_msipddp_sol[i](6); - - Eigen::Vector3d euler = quaternionToEuler(qw, qx, qy, qz); - phi_msipddp.push_back(euler(0)); - theta_msipddp.push_back(euler(1)); - psi_msipddp.push_back(euler(2)); - } - - // -------------------------------------------------------- - // 5. Baseline #5 & #6: IPOPT and SNOPT (using CasADi) - // -------------------------------------------------------- - // NOTE: Both solvers reuse the same NLP problem definition to avoid duplication - std::cout << "Solving with IPOPT..." << std::endl; - - std::vector X_ipopt_sol(horizon + 1, Eigen::VectorXd(state_dim)); - std::vector U_ipopt_sol(horizon, Eigen::VectorXd(control_dim)); - std::vector x_ipopt, y_ipopt, z_ipopt; - std::vector phi_ipopt, theta_ipopt, psi_ipopt; - double solve_time_ipopt_numeric = 0.0; - double cost_ipopt = 0.0; - - { // IPOPT specific scope - const int n_states = (horizon + 1) * state_dim; - const int n_controls = horizon * control_dim; - const int n_dec = n_states + n_controls; - - // Define symbolic variables for states and controls - casadi::MX X_casadi = casadi::MX::sym("X", n_states); - casadi::MX U_casadi = casadi::MX::sym("U", n_controls); - casadi::MX z = casadi::MX::vertcat({X_casadi, U_casadi}); - - // Helper lambdas to extract the state and control at time step t - auto X_t = [=](int t) -> casadi::MX { - return X_casadi(casadi::Slice(t * state_dim, (t + 1) * state_dim)); - }; - auto U_t = [=](int t) -> casadi::MX { - return U_casadi(casadi::Slice(t * control_dim, (t + 1) * control_dim)); - }; - - // Convert Eigen matrices to CasADi - casadi::DM Q_dm(Q.rows(), Q.cols()); - for (int i = 0; i < Q.rows(); i++) { - for (int j = 0; j < Q.cols(); j++) { - Q_dm(i, j) = Q(i, j) * timestep; - } - } - casadi::DM R_dm(R.rows(), R.cols()); - for (int i = 0; i < R.rows(); i++) { - for (int j = 0; j < R.cols(); j++) { - R_dm(i, j) = R(i, j) * timestep; - } - } - casadi::DM Qf_dm(Qf.rows(), Qf.cols()); - for (int i = 0; i < Qf.rows(); i++) { - for (int j = 0; j < Qf.cols(); j++) { - Qf_dm(i, j) = Qf(i, j); - } - } - - // Convert inertia matrix to CasADi - casadi::DM inertia_dm(3, 3); - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - inertia_dm(i, j) = inertia_matrix(i, j); - } - } - - // Quadrotor continuous dynamics function (computes derivatives) - auto quadrotor_derivatives = [=](casadi::MX x, casadi::MX u) -> casadi::MX { - casadi::MX x_dot = casadi::MX::zeros(state_dim, 1); - - // Extract states - casadi::MX pos = x(casadi::Slice(0, 3)); // [x, y, z] - casadi::MX quat = x(casadi::Slice(3, 7)); // [qw, qx, qy, qz] - casadi::MX vel = x(casadi::Slice(7, 10)); // [vx, vy, vz] - casadi::MX omega = x(casadi::Slice(10, 13)); // [omega_x, omega_y, omega_z] - - casadi::MX qw = quat(0), qx = quat(1), qy = quat(2), qz = quat(3); - casadi::MX omega_x = omega(0), omega_y = omega(1), omega_z = omega(2); - - // Normalize quaternion - casadi::MX q_norm = casadi::MX::sqrt(qw*qw + qx*qx + qy*qy + qz*qz); - qw = qw / q_norm; - qx = qx / q_norm; - qy = qy / q_norm; - qz = qz / q_norm; - - // Extract control inputs (motor forces) - casadi::MX f1 = u(0), f2 = u(1), f3 = u(2), f4 = u(3); - - // Compute total thrust and moments - casadi::MX thrust = f1 + f2 + f3 + f4; - casadi::MX tau_x = arm_length * (f1 - f3); - casadi::MX tau_y = arm_length * (f2 - f4); - casadi::MX tau_z = 0.1 * (f1 - f2 + f3 - f4); - - // Rotation matrix from quaternion - casadi::MX R11 = 1 - 2 * (qy * qy + qz * qz); - casadi::MX R12 = 2 * (qx * qy - qz * qw); - casadi::MX R13 = 2 * (qx * qz + qy * qw); - casadi::MX R21 = 2 * (qx * qy + qz * qw); - casadi::MX R22 = 1 - 2 * (qx * qx + qz * qz); - casadi::MX R23 = 2 * (qy * qz - qx * qw); - casadi::MX R31 = 2 * (qx * qz - qy * qw); - casadi::MX R32 = 2 * (qy * qz + qx * qw); - casadi::MX R33 = 1 - 2 * (qx * qx + qy * qy); - - // Position derivative = velocity - x_dot(casadi::Slice(0, 3)) = vel; - - // Quaternion derivative - x_dot(3) = -0.5 * (qx * omega_x + qy * omega_y + qz * omega_z); // qw_dot - x_dot(4) = 0.5 * (qw * omega_x + qy * omega_z - qz * omega_y); // qx_dot - x_dot(5) = 0.5 * (qw * omega_y - qx * omega_z + qz * omega_x); // qy_dot - x_dot(6) = 0.5 * (qw * omega_z + qx * omega_y - qy * omega_x); // qz_dot - - // Velocity derivative (thrust is applied along body z-axis) - casadi::MX thrust_world_x = R13 * thrust; - casadi::MX thrust_world_y = R23 * thrust; - casadi::MX thrust_world_z = R33 * thrust; - - x_dot(7) = thrust_world_x / mass; // vx_dot - x_dot(8) = thrust_world_y / mass; // vy_dot - x_dot(9) = thrust_world_z / mass - 9.81; // vz_dot - - // Angular velocity derivative - casadi::MX inertia_inv = casadi::MX::inv(inertia_dm); - casadi::MX tau_vec = casadi::MX::vertcat({tau_x, tau_y, tau_z}); - casadi::MX gyroscopic = casadi::MX::cross(omega, casadi::MX::mtimes(inertia_dm, omega)); - casadi::MX angular_acc = casadi::MX::mtimes(inertia_inv, tau_vec - gyroscopic); - - x_dot(casadi::Slice(10, 13)) = angular_acc; - - return x_dot; - }; - - // RK4 integration function - auto quadrotor_dynamics = [=](casadi::MX x, casadi::MX u) -> casadi::MX { - // RK4 integration: k1, k2, k3, k4 - casadi::MX k1 = quadrotor_derivatives(x, u); - casadi::MX k2 = quadrotor_derivatives(x + timestep/2.0 * k1, u); - casadi::MX k3 = quadrotor_derivatives(x + timestep/2.0 * k2, u); - casadi::MX k4 = quadrotor_derivatives(x + timestep * k3, u); - - // RK4 final integration step - return x + timestep/6.0 * (k1 + 2.0*k2 + 2.0*k3 + k4); - }; - - casadi::MX g; - - // Initial state constraint: X₀ = initial_state - casadi::DM init_state_dm(std::vector(initial_state.data(), initial_state.data() + state_dim)); - g = casadi::MX::vertcat({g, X_t(0) - init_state_dm}); - - // Dynamics constraints - for (int t = 0; t < horizon; t++) { - casadi::MX x_next_expr = quadrotor_dynamics(X_t(t), U_t(t)); - g = casadi::MX::vertcat({g, X_t(t + 1) - x_next_expr}); - } - - // Cost Function - casadi::MX cost = casadi::MX::zeros(1, 1); - - // Running cost - for (int t = 0; t < horizon; t++) { - // Convert reference state to CasADi - casadi::DM ref_dm(std::vector(figure8_reference_states[t].data(), - figure8_reference_states[t].data() + state_dim)); - - casadi::MX x_diff = X_t(t) - ref_dm; - casadi::MX u_diff = U_t(t); - - casadi::MX state_cost = casadi::MX::mtimes({x_diff.T(), Q_dm, x_diff}); - casadi::MX control_cost = casadi::MX::mtimes({u_diff.T(), R_dm, u_diff}); - cost = cost + state_cost + control_cost; - } - - // Terminal cost - casadi::DM goal_dm(std::vector(goal_state.data(), goal_state.data() + state_dim)); - casadi::MX x_diff_final = X_t(horizon) - goal_dm; - casadi::MX terminal_cost = casadi::MX::mtimes({x_diff_final.T(), Qf_dm, x_diff_final}); - cost = cost + terminal_cost; - - // Variable Bounds and Initial Guess - std::vector lbx(n_dec, -1e20); - std::vector ubx(n_dec, 1e20); - - // Apply control bounds - for (int t = 0; t < horizon; t++) { - for (int i = 0; i < control_dim; i++) { - lbx[n_states + t * control_dim + i] = control_lower_bound(i); - ubx[n_states + t * control_dim + i] = control_upper_bound(i); - } - } - - // The complete set of constraints (g) must be equal to zero - const int n_g = static_cast(g.size1()); - std::vector lbg(n_g, 0.0); - std::vector ubg(n_g, 0.0); - - // Provide an initial guess for the decision vector - std::vector x0(n_dec, 0.0); - - // Set the initial state portion - for (int i = 0; i < state_dim; i++) { - x0[i] = initial_state(i); - } - - // Use the reference trajectory as initial guess for states - for (int t = 1; t <= horizon; t++) { - for (int i = 0; i < state_dim; i++) { - x0[t * state_dim + i] = figure8_reference_states[t](i); - } - } - - // Initial guess for controls (hover thrust) - for (int t = 0; t < horizon; t++) { - for (int i = 0; i < control_dim; i++) { - x0[n_states + t * control_dim + i] = hover_thrust; - } - } - - // NLP Definition and IPOPT Solver Setup - std::map nlp; - nlp["x"] = z; - nlp["f"] = cost; - nlp["g"] = g; - - casadi::Dict solver_opts; - solver_opts["print_time"] = true; - solver_opts["ipopt.print_level"] = 0; - solver_opts["ipopt.max_iter"] = 1000; - solver_opts["ipopt.tol"] = 1e-6; - solver_opts["ipopt.acceptable_tol"] = 1e-4; - - // Create the NLP solver instance using IPOPT - casadi::Function solver = casadi::nlpsol("solver", "ipopt", nlp, solver_opts); - - // Convert the initial guess and bounds into DM objects - casadi::DM x0_dm = casadi::DM(x0); - casadi::DM lbx_dm = casadi::DM(lbx); - casadi::DM ubx_dm = casadi::DM(ubx); - casadi::DM lbg_dm = casadi::DM(lbg); - casadi::DM ubg_dm = casadi::DM(ubg); - - casadi::DMDict arg({ - {"x0", x0_dm}, - {"lbx", lbx_dm}, - {"ubx", ubx_dm}, - {"lbg", lbg_dm}, - {"ubg", ubg_dm} - }); - - // Solve the NLP - auto start_time_ipopt = std::chrono::high_resolution_clock::now(); - casadi::DMDict res = solver(arg); - auto end_time_ipopt = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end_time_ipopt - start_time_ipopt; - solve_time_ipopt_numeric = elapsed.count(); - - // Extract and Display the Solution - std::vector sol = std::vector(res.at("x")); - cost_ipopt = static_cast(casadi::DM(res.at("f"))); - - // Convert to state and control trajectories - for (int t = 0; t <= horizon; t++) { - for (int i = 0; i < state_dim; i++) { - X_ipopt_sol[t](i) = sol[t * state_dim + i]; - } - } - - for (int t = 0; t < horizon; t++) { - for (int i = 0; i < control_dim; i++) { - U_ipopt_sol[t](i) = sol[n_states + t * control_dim + i]; - } - } - - std::cout << "IPOPT Optimal Cost: " << cost_ipopt << std::endl; - std::cout << "IPOPT solve time: " << solve_time_ipopt_numeric << " seconds" << std::endl; - } - - // Extract data for plotting - for (size_t i = 0; i < X_ipopt_sol.size(); ++i) - { - x_ipopt.push_back(X_ipopt_sol[i](0)); - y_ipopt.push_back(X_ipopt_sol[i](1)); - z_ipopt.push_back(X_ipopt_sol[i](2)); - - double qw = X_ipopt_sol[i](3); - double qx = X_ipopt_sol[i](4); - double qy = X_ipopt_sol[i](5); - double qz = X_ipopt_sol[i](6); - - Eigen::Vector3d euler = quaternionToEuler(qw, qx, qy, qz); - phi_ipopt.push_back(euler(0)); - theta_ipopt.push_back(euler(1)); - psi_ipopt.push_back(euler(2)); - } - - // -------------------------------------------------------- - // SNOPT: Reusing the same NLP definition from IPOPT above - // -------------------------------------------------------- - std::cout << "Solving with SNOPT..." << std::endl; - - std::vector X_snopt_sol(horizon + 1, Eigen::VectorXd(state_dim)); - std::vector U_snopt_sol(horizon, Eigen::VectorXd(control_dim)); - std::vector x_snopt, y_snopt, z_snopt; - std::vector phi_snopt, theta_snopt, psi_snopt; - double solve_time_snopt_numeric = 0.0; - double cost_snopt = 0.0; - - { // SNOPT specific scope - const int n_states = (horizon + 1) * state_dim; - const int n_controls = horizon * control_dim; - const int n_dec = n_states + n_controls; - - // Define symbolic variables for states and controls - casadi::MX X_casadi = casadi::MX::sym("X", n_states); - casadi::MX U_casadi = casadi::MX::sym("U", n_controls); - casadi::MX z = casadi::MX::vertcat({X_casadi, U_casadi}); - - // Helper lambdas to extract the state and control at time step t - auto X_t = [=](int t) -> casadi::MX { - return X_casadi(casadi::Slice(t * state_dim, (t + 1) * state_dim)); - }; - auto U_t = [=](int t) -> casadi::MX { - return U_casadi(casadi::Slice(t * control_dim, (t + 1) * control_dim)); - }; - - // Convert Eigen matrices to CasADi - casadi::DM Q_dm(Q.rows(), Q.cols()); - for (int i = 0; i < Q.rows(); i++) { - for (int j = 0; j < Q.cols(); j++) { - Q_dm(i, j) = Q(i, j) * timestep; - } - } - casadi::DM R_dm(R.rows(), R.cols()); - for (int i = 0; i < R.rows(); i++) { - for (int j = 0; j < R.cols(); j++) { - R_dm(i, j) = R(i, j) * timestep; - } - } - casadi::DM Qf_dm(Qf.rows(), Qf.cols()); - for (int i = 0; i < Qf.rows(); i++) { - for (int j = 0; j < Qf.cols(); j++) { - Qf_dm(i, j) = Qf(i, j); - } - } - - // Convert inertia matrix to CasADi - casadi::DM inertia_dm(3, 3); - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - inertia_dm(i, j) = inertia_matrix(i, j); - } - } - - // Quadrotor continuous dynamics function (computes derivatives) - auto quadrotor_derivatives = [=](casadi::MX x, casadi::MX u) -> casadi::MX { - casadi::MX x_dot = casadi::MX::zeros(state_dim, 1); - - // Extract states - casadi::MX pos = x(casadi::Slice(0, 3)); // [x, y, z] - casadi::MX quat = x(casadi::Slice(3, 7)); // [qw, qx, qy, qz] - casadi::MX vel = x(casadi::Slice(7, 10)); // [vx, vy, vz] - casadi::MX omega = x(casadi::Slice(10, 13)); // [omega_x, omega_y, omega_z] - - casadi::MX qw = quat(0), qx = quat(1), qy = quat(2), qz = quat(3); - casadi::MX omega_x = omega(0), omega_y = omega(1), omega_z = omega(2); - - // Normalize quaternion - casadi::MX q_norm = casadi::MX::sqrt(qw*qw + qx*qx + qy*qy + qz*qz); - qw = qw / q_norm; - qx = qx / q_norm; - qy = qy / q_norm; - qz = qz / q_norm; - - // Extract control inputs (motor forces) - casadi::MX f1 = u(0), f2 = u(1), f3 = u(2), f4 = u(3); - - // Compute total thrust and moments - casadi::MX thrust = f1 + f2 + f3 + f4; - casadi::MX tau_x = arm_length * (f1 - f3); - casadi::MX tau_y = arm_length * (f2 - f4); - casadi::MX tau_z = 0.1 * (f1 - f2 + f3 - f4); - - // Rotation matrix from quaternion - casadi::MX R11 = 1 - 2 * (qy * qy + qz * qz); - casadi::MX R12 = 2 * (qx * qy - qz * qw); - casadi::MX R13 = 2 * (qx * qz + qy * qw); - casadi::MX R21 = 2 * (qx * qy + qz * qw); - casadi::MX R22 = 1 - 2 * (qx * qx + qz * qz); - casadi::MX R23 = 2 * (qy * qz - qx * qw); - casadi::MX R31 = 2 * (qx * qz - qy * qw); - casadi::MX R32 = 2 * (qy * qz + qx * qw); - casadi::MX R33 = 1 - 2 * (qx * qx + qy * qy); - - // Position derivative = velocity - x_dot(casadi::Slice(0, 3)) = vel; - - // Quaternion derivative - x_dot(3) = -0.5 * (qx * omega_x + qy * omega_y + qz * omega_z); // qw_dot - x_dot(4) = 0.5 * (qw * omega_x + qy * omega_z - qz * omega_y); // qx_dot - x_dot(5) = 0.5 * (qw * omega_y - qx * omega_z + qz * omega_x); // qy_dot - x_dot(6) = 0.5 * (qw * omega_z + qx * omega_y - qy * omega_x); // qz_dot - - // Velocity derivative (thrust is applied along body z-axis) - casadi::MX thrust_world_x = R13 * thrust; - casadi::MX thrust_world_y = R23 * thrust; - casadi::MX thrust_world_z = R33 * thrust; - - x_dot(7) = thrust_world_x / mass; // vx_dot - x_dot(8) = thrust_world_y / mass; // vy_dot - x_dot(9) = thrust_world_z / mass - 9.81; // vz_dot - - // Angular velocity derivative - casadi::MX inertia_inv = casadi::MX::inv(inertia_dm); - casadi::MX tau_vec = casadi::MX::vertcat({tau_x, tau_y, tau_z}); - casadi::MX gyroscopic = casadi::MX::cross(omega, casadi::MX::mtimes(inertia_dm, omega)); - casadi::MX angular_acc = casadi::MX::mtimes(inertia_inv, tau_vec - gyroscopic); - - x_dot(casadi::Slice(10, 13)) = angular_acc; - - return x_dot; - }; - - // RK4 integration function - auto quadrotor_dynamics = [=](casadi::MX x, casadi::MX u) -> casadi::MX { - // RK4 integration: k1, k2, k3, k4 - casadi::MX k1 = quadrotor_derivatives(x, u); - casadi::MX k2 = quadrotor_derivatives(x + timestep/2.0 * k1, u); - casadi::MX k3 = quadrotor_derivatives(x + timestep/2.0 * k2, u); - casadi::MX k4 = quadrotor_derivatives(x + timestep * k3, u); - - // RK4 final integration step - return x + timestep/6.0 * (k1 + 2.0*k2 + 2.0*k3 + k4); - }; - - casadi::MX g; - - // Initial state constraint: X₀ = initial_state - casadi::DM init_state_dm(std::vector(initial_state.data(), initial_state.data() + state_dim)); - g = casadi::MX::vertcat({g, X_t(0) - init_state_dm}); - - // Dynamics constraints - for (int t = 0; t < horizon; t++) { - casadi::MX x_next_expr = quadrotor_dynamics(X_t(t), U_t(t)); - g = casadi::MX::vertcat({g, X_t(t + 1) - x_next_expr}); - } - - // Cost Function - casadi::MX cost = casadi::MX::zeros(1, 1); - - // Running cost - for (int t = 0; t < horizon; t++) { - // Convert reference state to CasADi - casadi::DM ref_dm(std::vector(figure8_reference_states[t].data(), - figure8_reference_states[t].data() + state_dim)); - - casadi::MX x_diff = X_t(t) - ref_dm; - casadi::MX u_diff = U_t(t); - - casadi::MX state_cost = casadi::MX::mtimes({x_diff.T(), Q_dm, x_diff}); - casadi::MX control_cost = casadi::MX::mtimes({u_diff.T(), R_dm, u_diff}); - cost = cost + state_cost + control_cost; - } - - // Terminal cost - casadi::DM goal_dm(std::vector(goal_state.data(), goal_state.data() + state_dim)); - casadi::MX x_diff_final = X_t(horizon) - goal_dm; - casadi::MX terminal_cost = casadi::MX::mtimes({x_diff_final.T(), Qf_dm, x_diff_final}); - cost = cost + terminal_cost; - - // Variable Bounds and Initial Guess - std::vector lbx(n_dec, -1e20); - std::vector ubx(n_dec, 1e20); - - // Apply control bounds - for (int t = 0; t < horizon; t++) { - for (int i = 0; i < control_dim; i++) { - lbx[n_states + t * control_dim + i] = control_lower_bound(i); - ubx[n_states + t * control_dim + i] = control_upper_bound(i); - } - } - - // The complete set of constraints (g) must be equal to zero - const int n_g = static_cast(g.size1()); - std::vector lbg(n_g, 0.0); - std::vector ubg(n_g, 0.0); - - // Provide an initial guess for the decision vector - std::vector x0(n_dec, 0.0); - - // Set the initial state portion - for (int i = 0; i < state_dim; i++) { - x0[i] = initial_state(i); - } - - // Use the reference trajectory as initial guess for states - for (int t = 1; t <= horizon; t++) { - for (int i = 0; i < state_dim; i++) { - x0[t * state_dim + i] = figure8_reference_states[t](i); - } - } - - // Initial guess for controls (hover thrust) - for (int t = 0; t < horizon; t++) { - for (int i = 0; i < control_dim; i++) { - x0[n_states + t * control_dim + i] = hover_thrust; - } - } - - // NLP Definition and SNOPT Solver Setup - std::map nlp; - nlp["x"] = z; - nlp["f"] = cost; - nlp["g"] = g; - - casadi::Dict solver_opts; - // Basic SNOPT options - solver_opts["print_time"] = true; - solver_opts["snopt.print_level"] = 0; // Reduce output for speed - solver_opts["snopt.major_iterations_limit"] = 50; // Even more aggressive - solver_opts["snopt.minor_iterations_limit"] = 100; - - // More relaxed tolerances for speed - solver_opts["snopt.major_optimality_tolerance"] = 1e-4; - solver_opts["snopt.major_feasibility_tolerance"] = 1e-4; - solver_opts["snopt.minor_feasibility_tolerance"] = 1e-4; - - // Aggressive step control for faster convergence - solver_opts["snopt.linesearch_tolerance"] = 0.1; - solver_opts["snopt.major_step_limit"] = 10.0; - - // Reduce superbasics limit to encourage faster convergence - solver_opts["snopt.superbasics_limit"] = 1000; - - // Create the NLP solver instance using SNOPT - casadi::Function solver = casadi::nlpsol("solver", "snopt", nlp, solver_opts); - - // Convert the initial guess and bounds into DM objects - casadi::DM x0_dm = casadi::DM(x0); - casadi::DM lbx_dm = casadi::DM(lbx); - casadi::DM ubx_dm = casadi::DM(ubx); - casadi::DM lbg_dm = casadi::DM(lbg); - casadi::DM ubg_dm = casadi::DM(ubg); - - casadi::DMDict arg({ - {"x0", x0_dm}, - {"lbx", lbx_dm}, - {"ubx", ubx_dm}, - {"lbg", lbg_dm}, - {"ubg", ubg_dm} - }); - - // Solve the NLP - auto start_time_snopt = std::chrono::high_resolution_clock::now(); - casadi::DMDict res = solver(arg); - auto end_time_snopt = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end_time_snopt - start_time_snopt; - solve_time_snopt_numeric = elapsed.count(); - - // Extract and Display the Solution - std::vector sol = std::vector(res.at("x")); - cost_snopt = static_cast(casadi::DM(res.at("f"))); - - // Convert to state and control trajectories - for (int t = 0; t <= horizon; t++) { - for (int i = 0; i < state_dim; i++) { - X_snopt_sol[t](i) = sol[t * state_dim + i]; - } - } - - for (int t = 0; t < horizon; t++) { - for (int i = 0; i < control_dim; i++) { - U_snopt_sol[t](i) = sol[n_states + t * control_dim + i]; - } - } - - std::cout << "SNOPT Optimal Cost: " << cost_snopt << std::endl; - std::cout << "SNOPT solve time: " << solve_time_snopt_numeric << " seconds" << std::endl; - } - - // Extract data for plotting - for (size_t i = 0; i < X_snopt_sol.size(); ++i) - { - x_snopt.push_back(X_snopt_sol[i](0)); - y_snopt.push_back(X_snopt_sol[i](1)); - z_snopt.push_back(X_snopt_sol[i](2)); - - double qw = X_snopt_sol[i](3); - double qx = X_snopt_sol[i](4); - double qy = X_snopt_sol[i](5); - double qz = X_snopt_sol[i](6); - - Eigen::Vector3d euler = quaternionToEuler(qw, qx, qy, qz); - phi_snopt.push_back(euler(0)); - theta_snopt.push_back(euler(1)); - psi_snopt.push_back(euler(2)); - } - - // -------------------------------------------------------- - // 7. ACADOS Solver (using C interface) - // -------------------------------------------------------- - std::cout << "Solving with ACADOS..." << std::endl; - - std::vector X_acados_sol(horizon + 1, Eigen::VectorXd(state_dim)); - std::vector U_acados_sol(horizon, Eigen::VectorXd(control_dim)); - std::vector x_acados, y_acados, z_acados; - std::vector phi_acados, theta_acados, psi_acados; - double solve_time_acados_numeric = 0.0; - double cost_acados = 0.0; - -#ifdef CDDP_CPP_ACADOS_ENABLED - try { // ACADOS specific scope with exception handling - std::cout << "ACADOS: Using generated solver" << std::endl; - - const int N = horizon; - - auto start_time_acados = std::chrono::high_resolution_clock::now(); - - std::cout << "ACADOS: Creating solver capsule..." << std::endl; - // Create solver capsule - quadrotor_solver_capsule *capsule = quadrotor_acados_create_capsule(); - int status = quadrotor_acados_create(capsule); - - if (status) { - std::cerr << "ACADOS solver creation failed with status " << status << std::endl; - quadrotor_acados_free_capsule(capsule); - throw std::runtime_error("Failed to create ACADOS solver"); - } - - // Get internal structures - ocp_nlp_config *nlp_config = quadrotor_acados_get_nlp_config(capsule); - ocp_nlp_dims *nlp_dims = quadrotor_acados_get_nlp_dims(capsule); - ocp_nlp_in *nlp_in = quadrotor_acados_get_nlp_in(capsule); - ocp_nlp_out *nlp_out = quadrotor_acados_get_nlp_out(capsule); - - // Set initial state constraint - double x0[13]; - for (int i = 0; i < 13; i++) { - x0[i] = initial_state(i); - } - ocp_nlp_constraints_model_set(nlp_config, nlp_dims, nlp_in, nlp_out, 0, "lbx", x0); - ocp_nlp_constraints_model_set(nlp_config, nlp_dims, nlp_in, nlp_out, 0, "ubx", x0); - - std::cout << "ACADOS: Setting reference trajectory..." << std::endl; - // Set reference trajectory - for (int i = 0; i < N; i++) { - double yref[17]; // [u(4); x(13)] - // Control reference is zero (penalize u'*R*u not (u-u_ref)'*R*(u-u_ref)) - for (int j = 0; j < 4; j++) { - yref[j] = 0.0; - } - // State reference from figure-8 - for (int j = 0; j < 13; j++) { - yref[4 + j] = figure8_reference_states[i](j); - } - ocp_nlp_cost_model_set(nlp_config, nlp_dims, nlp_in, i, "yref", yref); - } - - // Terminal reference (use goal_state, not figure8_reference_states[N]) - double yref_e[13]; - for (int j = 0; j < 13; j++) { - yref_e[j] = goal_state(j); - } - ocp_nlp_cost_model_set(nlp_config, nlp_dims, nlp_in, N, "yref", yref_e); - - std::cout << "ACADOS: Initializing trajectory..." << std::endl; - // Initialize trajectory - for (int i = 0; i <= N; i++) { - double x_init_stage[13]; - for (int j = 0; j < 13; j++) { - x_init_stage[j] = X_init[i](j); - } - ocp_nlp_out_set(nlp_config, nlp_dims, nlp_out, nlp_in, i, "x", x_init_stage); - } - - for (int i = 0; i < N; i++) { - double u_init_stage[4]; - for (int j = 0; j < 4; j++) { - u_init_stage[j] = U_init[i](j); - } - ocp_nlp_out_set(nlp_config, nlp_dims, nlp_out, nlp_in, i, "u", u_init_stage); - } - - std::cout << "ACADOS: Solving..." << std::endl; - // Solve - status = quadrotor_acados_solve(capsule); - - auto end_time_acados = std::chrono::high_resolution_clock::now(); - solve_time_acados_numeric = std::chrono::duration(end_time_acados - start_time_acados).count(); - - // Extract solution - for (int i = 0; i <= N; i++) { - double x_sol[13]; - ocp_nlp_out_get(nlp_config, nlp_dims, nlp_out, i, "x", x_sol); - for (int j = 0; j < 13; j++) { - X_acados_sol[i](j) = x_sol[j]; - } - } - - for (int i = 0; i < N; i++) { - double u_sol[4]; - ocp_nlp_out_get(nlp_config, nlp_dims, nlp_out, i, "u", u_sol); - for (int j = 0; j < 4; j++) { - U_acados_sol[i](j) = u_sol[j]; - } - } - - // Compute cost by evaluating objective function (matching CDDP cost computation) - cost_acados = 0.0; - for (int i = 0; i <= N; i++) { - if (i < N) { - Eigen::VectorXd x_err = X_acados_sol[i] - figure8_reference_states[i]; - // Note: Other solvers penalize u'*R*u, not (u-u_ref)'*R*(u-u_ref) - // Multiply by timestep to match IPOPT/SNOPT cost scaling - cost_acados += timestep * (x_err.transpose() * Q * x_err + U_acados_sol[i].transpose() * R * U_acados_sol[i]).value(); - } else { - // Terminal cost uses goal_state, not figure8_reference_states[N] - Eigen::VectorXd x_err = X_acados_sol[i] - goal_state; - cost_acados += (x_err.transpose() * Qf * x_err).value(); - } - } - - // Get solver statistics - ocp_nlp_solver *solver = quadrotor_acados_get_nlp_solver(capsule); - int sqp_iter; - ocp_nlp_get(solver, "sqp_iter", &sqp_iter); - - double time_tot; - ocp_nlp_get(solver, "time_tot", &time_tot); - - std::cout << "ACADOS status: " << status << " (0 = success, 1 = max iter, 2 = unbounded, 3 = timeout, 4 = NaN/Inf)" << std::endl; - std::cout << "ACADOS iterations: " << sqp_iter << std::endl; - std::cout << "ACADOS Optimal Cost: " << cost_acados << std::endl; - std::cout << "ACADOS solve time: " << solve_time_acados_numeric << " seconds" << std::endl; - - // Cleanup - quadrotor_acados_free(capsule); - quadrotor_acados_free_capsule(capsule); - - } catch (const std::exception& e) { - std::cerr << "ACADOS error: " << e.what() << std::endl; - std::cerr << "Using fallback solution" << std::endl; - - // Use initial trajectory as fallback - X_acados_sol = X_init; - U_acados_sol = U_init; - - // Calculate cost manually for placeholder - cost_acados = 0.0; - for (int t = 0; t < horizon; ++t) { - Eigen::VectorXd x_diff = X_acados_sol[t] - figure8_reference_states[t]; - cost_acados += timestep * (x_diff.transpose() * Q * x_diff).value(); - cost_acados += timestep * (U_acados_sol[t].transpose() * R * U_acados_sol[t]).value(); - } - Eigen::VectorXd x_final_diff = X_acados_sol[horizon] - goal_state; - cost_acados += (x_final_diff.transpose() * Qf * x_final_diff).value(); - - solve_time_acados_numeric = 0.001; // 1ms fallback time - } -#else - { // ACADOS not available - use initial trajectory - std::cout << "ACADOS not available. Using initial trajectory as placeholder." << std::endl; - X_acados_sol = X_init; - U_acados_sol = U_init; - - // Calculate cost manually for placeholder - cost_acados = 0.0; - for (int t = 0; t < horizon; ++t) { - Eigen::VectorXd x_diff = X_acados_sol[t] - figure8_reference_states[t]; - cost_acados += timestep * (x_diff.transpose() * Q * x_diff).value(); - cost_acados += timestep * (U_acados_sol[t].transpose() * R * U_acados_sol[t]).value(); - } - Eigen::VectorXd x_final_diff = X_acados_sol[horizon] - goal_state; - cost_acados += (x_final_diff.transpose() * Qf * x_final_diff).value(); - - std::cout << "ACADOS Optimal Cost: " << cost_acados << std::endl; - std::cout << "ACADOS solve time: " << solve_time_acados_numeric << " seconds" << std::endl; - } -#endif - - // Extract data for plotting - for (size_t i = 0; i < X_acados_sol.size(); ++i) - { - x_acados.push_back(X_acados_sol[i](0)); - y_acados.push_back(X_acados_sol[i](1)); - z_acados.push_back(X_acados_sol[i](2)); - - double qw = X_acados_sol[i](3); - double qx = X_acados_sol[i](4); - double qy = X_acados_sol[i](5); - double qz = X_acados_sol[i](6); - - Eigen::Vector3d euler = quaternionToEuler(qw, qx, qy, qz); - phi_acados.push_back(euler(0)); - theta_acados.push_back(euler(1)); - psi_acados.push_back(euler(2)); - } - - // -------------------------------------------------------- - // 8. Reference trajectory for comparison - // -------------------------------------------------------- - std::vector x_ref, y_ref, z_ref; - for (const auto& ref_state : figure8_reference_states) { - x_ref.push_back(ref_state(0)); - y_ref.push_back(ref_state(1)); - z_ref.push_back(ref_state(2)); - } - - // -------------------------------------------------------- - // 8. Plot all trajectories comparison - // -------------------------------------------------------- - auto main_figure = figure(true); - main_figure->size(3600, 800); // 1x6 layout - - // --- Subplot 1: ASDDP --- - auto ax_asddp = subplot(1, 6, 0); - - auto traj3d_asddp = plot3(ax_asddp, x_asddp, y_asddp, z_asddp); - traj3d_asddp->display_name("ASDDP Trajectory"); - traj3d_asddp->line_style("-"); - traj3d_asddp->line_width(2); - traj3d_asddp->color("blue"); - - hold(ax_asddp, true); - - // Reference trajectory - auto ref3d_asddp = plot3(ax_asddp, x_ref, y_ref, z_ref); - ref3d_asddp->display_name("Reference"); - ref3d_asddp->line_style("--"); - ref3d_asddp->line_width(1); - ref3d_asddp->color("red"); - - // Project trajectory onto x-y plane at z=0 - auto proj_xy_asddp = plot3(ax_asddp, x_asddp, y_asddp, std::vector(x_asddp.size(), 0.0)); - proj_xy_asddp->display_name("X-Y Projection"); - proj_xy_asddp->line_style("--"); - proj_xy_asddp->line_width(1); - proj_xy_asddp->color("gray"); - - xlabel(ax_asddp, "X [m]"); - ylabel(ax_asddp, "Y [m]"); - zlabel(ax_asddp, "Z [m]"); - xlim(ax_asddp, {-4, 4}); - ylim(ax_asddp, {-2, 2}); - zlim(ax_asddp, {0, 4}); - title(ax_asddp, "ASDDP"); - auto leg_asddp = matplot::legend(ax_asddp); - leg_asddp->location(legend::general_alignment::topleft); - grid(ax_asddp, true); - - // --- Subplot 2: LogDDP --- - auto ax_logddp = subplot(1, 6, 1); - - auto traj3d_logddp = plot3(ax_logddp, x_logddp, y_logddp, z_logddp); - traj3d_logddp->display_name("LogDDP Trajectory"); - traj3d_logddp->line_style("-"); - traj3d_logddp->line_width(2); - traj3d_logddp->color("blue"); - - hold(ax_logddp, true); - - auto ref3d_logddp = plot3(ax_logddp, x_ref, y_ref, z_ref); - ref3d_logddp->display_name("Reference"); - ref3d_logddp->line_style("--"); - ref3d_logddp->line_width(1); - ref3d_logddp->color("red"); - - auto proj_xy_logddp = plot3(ax_logddp, x_logddp, y_logddp, std::vector(x_logddp.size(), 0.0)); - proj_xy_logddp->display_name("X-Y Projection"); - proj_xy_logddp->line_style("--"); - proj_xy_logddp->line_width(1); - proj_xy_logddp->color("gray"); - - xlabel(ax_logddp, "X [m]"); - ylabel(ax_logddp, "Y [m]"); - zlabel(ax_logddp, "Z [m]"); - xlim(ax_logddp, {-4, 4}); - ylim(ax_logddp, {-2, 2}); - zlim(ax_logddp, {0, 4}); - title(ax_logddp, "LogDDP"); - auto leg_logddp = matplot::legend(ax_logddp); - leg_logddp->location(legend::general_alignment::topleft); - grid(ax_logddp, true); - - // --- Subplot 3: IPDDP --- - auto ax_ipddp = subplot(1, 6, 2); - - auto traj3d_ipddp = plot3(ax_ipddp, x_ipddp, y_ipddp, z_ipddp); - traj3d_ipddp->display_name("IPDDP Trajectory"); - traj3d_ipddp->line_style("-"); - traj3d_ipddp->line_width(2); - traj3d_ipddp->color("blue"); - - hold(ax_ipddp, true); - - auto ref3d_ipddp = plot3(ax_ipddp, x_ref, y_ref, z_ref); - ref3d_ipddp->display_name("Reference"); - ref3d_ipddp->line_style("--"); - ref3d_ipddp->line_width(1); - ref3d_ipddp->color("red"); - - auto proj_xy_ipddp = plot3(ax_ipddp, x_ipddp, y_ipddp, std::vector(x_ipddp.size(), 0.0)); - proj_xy_ipddp->display_name("X-Y Projection"); - proj_xy_ipddp->line_style("--"); - proj_xy_ipddp->line_width(1); - proj_xy_ipddp->color("gray"); - - xlabel(ax_ipddp, "X [m]"); - ylabel(ax_ipddp, "Y [m]"); - zlabel(ax_ipddp, "Z [m]"); - xlim(ax_ipddp, {-4, 4}); - ylim(ax_ipddp, {-2, 2}); - zlim(ax_ipddp, {0, 4}); - title(ax_ipddp, "IPDDP"); - auto leg_ipddp = matplot::legend(ax_ipddp); - leg_ipddp->location(legend::general_alignment::topleft); - grid(ax_ipddp, true); - - // --- Subplot 4: MSIPDDP --- - auto ax_msipddp = subplot(1, 6, 3); - - auto traj3d_msipddp = plot3(ax_msipddp, x_msipddp, y_msipddp, z_msipddp); - traj3d_msipddp->display_name("MSIPDDP Trajectory"); - traj3d_msipddp->line_style("-"); - traj3d_msipddp->line_width(2); - traj3d_msipddp->color("blue"); - - hold(ax_msipddp, true); - - auto ref3d_msipddp = plot3(ax_msipddp, x_ref, y_ref, z_ref); - ref3d_msipddp->display_name("Reference"); - ref3d_msipddp->line_style("--"); - ref3d_msipddp->line_width(1); - ref3d_msipddp->color("red"); - - auto proj_xy_msipddp = plot3(ax_msipddp, x_msipddp, y_msipddp, std::vector(x_msipddp.size(), 0.0)); - proj_xy_msipddp->display_name("X-Y Projection"); - proj_xy_msipddp->line_style("--"); - proj_xy_msipddp->line_width(1); - proj_xy_msipddp->color("gray"); - - xlabel(ax_msipddp, "X [m]"); - ylabel(ax_msipddp, "Y [m]"); - zlabel(ax_msipddp, "Z [m]"); - xlim(ax_msipddp, {-4, 4}); - ylim(ax_msipddp, {-2, 2}); - zlim(ax_msipddp, {0, 4}); - title(ax_msipddp, "MSIPDDP"); - auto leg_msipddp = matplot::legend(ax_msipddp); - leg_msipddp->location(legend::general_alignment::topleft); - grid(ax_msipddp, true); - - // --- Subplot 5: IPOPT --- - auto ax_ipopt = subplot(1, 6, 4); - - auto traj3d_ipopt = plot3(ax_ipopt, x_ipopt, y_ipopt, z_ipopt); - traj3d_ipopt->display_name("IPOPT Trajectory"); - traj3d_ipopt->line_style("-"); - traj3d_ipopt->line_width(2); - traj3d_ipopt->color("blue"); - - hold(ax_ipopt, true); - - auto ref3d_ipopt = plot3(ax_ipopt, x_ref, y_ref, z_ref); - ref3d_ipopt->display_name("Reference"); - ref3d_ipopt->line_style("--"); - ref3d_ipopt->line_width(1); - ref3d_ipopt->color("red"); - - auto proj_xy_ipopt = plot3(ax_ipopt, x_ipopt, y_ipopt, std::vector(x_ipopt.size(), 0.0)); - proj_xy_ipopt->display_name("X-Y Projection"); - proj_xy_ipopt->line_style("--"); - proj_xy_ipopt->line_width(1); - proj_xy_ipopt->color("gray"); - - xlabel(ax_ipopt, "X [m]"); - ylabel(ax_ipopt, "Y [m]"); - zlabel(ax_ipopt, "Z [m]"); - xlim(ax_ipopt, {-4, 4}); - ylim(ax_ipopt, {-2, 2}); - zlim(ax_ipopt, {0, 4}); - title(ax_ipopt, "IPOPT"); - auto leg_ipopt = matplot::legend(ax_ipopt); - leg_ipopt->location(legend::general_alignment::topleft); - grid(ax_ipopt, true); - - // --- Subplot 6: SNOPT --- - auto ax_snopt = subplot(1, 6, 5); - - auto traj3d_snopt = plot3(ax_snopt, x_snopt, y_snopt, z_snopt); - traj3d_snopt->display_name("SNOPT Trajectory"); - traj3d_snopt->line_style("-"); - traj3d_snopt->line_width(2); - traj3d_snopt->color("blue"); - - hold(ax_snopt, true); - - auto ref3d_snopt = plot3(ax_snopt, x_ref, y_ref, z_ref); - ref3d_snopt->display_name("Reference"); - ref3d_snopt->line_style("--"); - ref3d_snopt->line_width(1); - ref3d_snopt->color("red"); - - auto proj_xy_snopt = plot3(ax_snopt, x_snopt, y_snopt, std::vector(x_snopt.size(), 0.0)); - proj_xy_snopt->display_name("X-Y Projection"); - proj_xy_snopt->line_style("--"); - proj_xy_snopt->line_width(1); - proj_xy_snopt->color("gray"); - - xlabel(ax_snopt, "X [m]"); - ylabel(ax_snopt, "Y [m]"); - zlabel(ax_snopt, "Z [m]"); - xlim(ax_snopt, {-4, 4}); - ylim(ax_snopt, {-2, 2}); - zlim(ax_snopt, {0, 4}); - title(ax_snopt, "SNOPT"); - auto leg_snopt = matplot::legend(ax_snopt); - leg_snopt->location(legend::general_alignment::topleft); - grid(ax_snopt, true); - - main_figure->draw(); - main_figure->save(plotDirectory + "/quadrotor_lemniscate_3d_comparison.png"); - std::cout << "Saved combined 3D trajectory plot to " - << (plotDirectory + "/quadrotor_lemniscate_3d_comparison.png") << std::endl; - - // -------------------------------------------------------- - // 9. Plot computation times - // -------------------------------------------------------- - auto time_figure = figure(true); - time_figure->size(1200, 600); - - std::vector solve_times = { - solve_time_asddp / 1000000.0, // Convert to seconds - solve_time_logddp / 1000000.0, // Convert to seconds - solve_time_ipddp / 1000000.0, // Convert to seconds - solve_time_msipddp / 1000000.0, // Convert to seconds - solve_time_ipopt_numeric, // Already in seconds - solve_time_snopt_numeric, // Already in seconds - solve_time_acados_numeric // Already in seconds - }; - - std::vector solver_names = { - "ASDDP", "LogDDP", "IPDDP", "MSIPDDP", "IPOPT", "SNOPT", "ACADOS" - }; - - auto ax_times = time_figure->current_axes(); - auto b = bar(ax_times, solve_times); - ax_times->xticks(matplot::iota(1.0, static_cast(solver_names.size()))); - ax_times->xticklabels(solver_names); - title(ax_times, "Quadrotor Figure-8 Solver Computation Time Comparison"); - xlabel(ax_times, "Solver"); - ylabel(ax_times, "Solve Time (seconds)"); - grid(ax_times, true); - - time_figure->draw(); - time_figure->save(plotDirectory + "/quadrotor_computation_time_comparison.png"); - std::cout << "Saved computation time plot to " - << (plotDirectory + "/quadrotor_computation_time_comparison.png") << std::endl; - - // -------------------------------------------------------- - // 10. Print summary - // -------------------------------------------------------- - std::cout << "\n========================================\n"; - std::cout << " Quadrotor Figure-8 Benchmark Summary\n"; - std::cout << "========================================\n"; - std::cout << "Solver | Final Cost | Solve Time (s)\n"; - std::cout << "----------|------------|---------------\n"; - std::cout << std::fixed << std::setprecision(4); - std::cout << "ASDDP | " << std::setw(10) << cost_asddp - << " | " << std::setw(13) << solve_time_asddp / 1000000.0 << "\n"; - std::cout << "LogDDP | " << std::setw(10) << cost_logddp - << " | " << std::setw(13) << solve_time_logddp / 1000000.0 << "\n"; - std::cout << "IPDDP | " << std::setw(10) << cost_ipddp - << " | " << std::setw(13) << solve_time_ipddp / 1000000.0 << "\n"; - std::cout << "MSIPDDP | " << std::setw(10) << cost_msipddp - << " | " << std::setw(13) << solve_time_msipddp / 1000000.0 << "\n"; - std::cout << "IPOPT | " << std::setw(10) << cost_ipopt - << " | " << std::setw(13) << solve_time_ipopt_numeric << "\n"; - std::cout << "SNOPT | " << std::setw(10) << cost_snopt - << " | " << std::setw(13) << solve_time_snopt_numeric << "\n"; - std::cout << "ACADOS | " << std::setw(10) << cost_acados - << " | " << std::setw(13) << solve_time_acados_numeric << "\n"; - std::cout << "========================================\n\n"; - - return 0; -} \ No newline at end of file diff --git a/examples/sqp_car.cpp b/examples/sqp_car.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/sqp_cartpole.cpp b/examples/sqp_cartpole.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/sqp_unicycle.cpp b/examples/sqp_unicycle.cpp deleted file mode 100644 index 151ba821..00000000 --- a/examples/sqp_unicycle.cpp +++ /dev/null @@ -1,127 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include -#include -#include -#include - -#include "cddp.hpp" -#include "sqp_core/sqp_core.hpp" - -int main() { - ////////// Problem Setup ////////// - const int state_dim = 3; // [x, y, theta] - const int control_dim = 2; // [v, omega] - const int horizon = 100; // Number of control intervals - const double timestep = 0.03; // Time step - const std::string integration_type = "euler"; - - // Define initial and goal states. - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI / 4.0; - - Eigen::VectorXd goal_state(state_dim); - goal_state << 2.0, 2.0, M_PI / 2.0; - - // Create the unicycle dynamical system. - std::unique_ptr system = - std::make_unique(timestep, integration_type); - - // Define cost weighting matrices. - // Running state cost (zero in this example). - Eigen::MatrixXd Q = 5 * Eigen::MatrixXd::Zero(state_dim, state_dim); - // Control cost. - Eigen::MatrixXd R = 0.5 * Eigen::MatrixXd::Identity(control_dim, control_dim); - // Terminal state cost. - Eigen::MatrixXd Qf(state_dim, state_dim); - Qf << 1200.0, 0.0, 0.0, - 0.0, 1200.0, 0.0, - 0.0, 0.0, 700.0; - - // Create a (empty) reference trajectory. - std::vector empty_reference_states; - - // Create the quadratic objective. - auto objective = std::make_unique( - Q, R, Qf, goal_state, empty_reference_states, timestep - ); - - // Set up SQP (SCP) options. - cddp::SCPOptions options; - options.max_iterations = 5; - options.min_iterations = 3; - options.ftol = 1e-6; - options.xtol = 1e-6; - options.gtol = 1e-6; - options.merit_penalty = 100.0; - options.verbose = true; - options.trust_region_radius= 100.0; - options.ipopt_print_level = 5; - - // Create the SQP solver. - cddp::SCPSolver sqp_solver(initial_state, goal_state, horizon, timestep); - sqp_solver.setDynamicalSystem(std::move(system)); - sqp_solver.setObjective(std::move(objective)); - sqp_solver.setOptions(options); - - // Define control bounds (v ∈ [-1, 1] and ω ∈ [-π, π]). - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -1.0, -M_PI; - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 1.0, M_PI; - - // Add the control box constraint. - sqp_solver.addConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, control_upper_bound) - ); - - // Set an initial trajectory. - std::vector X(horizon + 1, Eigen::VectorXd::Zero(state_dim)); - std::vector U(horizon, Eigen::VectorXd::Zero(control_dim)); - - X[0] = initial_state; - for (int t = 1; t <= horizon; ++t) { - X[t] = initial_state + (goal_state - initial_state) * (static_cast(t) / horizon); - } - sqp_solver.setInitialTrajectory(X, U); - - ////////// Solve the Problem ////////// - auto start_time = std::chrono::high_resolution_clock::now(); - cddp::SCPResult solution = sqp_solver.solve(); - auto end_time = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end_time - start_time; - std::cout << "SQP solver elapsed time: " << elapsed.count() << " s" << std::endl; - - ////////// Extract and Display the Results ////////// - // Extract the state trajectory for plotting. - std::vector x_hist, y_hist; - for (const auto& state : solution.X) { - x_hist.push_back(state(0)); - y_hist.push_back(state(1)); - } - - // Print summary results. - std::cout << "Initial state: " << solution.X.front().transpose() << std::endl; - std::cout << "Final state: " << solution.X.back().transpose() << std::endl; - std::cout << "Goal state: " << goal_state.transpose() << std::endl; - std::cout << "Total iterations: " << solution.iterations << std::endl; - std::cout << "Solve time (from SCPResult): " << solution.solve_time << " s" << std::endl; - - return 0; -} diff --git a/examples/unicycle_benchmark.cpp b/examples/unicycle_benchmark.cpp deleted file mode 100644 index 49461107..00000000 --- a/examples/unicycle_benchmark.cpp +++ /dev/null @@ -1,1172 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "cddp.hpp" -#include "matplot/matplot.h" - -// ACADOS includes (conditional compilation) -#ifdef CDDP_CPP_ACADOS_ENABLED -extern "C" { -#include "acados_solver_unicycle.h" -#include "acados_c/ocp_nlp_interface.h" -} -#endif - -namespace fs = std::filesystem; -using namespace matplot; - -int main() { - // -------------------------- - // 1. Shared problem setup - // -------------------------- - const int state_dim = 3; // [x, y, theta] - const int control_dim = 2; // [v, omega] - const int horizon = 100; - const double timestep = 0.03; - const std::string integration_type = "euler"; - - // Quadratic cost - Eigen::MatrixXd Q = 0.01 * Eigen::MatrixXd::Identity(state_dim, state_dim); - Q(2, 2) = 0.0; - Eigen::MatrixXd R = 0.05 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 100.0, 0.0, 0.0, - 0.0, 100.0, 0.0, - 0.0, 0.0, 100.0; - - // Goal state - Eigen::VectorXd goal_state(state_dim); - goal_state << 3.0, 3.0, M_PI / 2.0; - - // Empty reference states - std::vector empty_ref; - - // Initial state - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI / 2.0; - - // Constraint parameters - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 2.0, M_PI; - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -2.0, -M_PI; - double radius = 0.4; - Eigen::Vector2d center(1.0, 1.0); - double radius2 = 0.4; - Eigen::Vector2d center2(1.5, 2.5); - - // Control bias - Eigen::VectorXd control_bias(control_dim); - control_bias << 0.0, 0.0; - - // Create a directory for saving plots - const std::string plotDirectory = "../results/benchmark"; - if (!fs::exists(plotDirectory)) { - fs::create_directories(plotDirectory); - } - - // Helper function to create initial trajectory - auto createInitialTrajectory = [&]() { - std::vector X_init(horizon + 1, initial_state); - std::vector U_init(horizon, Eigen::VectorXd::Zero(control_dim)); - - auto dyn_system = std::make_unique(timestep, integration_type); - X_init[0] = initial_state; - for (int i = 0; i < horizon; ++i) { - U_init[i] = control_bias; - X_init[i + 1] = dyn_system->getDiscreteDynamics(X_init[i], U_init[i], i * timestep); - } - return std::make_pair(X_init, U_init); - }; - - auto [X_init, U_init] = createInitialTrajectory(); - - // -------------------------------------------------------- - // 2. Baseline #1: ASDDP (requires higher regularization) - // -------------------------------------------------------- - std::cout << "Solving with ASDDP..." << std::endl; - - cddp::CDDPOptions options_asddp; - options_asddp.max_iterations = 200; - options_asddp.verbose = true; - options_asddp.debug = false; - options_asddp.enable_parallel = false; - options_asddp.num_threads = 1; - options_asddp.tolerance = 1e-4; - options_asddp.acceptable_tolerance = 1e-5; - options_asddp.regularization.initial_value = 1e-2; - - cddp::CDDP solver_asddp( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_asddp - ); - - solver_asddp.setInitialTrajectory(X_init, U_init); - - // Add constraints (consistent with other solvers) - solver_asddp.addPathConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, control_upper_bound)); - solver_asddp.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - solver_asddp.addPathConstraint("BallConstraint2", - std::make_unique(radius2, center2)); - - // Solve for baseline #1 - auto start_time_asddp = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution sol_asddp = solver_asddp.solve(cddp::SolverType::ASDDP); - auto end_time_asddp = std::chrono::high_resolution_clock::now(); - auto solve_time_asddp = std::chrono::duration_cast(end_time_asddp - start_time_asddp).count(); - - auto X_asddp_sol = std::any_cast>(sol_asddp.at("state_trajectory")); - auto U_asddp_sol = std::any_cast>(sol_asddp.at("control_trajectory")); - double cost_asddp = std::any_cast(sol_asddp.at("final_objective")); - std::cout << "ASDDP Optimal Cost: " << cost_asddp << std::endl; - - // Extract X and Y coordinates for ASDDP - std::vector x_asddp, y_asddp; - for (const auto& state : X_asddp_sol) { - x_asddp.push_back(state(0)); - y_asddp.push_back(state(1)); - } - - // -------------------------------------------------------- - // 3. Baseline #2: LogDDP - // -------------------------------------------------------- - std::cout << "Solving with LogDDP..." << std::endl; - - cddp::CDDPOptions options_logddp; - options_logddp.max_iterations = 10000; - options_logddp.verbose = true; - options_logddp.debug = false; - options_logddp.tolerance = 1e-5; - options_logddp.acceptable_tolerance = 1e-4; - options_logddp.regularization.initial_value = 1e-4; - options_logddp.log_barrier.barrier.mu_initial = 1e-0; - options_logddp.log_barrier.barrier.mu_update_factor = 0.2; - options_logddp.log_barrier.relaxed_log_barrier_delta = 1e-12; - options_logddp.log_barrier.segment_length = horizon; - options_logddp.log_barrier.use_relaxed_log_barrier_penalty = true; - options_logddp.log_barrier.rollout_type = "nonlinear"; - options_logddp.log_barrier.use_controlled_rollout = true; - - cddp::CDDP solver_logddp( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_logddp - ); - - solver_logddp.setInitialTrajectory(X_init, U_init); - - // Add constraints for LogDDP - solver_logddp.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound)); - solver_logddp.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - solver_logddp.addPathConstraint("BallConstraint2", - std::make_unique(radius2, center2)); - - // Solve for baseline #2: LogDDP - auto start_time_logddp = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution sol_logddp = solver_logddp.solve("LogDDP"); - auto end_time_logddp = std::chrono::high_resolution_clock::now(); - auto solve_time_logddp = std::chrono::duration_cast(end_time_logddp - start_time_logddp).count(); - - auto X_logddp_sol = std::any_cast>(sol_logddp.at("state_trajectory")); - auto U_logddp_sol = std::any_cast>(sol_logddp.at("control_trajectory")); - double cost_logddp = std::any_cast(sol_logddp.at("final_objective")); - std::cout << "LogDDP Optimal Cost: " << cost_logddp << std::endl; - - // Extract X and Y coordinates for LogDDP - std::vector x_logddp, y_logddp; - for (const auto& state : X_logddp_sol) { - x_logddp.push_back(state(0)); - y_logddp.push_back(state(1)); - } - - // -------------------------------------------------------- - // 4. Baseline #3: IPDDP - // -------------------------------------------------------- - std::cout << "Solving with IPDDP..." << std::endl; - - cddp::CDDPOptions options_ipddp; - options_ipddp.max_iterations = 200; - options_ipddp.verbose = true; - options_ipddp.debug = false; - options_ipddp.tolerance = 1e-5; - options_ipddp.acceptable_tolerance = 1e-6; - options_ipddp.ipddp.barrier.mu_initial = 1e-0; - options_ipddp.ipddp.barrier.mu_update_factor = 0.5; - options_ipddp.ipddp.barrier.mu_update_power = 1.2; - - cddp::CDDP solver_ipddp( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_ipddp - ); - - solver_ipddp.setInitialTrajectory(X_init, U_init); - - // Add constraints for IPDDP - solver_ipddp.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound)); - solver_ipddp.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - solver_ipddp.addPathConstraint("BallConstraint2", - std::make_unique(radius2, center2)); - - // Solve for baseline #3: IPDDP - auto start_time_ipddp = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution sol_ipddp = solver_ipddp.solve(cddp::SolverType::IPDDP); - auto end_time_ipddp = std::chrono::high_resolution_clock::now(); - auto solve_time_ipddp = std::chrono::duration_cast(end_time_ipddp - start_time_ipddp).count(); - - auto X_ipddp_sol = std::any_cast>(sol_ipddp.at("state_trajectory")); - auto U_ipddp_sol = std::any_cast>(sol_ipddp.at("control_trajectory")); - double cost_ipddp = std::any_cast(sol_ipddp.at("final_objective")); - std::cout << "IPDDP Optimal Cost: " << cost_ipddp << std::endl; - - // Extract X and Y coordinates for IPDDP - std::vector x_ipddp, y_ipddp; - for (const auto& state : X_ipddp_sol) { - x_ipddp.push_back(state(0)); - y_ipddp.push_back(state(1)); - } - - // -------------------------------------------------------- - // 5. Baseline #4: MSIPDDP - // -------------------------------------------------------- - std::cout << "Solving with MSIPDDP..." << std::endl; - - cddp::CDDPOptions options_msipddp; - options_msipddp.max_iterations = 200; - options_msipddp.verbose = true; - options_msipddp.debug = false; - options_msipddp.tolerance = 1e-5; - options_msipddp.acceptable_tolerance = 1e-6; - options_msipddp.msipddp.segment_length = 5; - options_msipddp.msipddp.rollout_type = "nonlinear"; - options_msipddp.msipddp.use_controlled_rollout = false; - options_msipddp.msipddp.barrier.mu_initial = 1e-0; - options_msipddp.msipddp.barrier.mu_update_factor = 0.5; - options_msipddp.msipddp.barrier.mu_update_power = 1.2; - - cddp::CDDP solver_msipddp( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_ref, timestep), - options_msipddp - ); - - solver_msipddp.setInitialTrajectory(X_init, U_init); - - // Add constraints for MSIPDDP - solver_msipddp.addPathConstraint("ControlConstraint", - std::make_unique(control_upper_bound)); - solver_msipddp.addPathConstraint("BallConstraint", - std::make_unique(radius, center)); - solver_msipddp.addPathConstraint("BallConstraint2", - std::make_unique(radius2, center2)); - - // Solve for baseline #4: MSIPDDP - auto start_time_msipddp = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution sol_msipddp = solver_msipddp.solve(cddp::SolverType::MSIPDDP); - auto end_time_msipddp = std::chrono::high_resolution_clock::now(); - auto solve_time_msipddp = std::chrono::duration_cast(end_time_msipddp - start_time_msipddp).count(); - - auto X_msipddp_sol = std::any_cast>(sol_msipddp.at("state_trajectory")); - auto U_msipddp_sol = std::any_cast>(sol_msipddp.at("control_trajectory")); - double cost_msipddp = std::any_cast(sol_msipddp.at("final_objective")); - std::cout << "MSIPDDP Optimal Cost: " << cost_msipddp << std::endl; - - // Extract X and Y coordinates for MSIPDDP - std::vector x_msipddp, y_msipddp; - for (const auto& state : X_msipddp_sol) { - x_msipddp.push_back(state(0)); - y_msipddp.push_back(state(1)); - } - - // -------------------------------------------------------- - // 5. Baseline #5 & #6: IPOPT and SNOPT (using CasADi) - // -------------------------------------------------------- - // NOTE: Both solvers reuse the same NLP problem definition to avoid duplication - std::cout << "Solving with IPOPT..." << std::endl; - - std::vector X_ipopt_sol(horizon + 1, Eigen::VectorXd(state_dim)); - std::vector U_ipopt_sol(horizon, Eigen::VectorXd(control_dim)); - std::vector x_ipopt, y_ipopt; - double solve_time_ipopt_numeric = 0.0; - double cost_ipopt = 0.0; - - { // IPOPT specific scope - const int n_s = state_dim; // Renaming for clarity in CasADi context - const int n_c = control_dim; // Renaming for clarity in CasADi context - const int H = horizon; // Renaming for clarity - - // Define symbolic variables for states and controls - casadi::MX X_casadi = casadi::MX::sym("X_casadi", (H + 1) * n_s); - casadi::MX U_casadi = casadi::MX::sym("U_casadi", H * n_c); - casadi::MX Z_casadi = casadi::MX::vertcat({X_casadi, U_casadi}); - - // Helper lambdas to extract the state and control at time step t - auto X_t = [&](int t) -> casadi::MX { - return X_casadi(casadi::Slice(t * n_s, (t + 1) * n_s)); - }; - auto U_t = [&](int t) -> casadi::MX { - return U_casadi(casadi::Slice(t * n_c, (t + 1) * n_c)); - }; - using casadi::cos; - using casadi::sin; - - // Unicycle dynamics function for CasADi - auto unicycle_dynamics_casadi = [&](casadi::MX x, casadi::MX u) -> casadi::MX { - casadi::MX x_next = casadi::MX::zeros(n_s, 1); - casadi::MX theta = x(2); - casadi::MX v = u(0); - casadi::MX omega = u(1); - casadi::MX ctheta = cos(theta); - casadi::MX stheta = sin(theta); - x_next(0) = x(0) + v * ctheta * timestep; - x_next(1) = x(1) + v * stheta * timestep; - x_next(2) = x(2) + omega * timestep; - return x_next; - }; - - // Cost function - casadi::MX cost_casadi = 0; - casadi::DM Q_dm = casadi::DM::zeros(n_s, n_s); - casadi::DM R_dm = casadi::DM::zeros(n_c, n_c); - casadi::DM Qf_dm = casadi::DM::zeros(n_s, n_s); - - for(int i=0; i(goal_state.data(), goal_state.data() + n_s)); - - for (int t = 0; t < H; ++t) { - casadi::MX x_diff = X_t(t) - goal_state_dm; - casadi::MX u_curr = U_t(t); - cost_casadi += casadi::MX::mtimes({x_diff.T(), Q_dm, x_diff}); - cost_casadi += casadi::MX::mtimes({u_curr.T(), R_dm, u_curr}); - } - casadi::MX x_final_diff = X_t(H) - goal_state_dm; - cost_casadi += casadi::MX::mtimes({x_final_diff.T(), Qf_dm, x_final_diff}); - - // Constraints - casadi::MX g_casadi; - casadi::DM initial_state_dm(std::vector(initial_state.data(), initial_state.data() + n_s)); - g_casadi = casadi::MX::vertcat({g_casadi, X_t(0) - initial_state_dm}); - - for (int t = 0; t < H; ++t) { - g_casadi = casadi::MX::vertcat({g_casadi, X_t(t+1) - unicycle_dynamics_casadi(X_t(t), U_t(t))}); - } - - // Store the number of equality constraints (initial state + dynamics) - int num_equality_constraints = static_cast(g_casadi.size1()); - - // Add Ball Constraints (inequality constraints) - for (int t = 0; t <= H; ++t) { - casadi::MX x_coord = X_t(t)(0); - casadi::MX y_coord = X_t(t)(1); - - // Ball Constraint 1 - casadi::MX term1_c1 = x_coord - center(0); - casadi::MX term2_c1 = y_coord - center(1); - casadi::MX ball_constraint1 = term1_c1 * term1_c1 + - term2_c1 * term2_c1 - - radius * radius; - g_casadi = casadi::MX::vertcat({g_casadi, ball_constraint1}); - - // Ball Constraint 2 - casadi::MX term1_c2 = x_coord - center2(0); - casadi::MX term2_c2 = y_coord - center2(1); - casadi::MX ball_constraint2 = term1_c2 * term1_c2 + - term2_c2 * term2_c2 - - radius2 * radius2; - g_casadi = casadi::MX::vertcat({g_casadi, ball_constraint2}); - } - - // NLP definition - casadi::MXDict nlp_casadi = {{"x", Z_casadi}, {"f", cost_casadi}, {"g", g_casadi}}; - casadi::Dict solver_opts; - solver_opts["ipopt.print_level"] = 0; - solver_opts["print_time"] = true; - solver_opts["ipopt.tol"] = 1e-6; - solver_opts["ipopt.acceptable_tol"] = 1e-5; - casadi::Function solver_ipopt = casadi::nlpsol("solver_ipopt", "ipopt", nlp_casadi, solver_opts); - - // Bounds - std::vector lbx_casadi((H+1)*n_s + H*n_c, -casadi::inf); - std::vector ubx_casadi((H+1)*n_s + H*n_c, casadi::inf); - - for (int t = 0; t < H; ++t) { - for (int i = 0; i < n_c; ++i) { - lbx_casadi[(H+1)*n_s + t*n_c + i] = control_lower_bound(i); - ubx_casadi[(H+1)*n_s + t*n_c + i] = control_upper_bound(i); - } - } - - int n_g_casadi = static_cast(g_casadi.size1()); - std::vector lbg_casadi_vec(n_g_casadi); - std::vector ubg_casadi_vec(n_g_casadi); - - // Bounds for equality constraints - for (int i = 0; i < num_equality_constraints; ++i) { - lbg_casadi_vec[i] = 0.0; - ubg_casadi_vec[i] = 0.0; - } - - // Bounds for ball constraints (inequality) - for (int i = num_equality_constraints; i < n_g_casadi; ++i) { - lbg_casadi_vec[i] = 0.0; - ubg_casadi_vec[i] = casadi::inf; - } - - // Initial guess - std::vector x0_casadi_vec((H+1)*n_s + H*n_c, 0.0); - for (int i = 0; i < n_s; ++i) x0_casadi_vec[i] = initial_state(i); - for (int t = 1; t <= H; ++t) { - for (int i = 0; i < n_s; ++i) { - x0_casadi_vec[t*n_s + i] = initial_state(i); - } - } - - casadi::DMDict arg_ipopt; - arg_ipopt["lbx"] = casadi::DM(lbx_casadi); - arg_ipopt["ubx"] = casadi::DM(ubx_casadi); - arg_ipopt["lbg"] = casadi::DM(lbg_casadi_vec); - arg_ipopt["ubg"] = casadi::DM(ubg_casadi_vec); - arg_ipopt["x0"] = casadi::DM(x0_casadi_vec); - - auto start_time_ipopt = std::chrono::high_resolution_clock::now(); - casadi::DMDict res_ipopt = solver_ipopt(arg_ipopt); - auto end_time_ipopt = std::chrono::high_resolution_clock::now(); - std::chrono::duration duration_ipopt = end_time_ipopt - start_time_ipopt; - solve_time_ipopt_numeric = duration_ipopt.count(); - - std::vector sol_ipopt_vec = std::vector(res_ipopt.at("x")); - - if (res_ipopt.count("f")) { - double optimal_cost = static_cast(casadi::DM(res_ipopt.at("f"))); - std::cout << "IPOPT Optimal Cost: " << optimal_cost << std::endl; - cost_ipopt = optimal_cost; - } - - for (int t = 0; t <= H; ++t) { - for (int i = 0; i < n_s; ++i) X_ipopt_sol[t](i) = sol_ipopt_vec[t*n_s + i]; - } - for (int t = 0; t < H; ++t) { - for (int i = 0; i < n_c; ++i) U_ipopt_sol[t](i) = sol_ipopt_vec[(H+1)*n_s + t*n_c + i]; - } - - for (const auto& state : X_ipopt_sol) { - x_ipopt.push_back(state(0)); - y_ipopt.push_back(state(1)); - } - std::cout << "IPOPT solve time: " << solve_time_ipopt_numeric << " seconds" << std::endl; - } - - // -------------------------------------------------------- - // SNOPT: Reusing the same NLP definition from IPOPT above - // -------------------------------------------------------- - std::cout << "Solving with SNOPT..." << std::endl; - - std::vector X_snopt_sol(horizon + 1, Eigen::VectorXd(state_dim)); - std::vector U_snopt_sol(horizon, Eigen::VectorXd(control_dim)); - std::vector x_snopt, y_snopt; - double solve_time_snopt_numeric = 0.0; - double cost_snopt = 0.0; - - { // SNOPT specific scope - const int n_s = state_dim; // Renaming for clarity in CasADi context - const int n_c = control_dim; // Renaming for clarity in CasADi context - const int H = horizon; // Renaming for clarity - - // Define symbolic variables for states and controls - casadi::MX X_casadi = casadi::MX::sym("X_casadi", (H + 1) * n_s); - casadi::MX U_casadi = casadi::MX::sym("U_casadi", H * n_c); - casadi::MX Z_casadi = casadi::MX::vertcat({X_casadi, U_casadi}); - - // Helper lambdas to extract the state and control at time step t - auto X_t = [&](int t) -> casadi::MX { - return X_casadi(casadi::Slice(t * n_s, (t + 1) * n_s)); - }; - auto U_t = [&](int t) -> casadi::MX { - return U_casadi(casadi::Slice(t * n_c, (t + 1) * n_c)); - }; - using casadi::cos; - using casadi::sin; - - // Unicycle dynamics function for CasADi - auto unicycle_dynamics_casadi = [&](casadi::MX x, casadi::MX u) -> casadi::MX { - casadi::MX x_next = casadi::MX::zeros(n_s, 1); - casadi::MX theta = x(2); - casadi::MX v = u(0); - casadi::MX omega = u(1); - casadi::MX ctheta = cos(theta); - casadi::MX stheta = sin(theta); - x_next(0) = x(0) + v * ctheta * timestep; - x_next(1) = x(1) + v * stheta * timestep; - x_next(2) = x(2) + omega * timestep; - return x_next; - }; - - // Cost function - casadi::MX cost_casadi = 0; - casadi::DM Q_dm = casadi::DM::zeros(n_s, n_s); - casadi::DM R_dm = casadi::DM::zeros(n_c, n_c); - casadi::DM Qf_dm = casadi::DM::zeros(n_s, n_s); - - for(int i=0; i(goal_state.data(), goal_state.data() + n_s)); - - for (int t = 0; t < H; ++t) { - casadi::MX x_diff = X_t(t) - goal_state_dm; - casadi::MX u_curr = U_t(t); - cost_casadi += casadi::MX::mtimes({x_diff.T(), Q_dm, x_diff}); - cost_casadi += casadi::MX::mtimes({u_curr.T(), R_dm, u_curr}); - } - casadi::MX x_final_diff = X_t(H) - goal_state_dm; - cost_casadi += casadi::MX::mtimes({x_final_diff.T(), Qf_dm, x_final_diff}); - - // Constraints - casadi::MX g_casadi; - casadi::DM initial_state_dm(std::vector(initial_state.data(), initial_state.data() + n_s)); - g_casadi = casadi::MX::vertcat({g_casadi, X_t(0) - initial_state_dm}); - - for (int t = 0; t < H; ++t) { - g_casadi = casadi::MX::vertcat({g_casadi, X_t(t+1) - unicycle_dynamics_casadi(X_t(t), U_t(t))}); - } - - // Store the number of equality constraints (initial state + dynamics) - int num_equality_constraints = static_cast(g_casadi.size1()); - - // Add Ball Constraints (inequality constraints) - for (int t = 0; t <= H; ++t) { - casadi::MX x_coord = X_t(t)(0); - casadi::MX y_coord = X_t(t)(1); - - // Ball Constraint 1 - casadi::MX term1_c1 = x_coord - center(0); - casadi::MX term2_c1 = y_coord - center(1); - casadi::MX ball_constraint1 = term1_c1 * term1_c1 + - term2_c1 * term2_c1 - - radius * radius; - g_casadi = casadi::MX::vertcat({g_casadi, ball_constraint1}); - - // Ball Constraint 2 - casadi::MX term1_c2 = x_coord - center2(0); - casadi::MX term2_c2 = y_coord - center2(1); - casadi::MX ball_constraint2 = term1_c2 * term1_c2 + - term2_c2 * term2_c2 - - radius2 * radius2; - g_casadi = casadi::MX::vertcat({g_casadi, ball_constraint2}); - } - - // NLP definition - casadi::MXDict nlp_casadi = {{"x", Z_casadi}, {"f", cost_casadi}, {"g", g_casadi}}; - casadi::Dict solver_opts; - solver_opts["snopt.print_level"] = 1; - solver_opts["print_time"] = true; - solver_opts["snopt.major_iterations_limit"] = 500; - solver_opts["snopt.minor_iterations_limit"] = 500; - casadi::Function solver_snopt = casadi::nlpsol("solver_snopt", "snopt", nlp_casadi, solver_opts); - - // Bounds - std::vector lbx_casadi((H+1)*n_s + H*n_c, -casadi::inf); - std::vector ubx_casadi((H+1)*n_s + H*n_c, casadi::inf); - - for (int t = 0; t < H; ++t) { - for (int i = 0; i < n_c; ++i) { - lbx_casadi[(H+1)*n_s + t*n_c + i] = control_lower_bound(i); - ubx_casadi[(H+1)*n_s + t*n_c + i] = control_upper_bound(i); - } - } - - int n_g_casadi = static_cast(g_casadi.size1()); - std::vector lbg_casadi_vec(n_g_casadi); - std::vector ubg_casadi_vec(n_g_casadi); - - // Bounds for equality constraints - for (int i = 0; i < num_equality_constraints; ++i) { - lbg_casadi_vec[i] = 0.0; - ubg_casadi_vec[i] = 0.0; - } - - // Bounds for ball constraints (inequality) - for (int i = num_equality_constraints; i < n_g_casadi; ++i) { - lbg_casadi_vec[i] = 0.0; - ubg_casadi_vec[i] = casadi::inf; - } - - // Initial guess - std::vector x0_casadi_vec((H+1)*n_s + H*n_c, 0.0); - for (int i = 0; i < n_s; ++i) x0_casadi_vec[i] = initial_state(i); - for (int t = 1; t <= H; ++t) { - for (int i = 0; i < n_s; ++i) { - x0_casadi_vec[t*n_s + i] = initial_state(i); - } - } - - casadi::DMDict arg_snopt; - arg_snopt["lbx"] = casadi::DM(lbx_casadi); - arg_snopt["ubx"] = casadi::DM(ubx_casadi); - arg_snopt["lbg"] = casadi::DM(lbg_casadi_vec); - arg_snopt["ubg"] = casadi::DM(ubg_casadi_vec); - arg_snopt["x0"] = casadi::DM(x0_casadi_vec); - - auto start_time_snopt = std::chrono::high_resolution_clock::now(); - casadi::DMDict res_snopt = solver_snopt(arg_snopt); - auto end_time_snopt = std::chrono::high_resolution_clock::now(); - std::chrono::duration duration_snopt = end_time_snopt - start_time_snopt; - solve_time_snopt_numeric = duration_snopt.count(); - - std::vector sol_snopt_vec = std::vector(res_snopt.at("x")); - - if (res_snopt.count("f")) { - double optimal_cost = static_cast(casadi::DM(res_snopt.at("f"))); - std::cout << "SNOPT Optimal Cost: " << optimal_cost << std::endl; - cost_snopt = optimal_cost; - } - - for (int t = 0; t <= H; ++t) { - for (int i = 0; i < n_s; ++i) X_snopt_sol[t](i) = sol_snopt_vec[t*n_s + i]; - } - for (int t = 0; t < H; ++t) { - for (int i = 0; i < n_c; ++i) U_snopt_sol[t](i) = sol_snopt_vec[(H+1)*n_s + t*n_c + i]; - } - - for (const auto& state : X_snopt_sol) { - x_snopt.push_back(state(0)); - y_snopt.push_back(state(1)); - } - std::cout << "SNOPT solve time: " << solve_time_snopt_numeric << " seconds" << std::endl; - } - - // -------------------------------------------------------- - // 7. ACADOS Solver (using C interface) - // -------------------------------------------------------- - std::cout << "Solving with ACADOS..." << std::endl; - - std::vector X_acados_sol(horizon + 1, Eigen::VectorXd(state_dim)); - std::vector U_acados_sol(horizon, Eigen::VectorXd(control_dim)); - std::vector x_acados, y_acados; - double solve_time_acados_numeric = 0.0; - double cost_acados = 0.0; - -#ifdef CDDP_CPP_ACADOS_ENABLED - try { // ACADOS specific scope with exception handling - std::cout << "ACADOS: Using generated solver" << std::endl; - - const int N = horizon; - - auto start_time_acados = std::chrono::high_resolution_clock::now(); - - std::cout << "ACADOS: Creating solver capsule..." << std::endl; - // Create solver capsule - unicycle_solver_capsule *capsule = unicycle_acados_create_capsule(); - int status = unicycle_acados_create(capsule); - - if (status) { - std::cerr << "ACADOS solver creation failed with status " << status << std::endl; - unicycle_acados_free_capsule(capsule); - throw std::runtime_error("Failed to create ACADOS solver"); - } - - // Get internal structures - ocp_nlp_config *nlp_config = unicycle_acados_get_nlp_config(capsule); - ocp_nlp_dims *nlp_dims = unicycle_acados_get_nlp_dims(capsule); - ocp_nlp_in *nlp_in = unicycle_acados_get_nlp_in(capsule); - ocp_nlp_out *nlp_out = unicycle_acados_get_nlp_out(capsule); - - // Set initial state constraint - double x0[3] = {initial_state(0), initial_state(1), initial_state(2)}; - ocp_nlp_constraints_model_set(nlp_config, nlp_dims, nlp_in, nlp_out, 0, "lbx", x0); - ocp_nlp_constraints_model_set(nlp_config, nlp_dims, nlp_in, nlp_out, 0, "ubx", x0); - - std::cout << "ACADOS: Updating cost matrices..." << std::endl; - // Update cost matrices to match our problem - double W[25]; // 5x5 matrix for stage cost - double W_e[9]; // 3x3 matrix for terminal cost - double yref[5] = {0.0, 0.0, goal_state(0), goal_state(1), goal_state(2)}; // [u_ref; x_ref] - double yref_e[3] = {goal_state(0), goal_state(1), goal_state(2)}; // x_ref at terminal - - // W = diag([R*dt; Q*dt]) but we don't use dt scaling for ACADOS - std::fill_n(W, 25, 0.0); - W[0] = R(0,0); // v cost - W[6] = R(1,1); // omega cost - W[12] = Q(0,0); // x cost - W[18] = Q(1,1); // y cost - W[24] = Q(2,2); // theta cost - - // W_e = Qf - std::fill_n(W_e, 9, 0.0); - W_e[0] = Qf(0,0); // x terminal cost - W_e[4] = Qf(1,1); // y terminal cost - W_e[8] = Qf(2,2); // theta terminal cost - - // Update cost for all stages - for (int i = 0; i < N; i++) { - ocp_nlp_cost_model_set(nlp_config, nlp_dims, nlp_in, i, "W", W); - ocp_nlp_cost_model_set(nlp_config, nlp_dims, nlp_in, i, "yref", yref); - } - - // Terminal cost - ocp_nlp_cost_model_set(nlp_config, nlp_dims, nlp_in, N, "W", W_e); - ocp_nlp_cost_model_set(nlp_config, nlp_dims, nlp_in, N, "yref", yref_e); - - std::cout << "ACADOS: Initializing trajectory..." << std::endl; - // Initialize trajectory - for (int i = 0; i <= N; i++) { - double x_init_stage[3] = {X_init[i](0), X_init[i](1), X_init[i](2)}; - ocp_nlp_out_set(nlp_config, nlp_dims, nlp_out, nlp_in, i, "x", x_init_stage); - } - - for (int i = 0; i < N; i++) { - double u_init_stage[2] = {U_init[i](0), U_init[i](1)}; - ocp_nlp_out_set(nlp_config, nlp_dims, nlp_out, nlp_in, i, "u", u_init_stage); - } - - std::cout << "ACADOS: Solving..." << std::endl; - // Solve - status = unicycle_acados_solve(capsule); - - auto end_time_acados = std::chrono::high_resolution_clock::now(); - solve_time_acados_numeric = std::chrono::duration(end_time_acados - start_time_acados).count(); - - // Extract solution - for (int i = 0; i <= N; i++) { - double x_sol[3]; - ocp_nlp_out_get(nlp_config, nlp_dims, nlp_out, i, "x", x_sol); - X_acados_sol[i] << x_sol[0], x_sol[1], x_sol[2]; - x_acados.push_back(x_sol[0]); - y_acados.push_back(x_sol[1]); - } - - for (int i = 0; i < N; i++) { - double u_sol[2]; - ocp_nlp_out_get(nlp_config, nlp_dims, nlp_out, i, "u", u_sol); - U_acados_sol[i] << u_sol[0], u_sol[1]; - } - - // Compute cost by evaluating objective function - cost_acados = 0.0; - for (int i = 0; i <= N; i++) { - Eigen::VectorXd x_err = X_acados_sol[i] - goal_state; - if (i < N) { - cost_acados += timestep * (x_err.transpose() * Q * x_err + U_acados_sol[i].transpose() * R * U_acados_sol[i]).value(); - } else { - cost_acados += (x_err.transpose() * Qf * x_err).value(); - } - } - - // Get solver statistics - ocp_nlp_solver *solver = unicycle_acados_get_nlp_solver(capsule); - int sqp_iter; - ocp_nlp_get(solver, "sqp_iter", &sqp_iter); - - double time_tot; - ocp_nlp_get(solver, "time_tot", &time_tot); - - std::cout << "ACADOS status: " << status << " (0 = success)" << std::endl; - std::cout << "ACADOS iterations: " << sqp_iter << std::endl; - std::cout << "ACADOS Optimal Cost: " << cost_acados << std::endl; - std::cout << "ACADOS solve time: " << solve_time_acados_numeric << " seconds" << std::endl; - - // Cleanup - unicycle_acados_free(capsule); - unicycle_acados_free_capsule(capsule); - - } catch (const std::exception& e) { - std::cerr << "ACADOS error: " << e.what() << std::endl; - std::cerr << "Using fallback solution" << std::endl; - - // Use initial trajectory as fallback - X_acados_sol = X_init; - U_acados_sol = U_init; - - // Calculate cost manually for placeholder - cost_acados = 0.0; - for (int t = 0; t < horizon; ++t) { - Eigen::VectorXd x_diff = X_acados_sol[t] - goal_state; - Eigen::VectorXd u_curr = U_acados_sol[t]; - cost_acados += (x_diff.transpose() * Q * x_diff).value() * timestep; - cost_acados += (u_curr.transpose() * R * u_curr).value() * timestep; - } - Eigen::VectorXd x_final_diff = X_acados_sol[horizon] - goal_state; - cost_acados += (x_final_diff.transpose() * Qf * x_final_diff).value(); - - for (const auto& state : X_acados_sol) { - x_acados.push_back(state(0)); - y_acados.push_back(state(1)); - } - - solve_time_acados_numeric = 0.001; // 1ms fallback time - } -#else - { // ACADOS not available - use initial trajectory - std::cout << "ACADOS not available. Using initial trajectory as placeholder." << std::endl; - X_acados_sol = X_init; - U_acados_sol = U_init; - - // Calculate cost manually for placeholder - cost_acados = 0.0; - for (int t = 0; t < horizon; ++t) { - Eigen::VectorXd x_diff = X_acados_sol[t] - goal_state; - Eigen::VectorXd u_curr = U_acados_sol[t]; - cost_acados += (x_diff.transpose() * Q * x_diff).value() * timestep; - cost_acados += (u_curr.transpose() * R * u_curr).value() * timestep; - } - Eigen::VectorXd x_final_diff = X_acados_sol[horizon] - goal_state; - cost_acados += (x_final_diff.transpose() * Qf * x_final_diff).value(); - - for (const auto& state : X_acados_sol) { - x_acados.push_back(state(0)); - y_acados.push_back(state(1)); - } - - std::cout << "ACADOS Optimal Cost: " << cost_acados << std::endl; - std::cout << "ACADOS solve time: " << solve_time_acados_numeric << " seconds" << std::endl; - } -#endif - - // -------------------------------------------------------- - // 8. Plot all trajectories in one figure - // -------------------------------------------------------- - auto main_figure = figure(true); - main_figure->size(4200, 500); // Increased width for 7 solvers - - // Initial guess data - std::vector x_init_plot, y_init_plot; - for (int i = 0; i < horizon + 1; ++i) { - x_init_plot.push_back(X_init[i](0)); - y_init_plot.push_back(X_init[i](1)); - } - - std::vector cx, cy; - for (double th = 0.0; th < 2.0 * M_PI; th += 0.01) { - cx.push_back(center(0) + radius * std::cos(th)); - cy.push_back(center(1) + radius * std::sin(th)); - } - - std::vector cx2, cy2; - for (double th = 0.0; th < 2.0 * M_PI; th += 0.01) { - cx2.push_back(center2(0) + radius2 * std::cos(th)); - cy2.push_back(center2(1) + radius2 * std::sin(th)); - } - - // --- Subplot 1: ASDDP --- - auto ax_asddp = subplot(1, 7, 0); - - auto l_asddp = plot(ax_asddp, x_asddp, y_asddp, "-r"); - l_asddp->display_name("ASDDP Solution"); - l_asddp->line_width(2); - - hold(ax_asddp, true); - - auto cplot_asddp = plot(ax_asddp, cx, cy, "--g"); - cplot_asddp->display_name("Ball Constraint"); - cplot_asddp->line_width(2); - - auto cplot_asddp2 = plot(ax_asddp, cx2, cy2, "--g"); - cplot_asddp2->display_name("Ball Constraint 2"); - cplot_asddp2->line_width(2); - - auto l_asddp_init = plot(ax_asddp, x_init_plot, y_init_plot, "-k"); - l_asddp_init->display_name("Initial Guess"); - l_asddp_init->line_width(2); - - title(ax_asddp, "ASDDP Trajectory"); - xlabel(ax_asddp, "x [m]"); - ylabel(ax_asddp, "y [m]"); - xlim(ax_asddp, {-0.2, 3.2}); - ylim(ax_asddp, {-0.2, 3.2}); - auto leg_asddp = matplot::legend(ax_asddp); - leg_asddp->location(legend::general_alignment::topleft); - grid(ax_asddp, true); - - // --- Subplot 2: LogDDP --- - auto ax_logddp = subplot(1, 7, 1); - - auto l_logddp = plot(ax_logddp, x_logddp, y_logddp, "-b"); - l_logddp->display_name("LogDDP Solution"); - l_logddp->line_width(2); - - hold(ax_logddp, true); - - auto cplot_logddp = plot(ax_logddp, cx, cy, "--g"); - cplot_logddp->display_name("Ball Constraint"); - cplot_logddp->line_width(2); - - auto cplot_logddp2 = plot(ax_logddp, cx2, cy2, "--g"); - cplot_logddp2->display_name("Ball Constraint 2"); - cplot_logddp2->line_width(2); - - auto l_logddp_init = plot(ax_logddp, x_init_plot, y_init_plot, "-k"); - l_logddp_init->display_name("Initial Guess"); - l_logddp_init->line_width(2); - - title(ax_logddp, "LogDDP Trajectory"); - xlabel(ax_logddp, "x [m]"); - ylabel(ax_logddp, "y [m]"); - xlim(ax_logddp, {-0.2, 3.2}); - ylim(ax_logddp, {-0.2, 3.2}); - auto leg_logddp = matplot::legend(ax_logddp); - leg_logddp->location(legend::general_alignment::topleft); - grid(ax_logddp, true); - - // --- Subplot 3: IPDDP --- - auto ax_ipddp = subplot(1, 7, 2); - - auto l_ipddp = plot(ax_ipddp, x_ipddp, y_ipddp, "-g"); - l_ipddp->display_name("IPDDP Solution"); - l_ipddp->line_width(2); - - hold(ax_ipddp, true); - - auto cplot_ipddp = plot(ax_ipddp, cx, cy, "--g"); - cplot_ipddp->display_name("Ball Constraint"); - cplot_ipddp->line_width(2); - - auto cplot_ipddp2 = plot(ax_ipddp, cx2, cy2, "--g"); - cplot_ipddp2->display_name("Ball Constraint 2"); - cplot_ipddp2->line_width(2); - - auto l_ipddp_init = plot(ax_ipddp, x_init_plot, y_init_plot, "-k"); - l_ipddp_init->display_name("Initial Guess"); - l_ipddp_init->line_width(2); - - title(ax_ipddp, "IPDDP Trajectory"); - xlabel(ax_ipddp, "x [m]"); - ylabel(ax_ipddp, "y [m]"); - xlim(ax_ipddp, {-0.2, 3.2}); - ylim(ax_ipddp, {-0.2, 3.2}); - auto leg_ipddp = matplot::legend(ax_ipddp); - leg_ipddp->location(legend::general_alignment::topleft); - grid(ax_ipddp, true); - - // --- Subplot 4: MSIPDDP --- - auto ax_msipddp = subplot(1, 7, 3); - - auto l_msipddp = plot(ax_msipddp, x_msipddp, y_msipddp, "-c"); - l_msipddp->display_name("MSIPDDP Solution"); - l_msipddp->line_width(2); - - hold(ax_msipddp, true); - - auto cplot_msipddp = plot(ax_msipddp, cx, cy, "--g"); - cplot_msipddp->display_name("Ball Constraint"); - cplot_msipddp->line_width(2); - - auto cplot_msipddp2 = plot(ax_msipddp, cx2, cy2, "--g"); - cplot_msipddp2->display_name("Ball Constraint 2"); - cplot_msipddp2->line_width(2); - - auto l_msipddp_init = plot(ax_msipddp, x_init_plot, y_init_plot, "-k"); - l_msipddp_init->display_name("Initial Guess"); - l_msipddp_init->line_width(2); - - title(ax_msipddp, "MSIPDDP Trajectory"); - xlabel(ax_msipddp, "x [m]"); - ylabel(ax_msipddp, "y [m]"); - xlim(ax_msipddp, {-0.2, 3.2}); - ylim(ax_msipddp, {-0.2, 3.2}); - auto leg_msipddp = matplot::legend(ax_msipddp); - leg_msipddp->location(legend::general_alignment::topleft); - grid(ax_msipddp, true); - - // --- Subplot 5: IPOPT --- - auto ax_ipopt = subplot(1, 7, 4); - auto l_ipopt_sol = plot(ax_ipopt, x_ipopt, y_ipopt, "-m"); - l_ipopt_sol->display_name("IPOPT Solution"); - l_ipopt_sol->line_width(2); - - hold(ax_ipopt, true); - - auto cplot_ipopt = plot(ax_ipopt, cx, cy, "--g"); - cplot_ipopt->display_name("Ball Constraint"); - cplot_ipopt->line_width(2); - - auto cplot_ipopt2 = plot(ax_ipopt, cx2, cy2, "--g"); - cplot_ipopt2->display_name("Ball Constraint 2"); - cplot_ipopt2->line_width(2); - - auto l_ipopt_init = plot(ax_ipopt, x_init_plot, y_init_plot, "-k"); - l_ipopt_init->display_name("Initial Guess"); - l_ipopt_init->line_width(2); - - title(ax_ipopt, "IPOPT Trajectory"); - xlabel(ax_ipopt, "x [m]"); - ylabel(ax_ipopt, "y [m]"); - xlim(ax_ipopt, {-0.2, 3.2}); - ylim(ax_ipopt, {-0.2, 3.2}); - auto leg_ipopt = matplot::legend(ax_ipopt); - leg_ipopt->location(legend::general_alignment::topleft); - grid(ax_ipopt, true); - - // --- Subplot 6: SNOPT --- - auto ax_snopt = subplot(1, 7, 5); - auto l_snopt_sol = plot(ax_snopt, x_snopt, y_snopt, "-y"); - l_snopt_sol->display_name("SNOPT Solution"); - l_snopt_sol->line_width(2); - - hold(ax_snopt, true); - - auto cplot_snopt = plot(ax_snopt, cx, cy, "--g"); - cplot_snopt->display_name("Ball Constraint"); - cplot_snopt->line_width(2); - - auto cplot_snopt2 = plot(ax_snopt, cx2, cy2, "--g"); - cplot_snopt2->display_name("Ball Constraint 2"); - cplot_snopt2->line_width(2); - - auto l_snopt_init = plot(ax_snopt, x_init_plot, y_init_plot, "-k"); - l_snopt_init->display_name("Initial Guess"); - l_snopt_init->line_width(2); - - title(ax_snopt, "SNOPT Trajectory"); - xlabel(ax_snopt, "x [m]"); - ylabel(ax_snopt, "y [m]"); - xlim(ax_snopt, {-0.2, 3.2}); - ylim(ax_snopt, {-0.2, 3.2}); - auto leg_snopt = matplot::legend(ax_snopt); - leg_snopt->location(legend::general_alignment::topleft); - grid(ax_snopt, true); - - // --- Subplot 7: ACADOS --- - auto ax_acados = subplot(1, 7, 6); - auto l_acados_sol = plot(ax_acados, x_acados, y_acados, "-c"); - l_acados_sol->display_name("ACADOS Solution"); - l_acados_sol->line_width(2); - - hold(ax_acados, true); - - auto cplot_acados = plot(ax_acados, cx, cy, "--g"); - cplot_acados->display_name("Ball Constraint"); - cplot_acados->line_width(2); - - auto cplot_acados2 = plot(ax_acados, cx2, cy2, "--g"); - cplot_acados2->display_name("Ball Constraint 2"); - cplot_acados2->line_width(2); - - auto l_acados_init = plot(ax_acados, x_init_plot, y_init_plot, "-k"); - l_acados_init->display_name("Initial Guess"); - l_acados_init->line_width(2); - - title(ax_acados, "ACADOS Trajectory"); - xlabel(ax_acados, "x [m]"); - ylabel(ax_acados, "y [m]"); - xlim(ax_acados, {-0.2, 3.2}); - ylim(ax_acados, {-0.2, 3.2}); - auto leg_acados = matplot::legend(ax_acados); - leg_acados->location(legend::general_alignment::topleft); - grid(ax_acados, true); - - main_figure->draw(); - main_figure->save(plotDirectory + "/unicycle_baseline_trajectories_comparison.png"); - std::cout << "Saved combined trajectory plot to " - << (plotDirectory + "/unicycle_baseline_trajectories_comparison.png") << std::endl; - - // -------------------------------------------------------- - // 8. Plot computation times - // -------------------------------------------------------- - auto time_figure = figure(true); - time_figure->size(1200, 600); - - std::vector solve_times = { - solve_time_asddp / 1000000.0, // Convert to seconds - solve_time_logddp / 1000000.0, // Convert to seconds - solve_time_ipddp / 1000000.0, // Convert to seconds - solve_time_msipddp / 1000000.0, // Convert to seconds - solve_time_ipopt_numeric, - solve_time_snopt_numeric, - solve_time_acados_numeric - }; - - std::vector solver_names = { - "ASDDP", "LogDDP", "IPDDP", "MSIPDDP", "IPOPT", "SNOPT", "ACADOS" - }; - - auto ax_times = time_figure->current_axes(); - auto b = bar(ax_times, solve_times); - ax_times->xticks(matplot::iota(1.0, static_cast(solver_names.size()))); - ax_times->xticklabels(solver_names); - title(ax_times, "Solver Computation Time Comparison"); - xlabel(ax_times, "Solver"); - ylabel(ax_times, "Solve Time (seconds)"); - grid(ax_times, true); - - time_figure->draw(); - time_figure->save(plotDirectory + "/unicycle_computation_time_comparison.png"); - std::cout << "Saved computation time plot to " - << (plotDirectory + "/unicycle_computation_time_comparison.png") << std::endl; - - // -------------------------------------------------------- - // 9. Print summary - // -------------------------------------------------------- - std::cout << "\n========================================\n"; - std::cout << " Unicycle Benchmark Summary\n"; - std::cout << "========================================\n"; - std::cout << "Solver | Final Cost | Solve Time (s)\n"; - std::cout << "----------|------------|---------------\n"; - std::cout << std::fixed << std::setprecision(4); - std::cout << "ASDDP | " << std::setw(10) << cost_asddp - << " | " << std::setw(13) << solve_time_asddp / 1000000.0 << "\n"; - std::cout << "LogDDP | " << std::setw(10) << cost_logddp - << " | " << std::setw(13) << solve_time_logddp / 1000000.0 << "\n"; - std::cout << "IPDDP | " << std::setw(10) << cost_ipddp - << " | " << std::setw(13) << solve_time_ipddp / 1000000.0 << "\n"; - std::cout << "MSIPDDP | " << std::setw(10) << cost_msipddp - << " | " << std::setw(13) << solve_time_msipddp / 1000000.0 << "\n"; - std::cout << "IPOPT | " << std::setw(10) << cost_ipopt - << " | " << std::setw(13) << solve_time_ipopt_numeric << "\n"; - std::cout << "SNOPT | " << std::setw(10) << cost_snopt - << " | " << std::setw(13) << solve_time_snopt_numeric << "\n"; - std::cout << "ACADOS | " << std::setw(10) << cost_acados - << " | " << std::setw(13) << solve_time_acados_numeric << "\n"; - std::cout << "========================================\n\n"; - - return 0; -} diff --git a/include/cddp-cpp/cddp.hpp b/include/cddp-cpp/cddp.hpp index 3ea9d649..b41d4584 100644 --- a/include/cddp-cpp/cddp.hpp +++ b/include/cddp-cpp/cddp.hpp @@ -29,7 +29,6 @@ #include "cddp_core/options.hpp" #include "cddp_core/cddp_core.hpp" #include "cddp_core/clddp_solver.hpp" -#include "cddp_core/asddp_solver.hpp" #include "cddp_core/logddp_solver.hpp" #include "cddp_core/ipddp_solver.hpp" #include "cddp_core/msipddp_solver.hpp" @@ -38,10 +37,6 @@ #include "cddp_core/boxqp.hpp" #include "cddp_core/qp_solver.hpp" -#ifdef CDDP_CPP_TORCH_ENABLED -#include "cddp_core/neural_dynamical_system.hpp" -#endif - // Models #include "dynamics_model/pendulum.hpp" #include "dynamics_model/unicycle.hpp" @@ -65,4 +60,4 @@ #include "matplot/matplot.h" -#endif // CDDP_HPP \ No newline at end of file +#endif // CDDP_HPP diff --git a/include/cddp-cpp/cddp_core/asddp_solver.hpp b/include/cddp-cpp/cddp_core/asddp_solver.hpp deleted file mode 100644 index 3f6d3677..00000000 --- a/include/cddp-cpp/cddp_core/asddp_solver.hpp +++ /dev/null @@ -1,128 +0,0 @@ -/* - Copyright 2025 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#ifndef CDDP_ASDDP_SOLVER_HPP -#define CDDP_ASDDP_SOLVER_HPP - -#include "cddp_core/cddp_core.hpp" -#include "cddp_core/constraint.hpp" -#include "osqp++.h" -#include -#include - -namespace cddp { - -/** - * @brief Active Set Constrained DDP (ASDDP) solver implementation. - * - * This class implements the ISolverAlgorithm interface to provide - * an active set constrained DDP solver that uses OSQP to solve - * QP subproblems at each time step for constraint handling. - * - * Uses OSQP as the internal QP solver for handling constrained - * optimization subproblems in the forward pass. - */ -class ASDDPSolver : public ISolverAlgorithm { -public: - /** - * @brief Default constructor. - */ - ASDDPSolver(); - - /** - * @brief Initialize the solver with the given CDDP context. - * @param context Reference to the CDDP instance containing problem data and - * options. - */ - void initialize(CDDP &context) override; - - /** - * @brief Execute the ASDDP algorithm and return the solution. - * @param context Reference to the CDDP instance containing problem data and - * options. - * @return CDDPSolution containing the results. - */ - CDDPSolution solve(CDDP &context) override; - - /** - * @brief Get the name of the solver algorithm. - * @return String identifier "ASDDP". - */ - std::string getSolverName() const override; - -private: - // Control law parameters - std::vector k_u_; ///< Feedforward control gains - std::vector K_u_; ///< Feedback control gains - Eigen::Vector2d dV_; ///< Expected value function change - - // Q-function matrices for active set method - std::vector Q_UU_; ///< Control Hessian matrices - std::vector Q_UX_; ///< Control-state cross-derivatives - std::vector Q_U_; ///< Control gradients - - /** - * @brief Perform backward pass (Riccati recursion) with active set method. - * @param context Reference to the CDDP context. - * @return True if backward pass succeeds, false otherwise. - */ - bool backwardPass(CDDP &context); - - /** - * @brief Perform forward pass with line search and OSQP-based constraint - * handling. - * @param context Reference to the CDDP context. - * @return Best forward pass result. - */ - ForwardPassResult performForwardPass(CDDP &context); - - /** - * @brief Perform single forward pass with given step size using OSQP. - * @param context Reference to the CDDP context. - * @param alpha Step size for the forward pass. - * @return Forward pass result. - */ - ForwardPassResult forwardPass(CDDP &context, double alpha); - - /** - * @brief Compute the current cost given the trajectories. - * @param context Reference to the CDDP context. - */ - void computeCost(CDDP &context); - - /** - * @brief Compute total constraint violation. - * @param context Reference to the CDDP context. - * @return Total constraint violation. - */ - double computeConstraintViolation(CDDP &context); - - /** - * @brief Print iteration information. - */ - void printIteration(int iter, double cost, double merit, double inf_du, - double regularization, double alpha) const; - - /** - * @brief Print solution summary. - * @param solution The solution to print. - */ - void printSolutionSummary(const CDDPSolution &solution) const; -}; - -} // namespace cddp - -#endif // CDDP_ASDDP_SOLVER_HPP diff --git a/include/cddp-cpp/cddp_core/cddp_core.hpp b/include/cddp-cpp/cddp_core/cddp_core.hpp index f6d3396f..58b37df6 100644 --- a/include/cddp-cpp/cddp_core/cddp_core.hpp +++ b/include/cddp-cpp/cddp_core/cddp_core.hpp @@ -44,7 +44,6 @@ namespace cddp { */ enum class SolverType { CLDDP, ///< Control-Limited Differential Dynamic Programming - ASDDP, ///< Active Set Differential Dynamic Programming LogDDP, ///< Log-Barrier Differential Dynamic Programming IPDDP, ///< Interior Point Differential Dynamic Programming MSIPDDP, ///< Multi-Shooting Interior Point Differential Dynamic Programming @@ -309,7 +308,7 @@ class CDDP { * @brief Solves the optimal control problem using the specified algorithm * (string version for backward compatibility). * @param solver_type A string identifying the solver algorithm to use (e.g., - * "CLDDP", "ASDDP", "LOGDDP", "IPDDP", "MSIPDDP"). + * "CLDDP", "LOGDDP", "IPDDP", "MSIPDDP"). * @return CDDPSolution A map containing the solution details. */ CDDPSolution solve(const std::string &solver_type); @@ -460,4 +459,4 @@ class CDDP { }; } // namespace cddp -#endif // CDDP_CDDP_CORE_HPP \ No newline at end of file +#endif // CDDP_CDDP_CORE_HPP diff --git a/include/cddp-cpp/cddp_core/neural_dynamical_system.hpp b/include/cddp-cpp/cddp_core/neural_dynamical_system.hpp deleted file mode 100644 index 56823251..00000000 --- a/include/cddp-cpp/cddp_core/neural_dynamical_system.hpp +++ /dev/null @@ -1,167 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#ifndef CDDP_NEURAL_DYNAMICAL_SYSTEM_HPP -#define CDDP_NEURAL_DYNAMICAL_SYSTEM_HPP - -#include -#include -#include -#include -#include "cddp_core/dynamical_system.hpp" - -namespace cddp { -/** - * @brief Interface for a neural network model representing system dynamics. - */ - -class DynamicsModelInterface : public torch::nn::Module { -public: - virtual torch::Tensor forward(std::vector inputs) = 0; - virtual ~DynamicsModelInterface() = default; -}; - -/** - * @brief A NeuralDynamicalSystem that uses a PyTorch model to represent system dynamics. - */ -class NeuralDynamicalSystem : public DynamicalSystem { -public: - /** - * @brief Construct a new NeuralDynamicalSystem object - * - * @param state_dim Dimension of the system state - * @param control_dim Dimension of the system control - * @param timestep Integration timestep - * @param integration_type Type of numerical integration (Euler, Heun, RK3, RK4) - * @param model A torch::nn::Module (e.g. an MLP) representing the learned dynamics, - * @param device Device to run the model on (CPU or CUDA) - */ - NeuralDynamicalSystem(int state_dim, - int control_dim, - double timestep, - const std::string& integration_type, - std::shared_ptr model, - torch::Device device = torch::kCPU); - - /** - * @brief Compute continuous-time dynamics: x_dot = f(x, u). - * - * @param state Current state (Eigen vector) - * @param control Current control (Eigen vector) - * @param time Current time - * @return Eigen::VectorXd Continuous-time derivative of state - */ - Eigen::VectorXd getContinuousDynamics(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const override; - - /** - * @brief Compute discrete-time dynamics: x_{t+1} = f(x_t, u_t). - * - * @param state Current state (Eigen vector) - * @param control Current control (Eigen vector) - * @param time Current time - * @return Eigen::VectorXd Discrete next state - */ - Eigen::VectorXd getDiscreteDynamics(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const override; - - /** - * @brief Jacobian of the dynamics w.r.t. state: df/dx - * - * @param state Current state (Eigen vector) - * @param control Current control (Eigen vector) - * @param time Current time - * @return Eigen::MatrixXd Jacobian df/dx - */ - Eigen::MatrixXd getStateJacobian(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const override; - - /** - * @brief Jacobian of the dynamics w.r.t. control: df/du - * - * @param state Current state (Eigen vector) - * @param control Current control (Eigen vector) - * @param time Current time - * @return Eigen::MatrixXd Jacobian df/du - */ - Eigen::MatrixXd getControlJacobian(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const override; - - /** - * @brief Hessian of the dynamics w.r.t. state - * - * @param state Current state (Eigen vector) - * @param control Current control (Eigen vector) - * @param time Current time - * @return std::vector Vector of Hessian matrices, one per state dimension - */ - std::vector getStateHessian(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const override; - - /** - * @brief Hessian of the dynamics w.r.t. control - * - * @param state Current state (Eigen vector) - * @param control Current control (Eigen vector) - * @param time Current time - * @return std::vector Vector of Hessian matrices, one per state dimension - */ - std::vector getControlHessian(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const override; - - /** - * @brief Hessian of the dynamics w.r.t. state and control - * - * @param state Current state (Eigen vector) - * @param control Current control (Eigen vector) - * @param time Current time - * @return std::vector Vector of Hessian matrices, one per state dimension - */ - std::vector getCrossHessian(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const override; - - /** - * @brief Autodiff version of continuous dynamics using second-order duals. - * For NeuralDynamicalSystem, this method indicates that base class autodiff - * is not directly supported due to the external Torch model. - * @param state Current state vector (autodiff type) - * @param control Current control input (autodiff type) - * @param time Current time - * @return State derivative vector (autodiff type) - */ - VectorXdual2nd getContinuousDynamicsAutodiff( - const VectorXdual2nd& state, - const VectorXdual2nd& control, - double time) const override; - -private: - std::shared_ptr model_; - torch::Device device_; - - // Helper methods for tensor conversions - torch::Tensor eigenToTorch(const Eigen::VectorXd& eigen_vec, bool requires_grad = false) const; - Eigen::VectorXd torchToEigen(const torch::Tensor& tensor) const; -}; -} // namespace cddp - -#endif // CDDP_NEURAL_DYNAMICAL_SYSTEM_HPP diff --git a/include/cddp-cpp/sqp_core/sqp_core.hpp b/include/cddp-cpp/sqp_core/sqp_core.hpp deleted file mode 100644 index 685dc90e..00000000 --- a/include/cddp-cpp/sqp_core/sqp_core.hpp +++ /dev/null @@ -1,178 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#ifndef CDDP_SQP_CORE_HPP -#define CDDP_SQP_CORE_HPP - -#include -#include -#include -#include -#include -#include -#include - -#include "cddp_core/dynamical_system.hpp" -#include "cddp_core/objective.hpp" -#include "cddp_core/constraint.hpp" - -namespace cddp { - -/** - * @brief Configuration options for SCP solver - */ -struct SCPOptions { - int max_iterations = 100; // Maximum number of iterations - int min_iterations = 1; // Minimum number of iterations - double ftol = 1e-6; // Function value tolerance - double xtol = 1e-6; // Step size tolerance - double gtol = 1e-6; // Gradient tolerance - double merit_penalty = 100.0; // Penalty for constraint violation in merit function - bool verbose = false; // Verbosity flag - - // Trust region parameters - double trust_region_radius = 100.0; // Initial trust region radius - double trust_region_radius_max = 1e6; // Maximum trust region radius - double trust_region_increase_factor = 2.0; // Increase factor when step is good - double trust_region_decrease_factor = 0.5; // Decrease factor when step is rejected - - // IPOPT specific options - int ipopt_max_iter = 1000; - int ipopt_print_level = 5; - double ipopt_tol = 1e-6; -}; - -/** - * @brief Results from SCP optimization. - */ -struct SCPResult { - bool success; // Whether optimization succeeded - int iterations; // Number of iterations taken - double objective_value; // Final objective value - double constraint_violation; // Final constraint violation - std::vector X; // Optimal state trajectory (size: horizon+1) - std::vector U; // Optimal control trajectory (size: horizon) - std::vector obj_history; // History of objective values - std::vector viol_history; // History of constraint violations - double solve_time; // Solution time in seconds -}; - -/** - * @brief Sequential Convex Programming solver - */ -class SCPSolver { -public: - /** - * @brief Constructor. - * @param initial_state Initial state of the system. - * @param reference_state Desired (goal) state. - * @param horizon Time horizon (number of control intervals; the state trajectory will have horizon+1 points). - * @param timestep Time step. - */ - SCPSolver(const Eigen::VectorXd& initial_state, - const Eigen::VectorXd& reference_state, - int horizon, - double timestep); - - // Setter methods - void setDynamicalSystem(std::unique_ptr system) { system_ = std::move(system); } - void setInitialState(const Eigen::VectorXd& initial_state) { initial_state_ = initial_state; } - void setReferenceState(const Eigen::VectorXd& reference_state) { reference_state_ = reference_state; } - void setHorizon(int horizon) { horizon_ = horizon; } - void setTimestep(double timestep) { timestep_ = timestep; } - void setOptions(const SCPOptions& options) { options_ = options; } - void setObjective(std::unique_ptr objective) { objective_ = std::move(objective); } - void setInitialTrajectory(const std::vector& X, - const std::vector& U); - - /** - * @brief Solve the optimization problem. - * @return SCPResult with the solution details. - */ - SCPResult solve(); - - /** - * @brief Add a constraint to the problem. - * @param constraint_name Constraint name. - * @param constraint Constraint object. - */ - void addConstraint(const std::string& constraint_name, std::unique_ptr constraint) { - constraint_set_[constraint_name] = std::move(constraint); - } - - /** - * @brief Get a specific constraint by name. - * @tparam T Type of constraint. - * @param name Constraint name. - * @return Pointer to the constraint (or nullptr if not found). - */ - template - T* getConstraint(const std::string& name) const { - auto it = constraint_set_.find(name); - if (it == constraint_set_.end()) { - return nullptr; - } - return dynamic_cast(it->second.get()); - } - - // Getters - // Returns a pointer to the dynamical system. - const DynamicalSystem* getDynamicalSystem() const { return system_.get(); } - - // Returns a pointer to the objective. - const Objective* getObjective() const { return objective_.get(); } - -private: - SCPOptions options_; - - Eigen::VectorXd initial_state_; - Eigen::VectorXd reference_state_; - int horizon_; - double timestep_; - - std::unique_ptr system_; - std::unique_ptr objective_; - std::map> constraint_set_; - - // Current trajectory estimates. - // X: state trajectory (size: horizon_+1), U: control trajectory (size: horizon_) - std::vector X_; - std::vector U_; - - // Helper routines. - void initializeSCP(); - void computeLinearizedDynamics(const std::vector& X, - const std::vector& U, - std::vector& A, - std::vector& B) const; - - bool satisfies_trust_region_constraints(const std::vector& X, - const std::vector& X_prev, - double Delta) { - if (X.size() != X_prev.size()) { - throw std::runtime_error("Trajectory size mismatch in trust-region check."); - } - for (size_t t = 0; t < X.size(); ++t) { - if ((X[t] - X_prev[t]).norm() > Delta) { - return false; - } - } - return true; - } -}; - -} // namespace cddp - -#endif // CDDP_SQP_CORE_HPP diff --git a/src/cddp_core/asddp_solver.cpp b/src/cddp_core/asddp_solver.cpp deleted file mode 100644 index a11a1d01..00000000 --- a/src/cddp_core/asddp_solver.cpp +++ /dev/null @@ -1,822 +0,0 @@ -/* - Copyright 2025 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include "cddp_core/asddp_solver.hpp" -#include "cddp_core/cddp_core.hpp" -#include "osqp++.h" -#include "absl/status/status.h" -#include -#include -#include -#include -#include -#include - -namespace cddp { - -ASDDPSolver::ASDDPSolver() {} - -void ASDDPSolver::initialize(CDDP &context) { - const CDDPOptions &options = context.getOptions(); - - int horizon = context.getHorizon(); - int control_dim = context.getControlDim(); - int state_dim = context.getStateDim(); - - // For warm starts, verify that existing state is valid - if (options.warm_start) { - bool valid_warm_start = (k_u_.size() == static_cast(horizon) && - K_u_.size() == static_cast(horizon) && - Q_UU_.size() == static_cast(horizon) && - Q_UX_.size() == static_cast(horizon) && - Q_U_.size() == static_cast(horizon)); - - if (valid_warm_start && !k_u_.empty()) { - for (int t = 0; t < horizon; ++t) { - if (k_u_[t].size() != control_dim || K_u_[t].rows() != control_dim || - K_u_[t].cols() != state_dim) { - valid_warm_start = false; - break; - } - } - } else { - valid_warm_start = false; - } - - if (valid_warm_start) { - if (options.verbose) { - std::cout << "ASDDP: Using warm start with existing control gains" - << std::endl; - } - if (!context.X_.empty() && !context.U_.empty()) { - computeCost(context); - } - return; - } else { - if (options.verbose) { - std::cout << "ASDDP: Warning - warm start requested but no valid " - "solver state found. " - << "Falling back to cold start initialization." << std::endl; - } - } - } - - // Cold start: full initialization - k_u_.resize(horizon); - K_u_.resize(horizon); - Q_UU_.resize(horizon); - Q_UX_.resize(horizon); - Q_U_.resize(horizon); - - for (int t = 0; t < horizon; ++t) { - k_u_[t] = Eigen::VectorXd::Zero(control_dim); - K_u_[t] = Eigen::MatrixXd::Zero(control_dim, state_dim); - } - - dV_ = Eigen::Vector2d::Zero(); - - // Compute initial cost if trajectories exist - if (!context.X_.empty() && !context.U_.empty()) { - computeCost(context); - } -} - -CDDPSolution ASDDPSolver::solve(CDDP &context) { - const CDDPOptions &options = context.getOptions(); - - // Print solver header if requested - if (options.print_solver_header) { - context.printSolverInfo(); - } - - // Print solver options if requested - if (options.print_solver_options) { - context.printOptions(options); - } - - // Prepare solution map - CDDPSolution solution; - solution["solver_name"] = getSolverName(); - solution["status_message"] = std::string("Running"); - solution["iterations_completed"] = 0; - solution["solve_time_ms"] = 0.0; - - // Initialize history vectors only if requested - std::vector history_objective; - std::vector history_merit_function; - std::vector history_step_length_primal; - std::vector history_dual_infeasibility; - std::vector history_regularization; - - if (options.return_iteration_info) { - const size_t expected_size = - static_cast(options.max_iterations + 1); - history_objective.reserve(expected_size); - history_merit_function.reserve(expected_size); - history_step_length_primal.reserve(expected_size); - history_dual_infeasibility.reserve(expected_size); - history_regularization.reserve(expected_size); - - // Initial iteration values - history_objective.push_back(context.cost_); - history_merit_function.push_back(context.merit_function_); - history_dual_infeasibility.push_back(context.inf_du_); - history_regularization.push_back(context.regularization_); - } - - if (options.verbose) { - printIteration(0, context.cost_, context.merit_function_, context.inf_du_, - context.regularization_, context.alpha_pr_); - } - - // Start timer - auto start_time = std::chrono::high_resolution_clock::now(); - int iter = 0; - bool converged = false; - std::string termination_reason = "MaxIterationsReached"; - - // Main ASDDP loop - while (iter < options.max_iterations) { - ++iter; - - // Check maximum CPU time - if (options.max_cpu_time > 0) { - auto current_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast( - current_time - start_time); - if (duration.count() > options.max_cpu_time * 1000) { - termination_reason = "MaxCpuTimeReached"; - if (options.verbose) { - std::cerr - << "ASDDP: Maximum CPU time reached. Returning current solution" - << std::endl; - } - break; - } - } - - // 1. Backward pass - bool backward_pass_success = false; - while (!backward_pass_success) { - backward_pass_success = backwardPass(context); - - if (!backward_pass_success) { - context.increaseRegularization(); - if (context.isRegularizationLimitReached()) { - termination_reason = "RegularizationLimit_NotConverged"; - if (options.verbose) { - std::cerr << "ASDDP: Backward pass regularization limit reached" - << std::endl; - } - break; - } - } - } - - if (!backward_pass_success) - break; - - // Check convergence - if (context.inf_du_ < options.tolerance) { - converged = true; - termination_reason = "OptimalSolutionFound"; - break; - } - - // 2. Forward pass - ForwardPassResult best_result = performForwardPass(context); - - // Update solution if forward pass succeeded - if (best_result.success) { - context.X_ = best_result.state_trajectory; - context.U_ = best_result.control_trajectory; - double dJ = context.cost_ - best_result.cost; - context.cost_ = best_result.cost; - context.merit_function_ = - best_result.cost; // For ASDDP, merit function equals cost - context.alpha_pr_ = best_result.alpha_pr; - - // Store history only if requested - if (options.return_iteration_info) { - history_objective.push_back(context.cost_); - history_merit_function.push_back(context.merit_function_); - history_step_length_primal.push_back(context.alpha_pr_); - history_dual_infeasibility.push_back(context.inf_du_); - history_regularization.push_back(context.regularization_); - } - - context.decreaseRegularization(); - - // Check convergence - if (dJ < options.acceptable_tolerance) { - converged = true; - termination_reason = "AcceptableSolutionFound"; - break; - } - } else { - context.increaseRegularization(); - - // Check if regularization limit reached - if (context.isRegularizationLimitReached()) { - termination_reason = "RegularizationLimitReached_NotConverged"; - converged = false; - if (options.verbose) { - std::cerr << "ASDDP: Regularization limit reached. Not converged." - << std::endl; - } - break; - } - } - - // Print iteration info - if (options.verbose) { - printIteration(iter, context.cost_, context.merit_function_, - context.inf_du_, context.regularization_, - context.alpha_pr_); - } - } - - // Compute final timing - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast( - end_time - start_time); - - // Compute final constraint violation - context.inf_pr_ = computeConstraintViolation(context); - - // Populate final solution - solution["status_message"] = termination_reason; - solution["iterations_completed"] = iter; - solution["solve_time_ms"] = static_cast(duration.count()); - solution["final_objective"] = context.cost_; - solution["final_step_length"] = context.alpha_pr_; - solution["final_dual_infeasibility"] = context.inf_du_; - solution["final_primal_infeasibility"] = context.inf_pr_; - - // Add trajectories - std::vector time_points; - time_points.reserve(static_cast(context.getHorizon() + 1)); - for (int t = 0; t <= context.getHorizon(); ++t) { - time_points.push_back(t * context.getTimestep()); - } - solution["time_points"] = time_points; - solution["state_trajectory"] = context.X_; - solution["control_trajectory"] = context.U_; - - // Add iteration history if requested - if (options.return_iteration_info) { - solution["history_objective"] = history_objective; - solution["history_merit_function"] = history_merit_function; - solution["history_step_length_primal"] = history_step_length_primal; - solution["history_dual_infeasibility"] = history_dual_infeasibility; - solution["history_regularization"] = history_regularization; - } - - // Add control gains - solution["control_feedback_gains_K"] = K_u_; - - // Final metrics - solution["final_regularization"] = context.regularization_; - - if (options.verbose) { - printSolutionSummary(solution); - } - - return solution; -} - -std::string ASDDPSolver::getSolverName() const { return "ASDDP"; } - -bool ASDDPSolver::backwardPass(CDDP &context) { - const CDDPOptions &options = context.getOptions(); - const int state_dim = context.getStateDim(); - const int control_dim = context.getControlDim(); - const int horizon = context.getHorizon(); - const int dual_dim = context.getTotalDualDim() - control_dim; - const double timestep = context.getTimestep(); - const auto active_set_tol = 1e-6; - - // Extract control box constraint - auto control_box_constraint = - context.getConstraint("ControlBoxConstraint"); - - // Terminal cost and its derivatives - Eigen::VectorXd V_x = - context.getObjective().getFinalCostGradient(context.X_.back()); - Eigen::MatrixXd V_xx = - context.getObjective().getFinalCostHessian(context.X_.back()); - - // Pre-allocate matrices - Eigen::MatrixXd A(state_dim, state_dim); - Eigen::MatrixXd B(state_dim, control_dim); - Eigen::VectorXd Q_x(state_dim); - Eigen::VectorXd Q_u(control_dim); - Eigen::MatrixXd Q_xx(state_dim, state_dim); - Eigen::MatrixXd Q_uu(control_dim, control_dim); - Eigen::MatrixXd Q_uu_reg(control_dim, control_dim); - Eigen::MatrixXd Q_ux(control_dim, state_dim); - Eigen::MatrixXd Q_ux_reg(control_dim, state_dim); - Eigen::VectorXd k(control_dim); - Eigen::MatrixXd K(control_dim, state_dim); - - dV_ = Eigen::Vector2d::Zero(); - double Qu_error = 0.0; - - // Backward Riccati recursion - for (int t = horizon - 1; t >= 0; --t) { - const Eigen::VectorXd &x = context.X_[t]; - const Eigen::VectorXd &u = context.U_[t]; - - // Get continuous dynamics Jacobians - const auto [Fx, Fu] = context.getSystem().getJacobians(x, u, t * timestep); - - // Convert to discrete time - A = timestep * Fx; - A.diagonal().array() += 1.0; - B = timestep * Fu; - - // Get cost and its derivatives - auto [l_x, l_u] = context.getObjective().getRunningCostGradients(x, u, t); - auto [l_xx, l_uu, l_ux] = - context.getObjective().getRunningCostHessians(x, u, t); - - // Compute Q-function matrices - Q_x = l_x + A.transpose() * V_x; - Q_u = l_u + B.transpose() * V_x; - Q_xx = l_xx + A.transpose() * V_xx * A; - Q_ux = l_ux + B.transpose() * V_xx * A; - Q_uu = l_uu + B.transpose() * V_xx * B; - - // Apply regularization - state regularization to value function - Eigen::MatrixXd V_xx_reg = V_xx + context.regularization_ * Eigen::MatrixXd::Identity(state_dim, state_dim); - - // Compute regularized Q-function matrices - Q_ux_reg = l_ux + B.transpose() * V_xx_reg * A; - Q_uu_reg = l_uu + B.transpose() * V_xx_reg * B; - - // Apply control regularization - Q_uu_reg.diagonal().array() += context.regularization_; - - // Check positive definiteness - Eigen::EigenSolver es(Q_uu_reg); - if (es.eigenvalues().real().minCoeff() <= 0) { - if (options.debug) { - std::cerr << "ASDDP: Q_uu is not positive definite at time " << t - << std::endl; - } - return false; - } - - /* --- Identify Active Constraint --- */ - int active_constraint_index = 0; - Eigen::MatrixXd C(dual_dim, control_dim); // Control constraint matrix - Eigen::MatrixXd D(dual_dim, state_dim); // State constraint matrix - - // Identify control constraints - if (control_box_constraint != nullptr) { - for (int j = 0; j < control_dim; j++) { - if (u(j) <= - control_box_constraint->getLowerBound()(j) + active_set_tol) { - Eigen::VectorXd e = Eigen::VectorXd::Zero(control_dim); - e(j) = 1.0; - C.row(active_constraint_index) = -e; // Note the negative sign - D.row(active_constraint_index) = Eigen::VectorXd::Zero(state_dim); - active_constraint_index += 1; - } else if (u(j) >= control_box_constraint->getUpperBound()(j) - - active_set_tol) { - Eigen::VectorXd e = Eigen::VectorXd::Zero(control_dim); - e(j) = 1.0; // No negative here - C.row(active_constraint_index) = e; - D.row(active_constraint_index) = Eigen::VectorXd::Zero(state_dim); - active_constraint_index += 1; - } - } - } - - // Identify state constraints - const auto &constraint_set = context.getConstraintSet(); - if (t < horizon - 1) { - for (const auto &[name, constraint] : constraint_set) { - if (name == "ControlBoxConstraint") { - continue; - } - - Eigen::VectorXd constraint_vals = constraint->evaluate(context.X_[t + 1], context.U_[t + 1]) - constraint->getUpperBound(); - Eigen::MatrixXd cons_jac_x = constraint->getStateJacobian(context.X_[t + 1], context.U_[t + 1]); - Eigen::MatrixXd cons_jac_u = constraint->getControlJacobian(context.X_[t + 1], context.U_[t + 1]); - - for (int j = 0; j < constraint_vals.size(); j++) { - if (std::abs(constraint_vals(j)) <= active_set_tol) { - C.row(active_constraint_index) = cons_jac_x.row(j) * B; - D.row(active_constraint_index) = cons_jac_x.row(j) * A; - active_constraint_index++; - } - } - } - } - - if (active_constraint_index == 0) { // No active constraints - const Eigen::MatrixXd &H = Q_uu_reg.inverse(); - k = -H * Q_u; - K = -H * Q_ux_reg; - } else { - // Extract identified active constraints - Eigen::MatrixXd grad_x_g = D.topRows(active_constraint_index); - Eigen::MatrixXd grad_u_g = C.topRows(active_constraint_index); - - // Calculate Lagrange multipliers - Eigen::MatrixXd Q_uu_inv = Q_uu_reg.inverse(); - Eigen::MatrixXd lambda = - -(grad_u_g * Q_uu_inv * grad_u_g.transpose()).inverse() * - (grad_u_g * Q_uu_inv * Q_u); - - // Find indices where lambda is non-negative - std::vector active_indices; - for (int i = 0; i < lambda.rows(); ++i) { - if (lambda(i) >= 0) { - active_indices.push_back(i); - } - } - int active_count_new = active_indices.size(); - - // Create new constraint matrices - Eigen::MatrixXd C_new = - Eigen::MatrixXd::Zero(active_count_new, control_dim); - Eigen::MatrixXd D_new = - Eigen::MatrixXd::Zero(active_count_new, state_dim); - - if (active_count_new > 0) { - // Fill new constraint matrices with active constraints - for (int i = 0; i < active_count_new; ++i) { - C_new.row(i) = grad_u_g.row(active_indices[i]); - D_new.row(i) = grad_x_g.row(active_indices[i]); - } - - // Calculate feedback gains - Eigen::MatrixXd W = -(C_new * Q_uu_inv * C_new.transpose()).inverse() * - (C_new * Q_uu_inv); - Eigen::MatrixXd H = - Q_uu_inv * (Eigen::MatrixXd::Identity(control_dim, control_dim) - - C_new.transpose() * W); - k = -H * Q_u; - K = -H * Q_ux_reg + W.transpose() * D_new; - } else { - // If no active constraints remain, revert to unconstrained solution - Eigen::MatrixXd H = Q_uu_reg.inverse(); - K = -H * Q_ux_reg; - k = -H * Q_u; - } - } - - // Store Q-function matrices and gains - Q_UU_[t] = Q_uu_reg; - Q_UX_[t] = Q_ux_reg; - Q_U_[t] = Q_u; - k_u_[t] = k; - K_u_[t] = K; - - // Update value function - use original Q matrices for value function recursion - Eigen::Vector2d dV_step; - dV_step << Q_u.dot(k), 0.5 * k.dot(Q_uu * k); - dV_ += dV_step; - - V_x = Q_x + K.transpose() * Q_uu * k + Q_ux.transpose() * k + - K.transpose() * Q_u; - V_xx = Q_xx + K.transpose() * Q_uu * K + Q_ux.transpose() * K + - K.transpose() * Q_ux; - V_xx = 0.5 * (V_xx + V_xx.transpose()); // Symmetrize - - // Compute optimality gap (Inf-norm) for convergence check - simplified like asddp_core.cpp - Qu_error = std::max(Qu_error, Q_u.lpNorm()); - } - - // Simplified dual infeasibility computation like asddp_core.cpp - context.inf_du_ = Qu_error; - - if (options.debug) { - std::cout << "Qu_error: " << Qu_error << std::endl; - std::cout << "dV: " << dV_.transpose() << std::endl; - } - - return true; -} - -ForwardPassResult ASDDPSolver::performForwardPass(CDDP &context) { - const CDDPOptions &options = context.getOptions(); - ForwardPassResult best_result; - best_result.cost = std::numeric_limits::infinity(); - best_result.success = false; - - if (!options.enable_parallel) { - // Single-threaded execution with early termination - for (double alpha : context.alphas_) { - ForwardPassResult result = forwardPass(context, alpha); - - if (result.success && result.cost < best_result.cost) { - best_result = result; - if (result.success) { - break; // Early termination - } - } - } - } else { - // Multi-threaded execution - std::vector> futures; - futures.reserve(context.alphas_.size()); - - for (double alpha_pr : context.alphas_) { - futures.push_back( - std::async(std::launch::async, [this, &context, alpha_pr]() { - return forwardPass(context, alpha_pr); - })); - } - - for (auto &future : futures) { - try { - if (future.valid()) { - ForwardPassResult result = future.get(); - if (result.success && result.cost < best_result.cost) { - best_result = result; - } - } - } catch (const std::exception &e) { - if (options.verbose) { - std::cerr << "ASDDP: Forward pass thread failed: " << e.what() - << std::endl; - } - } - } - } - - return best_result; -} - -ForwardPassResult ASDDPSolver::forwardPass(CDDP &context, double alpha_pr) { - const CDDPOptions &options = context.getOptions(); - - ForwardPassResult result; - result.success = false; - result.cost = std::numeric_limits::infinity(); - result.merit_function = std::numeric_limits::infinity(); - result.alpha_pr = alpha_pr; - - const int state_dim = context.getStateDim(); - const int control_dim = context.getControlDim(); - const int dual_dim = context.getTotalDualDim() - control_dim; - const double timestep = context.getTimestep(); - - // Extract control box constraint - auto control_box_constraint = - context.getConstraint("ControlBoxConstraint"); - - // Initialize trajectories - result.state_trajectory = context.X_; - result.control_trajectory = context.U_; - result.state_trajectory[0] = context.getInitialState(); - - double J_new = 0.0; - - // Forward simulation with OSQP - for (int t = 0; t < context.getHorizon(); ++t) { - const Eigen::VectorXd &x = result.state_trajectory[t]; - const Eigen::VectorXd &u = result.control_trajectory[t]; - const Eigen::VectorXd delta_x = x - context.X_[t]; - - // Extract Q-function matrices computed in the backward pass - const Eigen::VectorXd &Q_u = Q_U_[t]; - const Eigen::MatrixXd &Q_uu = Q_UU_[t]; - const Eigen::MatrixXd &Q_ux = Q_UX_[t]; - - // Create QP problem - Eigen::SparseMatrix P = Q_uu.sparseView(); - P.makeCompressed(); - - // Form the gradient of the QP objective: q = alpha * Q_u + Q_ux * delta_x - Eigen::VectorXd q = alpha_pr * Q_u + Q_ux * delta_x; - - // Create QP constraints - Eigen::MatrixXd A_dense = - Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::VectorXd lb_dense = control_box_constraint->getLowerBound() - u; - Eigen::VectorXd ub_dense = control_box_constraint->getUpperBound() - u; - - Eigen::MatrixXd A_aug = Eigen::MatrixXd::Zero(dual_dim, control_dim); - Eigen::VectorXd lb_aug = Eigen::VectorXd::Zero(dual_dim); - Eigen::VectorXd ub_aug = Eigen::VectorXd::Zero(dual_dim); - - // First block: control constraints - A_aug.topRows(control_dim) = A_dense; - lb_aug.head(control_dim) = lb_dense; - ub_aug.head(control_dim) = ub_dense; - - // Second block: state constraints - int row_index = control_dim; - if (t < context.getHorizon() - 1) { - auto [fx, fu] = context.getSystem().getJacobians(x, u, t * timestep); - Eigen::MatrixXd Fu = timestep * fu; - - // Predicted next state - Eigen::VectorXd x_next = - context.getSystem().getDiscreteDynamics(x, u, t * timestep); - - const auto &constraint_set = context.getConstraintSet(); - for (const auto &[name, constraint] : constraint_set) { - if (name == "ControlBoxConstraint") { - continue; - } - Eigen::VectorXd cons_vals = constraint->evaluate(x_next, u) - constraint->getUpperBound(); - Eigen::MatrixXd cons_jac_x = constraint->getStateJacobian(x_next, u); - - int m = cons_vals.size(); - A_aug.block(row_index, 0, m, control_dim) = cons_jac_x * Fu; - lb_aug.segment(row_index, m) - .setConstant(-std::numeric_limits::infinity()); - ub_aug.segment(row_index, m) = -cons_vals; - row_index += m; - } - } - - // Convert augmented constraint matrix to sparse format - Eigen::SparseMatrix A_sparse = A_aug.sparseView(); - - // Initialize QP solver - osqp::OsqpInstance instance; - instance.objective_matrix = P; - instance.objective_vector = q; - instance.constraint_matrix = A_sparse; - instance.lower_bounds = lb_aug; - instance.upper_bounds = ub_aug; - - // Solve the QP problem - osqp::OsqpSolver osqp_solver; - osqp::OsqpSettings osqp_settings; - osqp_settings.warm_start = true; - osqp_settings.verbose = false; - - try { - absl::Status init_status = osqp_solver.Init(instance, osqp_settings); - if (!init_status.ok()) { - if (options.debug) { - std::cerr << "ASDDP: QP solver initialization failed at time step " << t - << ": " << init_status.message() << std::endl; - } - result.success = false; - return result; - } - - osqp::OsqpExitCode exit_code = osqp_solver.Solve(); - - if (exit_code != osqp::OsqpExitCode::kOptimal) { - if (options.debug) { - std::cerr << "ASDDP: QP solver failed at time step " << t - << std::endl; - } - result.success = false; - return result; - } - - // Update control using the QP solution delta_u - Eigen::VectorXd delta_u = osqp_solver.primal_solution(); - result.control_trajectory[t] += delta_u; - } catch (const std::exception &e) { - if (options.debug) { - std::cerr << "ASDDP: OSQP exception at time step " << t << ": " - << e.what() << std::endl; - } - result.success = false; - return result; - } - - // Compute running cost and propagate state - J_new += - context.getObjective().running_cost(x, result.control_trajectory[t], t); - result.state_trajectory[t + 1] = context.getSystem().getDiscreteDynamics( - x, result.control_trajectory[t], t * context.getTimestep()); - } - - // Add terminal cost - J_new += context.getObjective().terminal_cost(result.state_trajectory.back()); - - // Compute actual cost reduction and the predicted improvement - double dJ = context.cost_ - J_new; - double expected = -alpha_pr * (dV_(0) + 0.5 * alpha_pr * dV_(1)); - double reduction_ratio = - (expected > 0.0) ? dJ / expected : std::copysign(1.0, dJ); - - // Acceptance criterion - if (dJ <= 0) { - if (options.debug) { - std::cerr << "ASDDP: Forward pass did not yield sufficient decrease (dJ: " - << dJ << ", reduction_ratio: " << reduction_ratio << ")" - << std::endl; - } - result.success = false; - } else { - result.success = true; - result.cost = J_new; - } - - return result; -} - -void ASDDPSolver::computeCost(CDDP &context) { - context.cost_ = 0.0; - - // Running costs - for (int t = 0; t < context.getHorizon(); ++t) { - context.cost_ += - context.getObjective().running_cost(context.X_[t], context.U_[t], t); - } - - // Terminal cost - context.cost_ += context.getObjective().terminal_cost(context.X_.back()); - context.merit_function_ = - context.cost_; // For ASDDP, merit function equals cost - - // Compute constraint violation - context.inf_pr_ = computeConstraintViolation(context); -} - -double ASDDPSolver::computeConstraintViolation(CDDP &context) { - double total_violation = 0.0; - - // Check constraint violations for each time step - for (int t = 0; t <= context.getHorizon(); ++t) { - const Eigen::VectorXd &x = context.X_[t]; - const Eigen::VectorXd &u = (t < context.getHorizon()) ? context.U_[t] : Eigen::VectorXd::Zero(context.getControlDim()); - - // Control box constraints - auto control_box_constraint = - context.getConstraint("ControlBoxConstraint"); - if (control_box_constraint != nullptr && t < context.getHorizon()) { - total_violation += control_box_constraint->computeViolation(x, u); - } - - // Path constraints - const auto &constraint_set = context.getConstraintSet(); - for (const auto &[name, constraint] : constraint_set) { - if (name == "ControlBoxConstraint") { - continue; - } - total_violation += constraint->computeViolation(x, u); - } - } - - return total_violation; -} - -void ASDDPSolver::printIteration(int iter, double cost, double merit, - double inf_du, double regularization, - double alpha) const { - if (iter == 0) { - std::cout << std::setw(4) << "iter" << " " << std::setw(12) << "objective" - << " " << std::setw(12) << "merit" << " " << std::setw(10) - << "inf_du" << " " << std::setw(8) << "lg(rg)" << " " - << std::setw(8) << "alpha" << std::endl; - } - - std::cout << std::setw(4) << iter << " " << std::setw(12) << std::scientific - << std::setprecision(4) << cost << " " << std::setw(12) - << std::scientific << std::setprecision(4) << merit << " " - << std::setw(10) << std::scientific << std::setprecision(2) - << inf_du << " " << std::setw(8) << std::fixed - << std::setprecision(1) << std::log10(regularization) << " " - << std::setw(8) << std::fixed << std::setprecision(4) << alpha - << std::endl; -} - -void ASDDPSolver::printSolutionSummary(const CDDPSolution &solution) const { - std::cout << "\n========================================\n"; - std::cout << " ASDDP Solution Summary\n"; - std::cout << "========================================\n"; - - auto iterations = std::any_cast(solution.at("iterations_completed")); - auto solve_time = std::any_cast(solution.at("solve_time_ms")); - auto final_cost = std::any_cast(solution.at("final_objective")); - auto status = std::any_cast(solution.at("status_message")); - auto final_inf_pr = - std::any_cast(solution.at("final_primal_infeasibility")); - - std::cout << "Status: " << status << "\n"; - std::cout << "Iterations: " << iterations << "\n"; - std::cout << "Solve Time: " << std::setprecision(2) << solve_time << " ms\n"; - std::cout << "Final Cost: " << std::setprecision(6) << final_cost << "\n"; - std::cout << "Final Constraint Violation: " << std::setprecision(4) - << final_inf_pr << "\n"; - std::cout << "========================================\n\n"; -} - -} // namespace cddp diff --git a/src/cddp_core/cddp_core.cpp b/src/cddp_core/cddp_core.cpp index 4a84d294..403a50df 100644 --- a/src/cddp_core/cddp_core.cpp +++ b/src/cddp_core/cddp_core.cpp @@ -16,7 +16,6 @@ #include "cddp_core/cddp_core.hpp" // For CDDP class declaration #include "cddp_core/alddp_solver.hpp" // For AlddpSolver -#include "cddp_core/asddp_solver.hpp" // For ASDDPSolver #include "cddp_core/clddp_solver.hpp" // For CLDDPSolver #include "cddp_core/ipddp_solver.hpp" // For IPDDPSolver #include "cddp_core/logddp_solver.hpp" // For LogDDPSolver @@ -258,8 +257,6 @@ std::string solverTypeToString(SolverType solver_type) { switch (solver_type) { case SolverType::CLDDP: return "CLDDP"; - case SolverType::ASDDP: - return "ASDDP"; case SolverType::LogDDP: return "LogDDP"; case SolverType::IPDDP: @@ -290,8 +287,6 @@ CDDP::createSolver(const std::string &solver_type) { // Fall back to built-in solvers if (solver_type == "CLCDDP" || solver_type == "CLDDP") { return std::make_unique(); - } else if (solver_type == "ASDDP") { - return std::make_unique(); } else if (solver_type == "LogDDP" || solver_type == "LOGDDP") { return std::make_unique(); } else if (solver_type == "IPDDP") { @@ -337,7 +332,7 @@ CDDPSolution CDDP::solve(const std::string &solver_type) { for (const auto &name : available) { std::cout << name << " "; } - std::cout << "CLDDP ASDDP LogDDP IPDDP MSIPDDP ALDDP" << std::endl; + std::cout << "CLDDP LogDDP IPDDP MSIPDDP ALDDP" << std::endl; } return solution; diff --git a/src/cddp_core/neural_dynamical_system.cpp b/src/cddp_core/neural_dynamical_system.cpp deleted file mode 100644 index f8bd4ca6..00000000 --- a/src/cddp_core/neural_dynamical_system.cpp +++ /dev/null @@ -1,251 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include "cddp_core/neural_dynamical_system.hpp" - -namespace cddp { - -NeuralDynamicalSystem::NeuralDynamicalSystem(int state_dim, - int control_dim, - double timestep, - const std::string& integration_type, - std::shared_ptr model, - torch::Device device) - : DynamicalSystem(state_dim, control_dim, timestep, integration_type) - , model_(std::move(model)) - , device_(device) -{ - if (!model_) { - throw std::runtime_error("NeuralDynamicalSystem: Received null model pointer."); - } - - // Move model to desired device - model_->to(device_); - - // Check model dimension - auto dummy_state = torch::zeros({1, state_dim_}, torch::kDouble).to(device_); - auto dummy_control = torch::zeros({1, control_dim_}, torch::kDouble).to(device_); - auto output = model_->forward({dummy_state, dummy_control}); - - // Expected output to be shape [1, state_dim_] - if (output.dim() != 2 || output.size(0) != 1 || output.size(1) != state_dim_) { - throw std::runtime_error( - "NeuralDynamicalSystem: Model output shape mismatch. " - "Expected [1, " + std::to_string(state_dim_) + "] but got " + - std::to_string(output.sizes()[0]) + " x " + std::to_string(output.sizes()[1])); - } -} - -// ---------------------------------------------------------------------------- -// getContinuousDynamics -// ---------------------------------------------------------------------------- -Eigen::VectorXd NeuralDynamicalSystem::getContinuousDynamics(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double /*time*/) const -{ - // We'll assume the model produces x_dot = f(x,u). - // Or if it produces next state, adapt accordingly. - torch::NoGradGuard no_grad; // We don't need gradient for forward pass - - auto state_tensor = eigenToTorch(state); // shape: [1, state_dim_] - auto control_tensor = eigenToTorch(control); // shape: [1, control_dim_] - - auto output = model_->forward({state_tensor, control_tensor}); - // Convert to Eigen - return torchToEigen(output); -} - -// ---------------------------------------------------------------------------- -// getDiscreteDynamics -// ---------------------------------------------------------------------------- -Eigen::VectorXd NeuralDynamicalSystem::getDiscreteDynamics(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const -{ - // A typical approach: x_next = x + x_dot*dt if the model outputs x_dot - // If your model directly outputs next-state, just return the model result. - // For demonstration: x_{t+1} = x + dt * f(x, u) - // Adjust as needed if your model is purely discrete. - - Eigen::VectorXd x_dot = getContinuousDynamics(state, control, time); - return state + x_dot * timestep_; -} - -// ---------------------------------------------------------------------------- -// getStateJacobian -// ---------------------------------------------------------------------------- -Eigen::MatrixXd NeuralDynamicalSystem::getStateJacobian(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const -{ - // Placeholder approach #1: Identity, as a quick stub - // return Eigen::MatrixXd::Identity(state_dim_, state_dim_); - - // Placeholder approach #2: zero - // return Eigen::MatrixXd::Zero(state_dim_, state_dim_); - - // Real approach: use finite difference or PyTorch autograd. - // For illustration, let's do a naive finite-difference: - const double eps = 1e-6; - Eigen::MatrixXd A(state_dim_, state_dim_); - - // Baseline - Eigen::VectorXd f0 = getContinuousDynamics(state, control, time); - - for (int i = 0; i < state_dim_; ++i) { - Eigen::VectorXd perturbed = state; - perturbed(i) += eps; - - Eigen::VectorXd f_pert = getContinuousDynamics(perturbed, control, time); - A.col(i) = (f_pert - f0) / eps; - } - return A; -} - -// ---------------------------------------------------------------------------- -// getControlJacobian -// ---------------------------------------------------------------------------- -Eigen::MatrixXd NeuralDynamicalSystem::getControlJacobian(const Eigen::VectorXd& state, - const Eigen::VectorXd& control, - double time) const -{ - // Similar naive finite-difference: - const double eps = 1e-6; - Eigen::MatrixXd B(state_dim_, control_dim_); - - // Baseline - Eigen::VectorXd f0 = getContinuousDynamics(state, control, time); - - for (int j = 0; j < control_dim_; ++j) { - Eigen::VectorXd ctrl_pert = control; - ctrl_pert(j) += eps; - - Eigen::VectorXd f_pert = getContinuousDynamics(state, ctrl_pert, time); - B.col(j) = (f_pert - f0) / eps; - } - return B; -} - -// ---------------------------------------------------------------------------- -// Hessians (placeholders) -// ---------------------------------------------------------------------------- -std::vector NeuralDynamicalSystem::getStateHessian( - const Eigen::VectorXd& /*state*/, - const Eigen::VectorXd& /*control*/, - double /*time*/) const -{ - // Initialize vector of matrices (one matrix per state dimension) - std::vector hessian(state_dim_); - for (int i = 0; i < state_dim_; ++i) { - hessian[i] = Eigen::MatrixXd::Zero(state_dim_, state_dim_); - } - - // For neural network models, computing Hessians usually requires - // second-order automatic differentiation or finite differencing. - // This is a placeholder implementation that returns zeros. - - return hessian; -} - -std::vector NeuralDynamicalSystem::getControlHessian( - const Eigen::VectorXd& /*state*/, - const Eigen::VectorXd& /*control*/, - double /*time*/) const -{ - // Initialize vector of matrices (one matrix per state dimension) - std::vector hessian(state_dim_); - for (int i = 0; i < state_dim_; ++i) { - hessian[i] = Eigen::MatrixXd::Zero(control_dim_, control_dim_); - } - - // Placeholder implementation - - return hessian; -} - -std::vector NeuralDynamicalSystem::getCrossHessian( - const Eigen::VectorXd& /*state*/, - const Eigen::VectorXd& /*control*/, - double /*time*/) const -{ - // Initialize vector of matrices (one matrix per state dimension) - std::vector hessian(state_dim_); - for (int i = 0; i < state_dim_; ++i) { - hessian[i] = Eigen::MatrixXd::Zero(control_dim_, state_dim_); - } - - // Placeholder implementation - - return hessian; -} - -// ---------------------------------------------------------------------------- -// getContinuousDynamicsAutodiff -// ---------------------------------------------------------------------------- -VectorXdual2nd NeuralDynamicalSystem::getContinuousDynamicsAutodiff( - const VectorXdual2nd& /*state*/, - const VectorXdual2nd& /*control*/, - double /*time*/) const { - throw std::logic_error( - "getContinuousDynamicsAutodiff is not implemented for NeuralDynamicalSystem " - "in a way that supports base class autodiff with external Torch models. " - "Please use the overridden Jacobian/Hessian methods specific to NeuralDynamicalSystem, " - "which should ideally use PyTorch's autograd."); -} - -// ---------------------------------------------------------------------------- -// eigenToTorch / torchToEigen -// ---------------------------------------------------------------------------- -torch::Tensor NeuralDynamicalSystem::eigenToTorch(const Eigen::VectorXd& eigen_vec, - bool requires_grad) const -{ - // Shape [1, size] - // If you prefer shape [size] without batch-dim, adjust accordingly. - auto tensor = torch::from_blob( - const_cast(eigen_vec.data()), - {1, static_cast(eigen_vec.size())}, - torch::TensorOptions().dtype(torch::kDouble) - ).clone(); // .clone() to own memory - - // Move to device - tensor = tensor.to(device_); - - // optionally set requires_grad - if (requires_grad) { - tensor.set_requires_grad(true); - } - return tensor; -} - -Eigen::VectorXd NeuralDynamicalSystem::torchToEigen(const torch::Tensor& tensor) const -{ - // Expect shape [1, state_dim_] or [1, control_dim_]. We'll read out the second dimension. - auto cpu_tensor = tensor.to(torch::kCPU).contiguous(); - if (cpu_tensor.dim() != 2) { - throw std::runtime_error("torchToEigen: expected a 2D tensor with batch-dim"); - } - auto rows = cpu_tensor.size(0); - auto cols = cpu_tensor.size(1); - - // For a single sample, rows == 1. We'll produce an Eigen vector of length = cols. - Eigen::VectorXd eigen_vec(cols); - std::memcpy(eigen_vec.data(), cpu_tensor.data_ptr(), sizeof(double)*cols); - return eigen_vec; -} - -} // namespace cddp diff --git a/src/sqp_core/sqp_core.cpp b/src/sqp_core/sqp_core.cpp deleted file mode 100644 index 154f07a2..00000000 --- a/src/sqp_core/sqp_core.cpp +++ /dev/null @@ -1,317 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#include "sqp_core/sqp_core.hpp" -#include -#include -#include -#include - -namespace cddp { - -SCPSolver::SCPSolver(const Eigen::VectorXd& initial_state, - const Eigen::VectorXd& reference_state, - int horizon, - double timestep) - : initial_state_(initial_state), - reference_state_(reference_state), - horizon_(horizon), - timestep_(timestep) -{ - // Initialize trajectory estimates (if not provided later). - initializeSCP(); -} - -void SCPSolver::setInitialTrajectory(const std::vector& X, - const std::vector& U) { - X_ = X; - U_ = U; -} - -// -// If trajectories have not been provided, initialize by linear interpolation. -// -void SCPSolver::initializeSCP() { - int state_dim = initial_state_.size(); - // Initialize X_ with horizon_+1 states via linear interpolation. - if (X_.empty()) { - X_.resize(horizon_ + 1); - for (int t = 0; t <= horizon_; ++t) { - double alpha = static_cast(t) / horizon_; - X_[t] = initial_state_ + alpha * (reference_state_ - initial_state_); - } - X_[0] = initial_state_; - } - // Initialize U_ (if not provided) with zeros. - if (U_.empty()) { - int control_dim = 2; - if (system_) { - control_dim = system_->getControlDim(); - } - U_.resize(horizon_); - for (int t = 0; t < horizon_; ++t) { - U_[t] = Eigen::VectorXd::Zero(control_dim); - } - } -} - - -void SCPSolver::computeLinearizedDynamics(const std::vector& X, - const std::vector& U, - std::vector& A, - std::vector& B) const { - int state_dim = initial_state_.size(); - int control_dim = U[0].size(); - double eps = 1e-5; - A.resize(horizon_); - B.resize(horizon_); - for (int t = 0; t < horizon_; ++t) { - Eigen::VectorXd x = X[t]; - Eigen::VectorXd u = U[t]; - Eigen::VectorXd f0 = system_->getDiscreteDynamics(x, u, t * timestep_); - A[t] = Eigen::MatrixXd::Zero(state_dim, state_dim); - B[t] = Eigen::MatrixXd::Zero(state_dim, control_dim); - - auto [fx, fu] = system_->getJacobians(x, u, t * timestep_); - A[t] = Eigen::MatrixXd::Identity(state_dim, state_dim) + timestep_ * fx; - B[t] = timestep_ * fu; - } -} - -SCPResult SCPSolver::solve() { - using namespace casadi; - SCPResult result; - - // --- Parameters (adjust as needed) --- - double Delta = options_.trust_region_radius; // initial trust-region radius (Δ₀) - double convergence_threshold = 1e-3; // convergence threshold - int max_it = options_.max_iterations; // maximum iterations - - // --- Dimensions and Initialization --- - int state_dim = initial_state_.size(); - int control_dim = U_[0].size(); - int N = horizon_; // There are N control intervals, so the state trajectory has N+1 points - - // Use the current trajectory estimates stored in X_ and U_ - std::vector X = X_; // our state trajectory - std::vector U = U_; - - // If no initial trajectory is provided, you might consider initializing X by linear interpolation: - // for (int t = 0; t <= N; ++t) { - // double alpha = static_cast(t) / N; - // X[t] = initial_state_ + alpha * (reference_state_ - initial_state_); - // } - - // Store previous iterate for linearization and convergence checking. - std::vector X_prev = X; - std::vector U_prev = U; - - // Save history of iterates. - std::vector< std::vector > X_all; - std::vector< std::vector > U_all; - X_all.push_back(X); - U_all.push_back(U); - - bool success = false; - int it = 1; - - // Start timing. - auto start = std::chrono::high_resolution_clock::now(); - - // --- Main SCP Loop --- - while (it < max_it) { - // Compute convergence metric: sum of norm differences between current and previous iterates. - double conv_metric = 0.0; - for (size_t t = 0; t < X.size(); ++t) - conv_metric += (X[t] - X_prev[t]).norm(); - for (size_t t = 0; t < U.size(); ++t) - conv_metric += (U[t] - U_prev[t]).norm(); - - // After at least 3 iterations, check convergence. - if (it > 2 && conv_metric < convergence_threshold) { - success = true; - break; - } - - // Save the current iterate as previous. - X_prev = X; - U_prev = U; - - std::vector x_vars, u_vars; - for (int t = 0; t <= N; ++t) - x_vars.push_back(MX::sym("x_" + std::to_string(t), state_dim)); - for (int t = 0; t < N; ++t) - u_vars.push_back(MX::sym("u_" + std::to_string(t), control_dim)); - - // --- Define the Objective --- - MX cost = 0; - for (int t = 0; t < N; ++t) { - cost += dot(u_vars[t], u_vars[t]); - } - DM ref_dm = DM(std::vector(reference_state_.data(), - reference_state_.data() + state_dim)); - MX terminal_error = x_vars[N] - ref_dm; - cost += 1e6 * dot(terminal_error, terminal_error); - - // --- Define Constraints --- - std::vector g; - // (1) Initial state constraint: x₀ == initial_state. - DM init_dm = DM(std::vector(initial_state_.data(), - initial_state_.data() + state_dim)); - g.push_back(x_vars[0] - init_dm); - - // (2) Dynamics constraints: linearize the dynamics around (X_prev, U_prev). - std::vector A, B; - computeLinearizedDynamics(X_prev, U_prev, A, B); - for (int t = 0; t < N; ++t) { - Eigen::VectorXd f_nom = system_->getDiscreteDynamics(X_prev[t], U_prev[t], t * timestep_); - DM f_nom_dm = DM(std::vector(f_nom.data(), - f_nom.data() + state_dim)); - DM xbar = DM(std::vector(X_prev[t].data(), - X_prev[t].data() + state_dim)); - DM ubar = DM(std::vector(U_prev[t].data(), - U_prev[t].data() + control_dim)); - // Convert A[t] and B[t] to CasADi DM. - DM A_dm = DM::zeros(state_dim, state_dim); - DM B_dm = DM::zeros(state_dim, control_dim); - for (int i = 0; i < state_dim; ++i) - for (int j = 0; j < state_dim; ++j) - A_dm(i, j) = A[t](i, j); - for (int i = 0; i < state_dim; ++i) - for (int j = 0; j < control_dim; ++j) - B_dm(i, j) = B[t](i, j); - // Linearized dynamics constraint: - // x_{t+1} ≈ f_nom + A*(x_t - xbar) + B*(u_t - ubar) - MX dyn_lin = f_nom_dm + mtimes(A_dm, (x_vars[t] - xbar)) - + mtimes(B_dm, (u_vars[t] - ubar)); - g.push_back(x_vars[t+1] - dyn_lin); - } - - // (3) Trust-region constraints: force the new decision variables to stay within [-Delta, Delta] of the previous iterate. - for (int t = 0; t <= N; ++t) { - g.push_back(x_vars[t] - DM(std::vector(X_prev[t].data(), - X_prev[t].data() + state_dim))); - } - for (int t = 0; t < N; ++t) { - g.push_back(u_vars[t] - DM(std::vector(U_prev[t].data(), - U_prev[t].data() + control_dim))); - } - MX g_all = vertcat(g); - - // --- Concatenate Decision Variables --- - std::vector w_list; - for (int t = 0; t <= N; ++t) - w_list.push_back(x_vars[t]); - for (int t = 0; t < N; ++t) - w_list.push_back(u_vars[t]); - MX w = vertcat(w_list); - int n_w = w.size1(); - - // --- Set Variable and Constraint Bounds --- - std::vector lbw(n_w, -1e20), ubw(n_w, 1e20); - // For constraints: - int num_eq = state_dim + N * state_dim; - int num_trust = (N + 1) * state_dim + N * control_dim; - std::vector lbg, ubg; - for (int i = 0; i < num_eq; ++i) { - lbg.push_back(0.0); - ubg.push_back(0.0); - } - for (int i = 0; i < num_trust; ++i) { - lbg.push_back(-Delta); - ubg.push_back(Delta); - } - - // --- Formulate and Solve the NLP or QP --- - MXDict nlp = {{"x", w}, {"f", cost}, {"g", g_all}}; - // Dict solver_opts; - // solver_opts["ipopt.max_iter"] = options_.ipopt_max_iter; - // solver_opts["ipopt.tol"] = options_.ipopt_tol; - Function solver_fun = qpsol("solver", "qrqp", nlp); - - // Use the previous iterate as the initial guess. - std::vector w0(n_w, 0.0); - int offset = 0; - for (int t = 0; t <= N; ++t) { - for (int i = 0; i < state_dim; ++i) - w0[offset++] = X_prev[t](i); - } - for (int t = 0; t < N; ++t) { - for (int i = 0; i < control_dim; ++i) - w0[offset++] = U_prev[t](i); - } - - DMDict solver_args = {{"x0", w0}, {"lbx", lbw}, {"ubx", ubw}, - {"lbg", lbg}, {"ubg", ubg}}; - DMDict solver_res = solver_fun(solver_args); - std::vector sol = std::vector(solver_res.at("x")); - - // --- Extract the New Trajectory --- - std::vector X_new(N + 1, Eigen::VectorXd(state_dim)); - std::vector U_new(N, Eigen::VectorXd(control_dim)); - offset = 0; - for (int t = 0; t <= N; ++t) { - Eigen::VectorXd x_new(state_dim); - for (int i = 0; i < state_dim; ++i) - x_new(i) = sol[offset++]; - X_new[t] = x_new; - } - for (int t = 0; t < N; ++t) { - Eigen::VectorXd u_new(control_dim); - for (int i = 0; i < control_dim; ++i) - u_new(i) = sol[offset++]; - U_new[t] = u_new; - } - - // --- Update the Current Trajectory --- - X = X_new; - U = U_new; - X_all.push_back(X); - U_all.push_back(U); - - // --- Adjust Trust-Region Radius --- - double step_norm = 0.0; - for (int t = 0; t <= N; ++t) - step_norm += (X[t] - X_prev[t]).norm(); - for (int t = 0; t < N; ++t) - step_norm += (U[t] - U_prev[t]).norm(); - if (step_norm > 0.9 * Delta) - Delta = std::min(Delta * 2.0, 1e6); - else - Delta *= 0.5; - - if (options_.verbose) - std::cout << "[SCP] Iteration " << it << ", convergence metric: " << conv_metric << std::endl; - - it++; - } - - auto end = std::chrono::high_resolution_clock::now(); - result.solve_time = std::chrono::duration(end - start).count(); - result.iterations = it; - result.success = (it < max_it); - result.X = X; - result.U = U; - result.objective_value = 0.0; // (Not computed in this simplified version) - result.constraint_violation = 0.0; // (Not computed in this simplified version) - - // Optionally, check trust-region satisfaction. - bool B_trust_satisfied = satisfies_trust_region_constraints(X, X_all[X_all.size()-2], Delta); - if (options_.verbose) - std::cout << "[SCP] Trust region satisfied: " << (B_trust_satisfied ? "true" : "false") << std::endl; - - return result; -} -} // namespace cddp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7d258ea1..7de0b40e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -131,10 +131,6 @@ add_executable(test_clddp_solver cddp_core/test_clddp_solver.cpp) target_link_libraries(test_clddp_solver gtest gmock gtest_main cddp) gtest_discover_tests(test_clddp_solver) -add_executable(test_asddp_solver cddp_core/test_asddp_solver.cpp) -target_link_libraries(test_asddp_solver gtest gmock gtest_main cddp) -gtest_discover_tests(test_asddp_solver) - add_executable(test_logddp_solver cddp_core/test_logddp_solver.cpp) target_link_libraries(test_logddp_solver gtest gmock gtest_main cddp) gtest_discover_tests(test_logddp_solver) @@ -151,8 +147,6 @@ add_executable(test_alddp_solver cddp_core/test_alddp_solver.cpp) target_link_libraries(test_alddp_solver gtest gmock gtest_main cddp) gtest_discover_tests(test_alddp_solver) -# add_executable(test_asddp_core cddp_core/test_asddp_core.cpp) - # add_executable(test_logcddp_core cddp_core/test_logcddp_core.cpp) # target_link_libraries(test_logcddp_core gtest gmock gtest_main cddp) # gtest_discover_tests(test_logcddp_core) @@ -189,23 +183,6 @@ gtest_discover_tests(test_alddp_solver) # target_link_libraries(test_matplot gtest gmock gtest_main cddp) # gtest_discover_tests(test_matplot) -# Test for torch -if (CDDP_CPP_TORCH) - add_executable(test_torch test_torch.cpp) - target_link_libraries(test_torch gtest gmock gtest_main cddp) - gtest_discover_tests(test_torch) - - add_executable(test_neural_pendulum dynamics_model/test_neural_pendulum.cpp) - target_link_libraries(test_neural_pendulum gtest gmock gtest_main cddp) - gtest_discover_tests(test_neural_pendulum) -endif() - -if (CDDP_CPP_SQP AND CDDP_CPP_CASADI) - add_executable(test_sqp_core sqp_core/test_sqp_core.cpp) - target_link_libraries(test_sqp_core gtest gmock gtest_main cddp) - gtest_discover_tests(test_sqp_core) -endif() - if (CDDP_CPP_CASADI) add_executable(test_casadi test_casadi_solver.cpp) target_link_libraries(test_casadi gtest gmock gtest_main cddp) @@ -221,15 +198,3 @@ gtest_discover_tests(test_eigen) add_executable(test_autodiff test_autodiff.cpp) target_link_libraries(test_autodiff gtest gmock gtest_main cddp) gtest_discover_tests(test_autodiff) - -# Test for gurobi -if (CDDP_CPP_GUROBI) - add_executable(test_gurobi test_gurobi.cpp) - target_link_libraries(test_gurobi gtest gmock gtest_main cddp) - gtest_discover_tests(test_gurobi) - - # Test for qp solver comparison - add_executable(test_qp_solvers test_qp_solvers.cpp) - target_link_libraries(test_qp_solvers gtest gmock gtest_main cddp) - gtest_discover_tests(test_qp_solvers) -endif() diff --git a/tests/cddp_core/test_asddp_core.cpp b/tests/cddp_core/test_asddp_core.cpp deleted file mode 100644 index 85381127..00000000 --- a/tests/cddp_core/test_asddp_core.cpp +++ /dev/null @@ -1,114 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#include -#include -#include - -#include "gmock/gmock.h" -#include "gtest/gtest.h" - -#include "cddp-cpp/cddp.hpp" - -TEST(CDDPTest, SolveASCDDP) { - // Problem parameters - int state_dim = 3; - int control_dim = 2; - int horizon = 100; - double timestep = 0.03; - std::string integration_type = "euler"; - - // Create a dubins car instance - std::unique_ptr system = std::make_unique(timestep, integration_type); // Create unique_ptr - - // Create objective function - Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); - Eigen::MatrixXd R = 0.05 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 50.0, 0.0, 0.0, - 0.0, 50.0, 0.0, - 0.0, 0.0, 10.0; - Eigen::VectorXd goal_state(state_dim); - goal_state << 2.0, 2.0, M_PI/2.0; - - // Create an empty vector of Eigen::VectorXd - std::vector empty_reference_states; - auto objective = std::make_unique(Q, R, Qf, goal_state, empty_reference_states, timestep); - - // Initial and target states - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI/4.0; - - // Create CDDP Options - cddp::CDDPOptions options; - options.max_iterations = 20; - options.verbose = true; - options.cost_tolerance = 1e-5; - options.grad_tolerance = 1e-4; - options.regularization_type = "none"; - options.max_line_search_iterations = 21; - options.debug = true; - // options.max_cpu_time = 1e-1; - - // // Create CDDP solver - cddp::CDDP cddp_solver( - initial_state, - goal_state, - horizon, - timestep, - std::make_unique(timestep, integration_type), - std::make_unique(Q, R, Qf, goal_state, empty_reference_states, timestep), - options); - cddp_solver.setDynamicalSystem(std::move(system)); - cddp_solver.setObjective(std::move(objective)); - - // Define constraints - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -5.0, -M_PI; - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 5.0, M_PI; - // Add the constraint to the solver - cddp_solver.addConstraint(std::string("ControlBoxConstraint"), std::make_unique(control_lower_bound, control_upper_bound)); - auto constraint = cddp_solver.getConstraint("ControlBoxConstraint"); - - // Define ball constraint - double radius = 0.2; - Eigen::Vector2d center(1.0, 1.0); - cddp_solver.addConstraint(std::string("BallConstraint"), std::make_unique(radius, center)); - - // Set options - cddp_solver.setOptions(options); - - // Set initial trajectory - std::vector X(horizon + 1, Eigen::VectorXd::Zero(state_dim)); - std::vector U(horizon, Eigen::VectorXd::Zero(control_dim)); - for (int i = 0; i < horizon + 1; ++i) { - X[i] = initial_state; - } - cddp_solver.setInitialTrajectory(X, U); - - // // Solve the problem - cddp::CDDPSolution solution = cddp_solver.solve("ASCDDP"); - - ASSERT_TRUE(solution.converged); - - // Extract solution - auto X_sol = solution.state_sequence; // size: horizon + 1 - auto U_sol = solution.control_sequence; // size: horizon - auto t_sol = solution.time_sequence; // size: horizon + 1 - - - -} \ No newline at end of file diff --git a/tests/cddp_core/test_asddp_solver.cpp b/tests/cddp_core/test_asddp_solver.cpp deleted file mode 100644 index 210aa059..00000000 --- a/tests/cddp_core/test_asddp_solver.cpp +++ /dev/null @@ -1,305 +0,0 @@ -/* - Copyright 2025 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#include -#include -#include -#include -#include -#include -#include - -#include "gmock/gmock.h" -#include "gtest/gtest.h" - -#include "cddp.hpp" - -TEST(ASDDPTest, SolvePendulum) -{ - int state_dim = 2; - int control_dim = 1; - int horizon = 500; - double timestep = 0.05; - // Create a pendulum instance - double mass = 1.0; - double length = 1.0; - double damping = 0.00; - std::string integration_type = "euler"; - - std::unique_ptr system = std::make_unique(timestep, length, mass, damping, integration_type); - - // Cost matrices - Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); - Eigen::MatrixXd R = 0.1 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 100.0, 0.0, - 0.0, 100.0; - - Eigen::VectorXd goal_state(state_dim); - goal_state << 0.0, 0.0; // Upright position with zero velocity - - std::vector empty_reference_states; - // empty_reference_states.back() << 0.0, 0.0; - auto objective = std::make_unique(Q, R, Qf, goal_state, empty_reference_states, timestep); - - // Initial state (pendulum pointing down) - Eigen::VectorXd initial_state(state_dim); - initial_state << M_PI, 0.0; // Zero angle and angular velocity - - // Construct zero control sequence - std::vector zero_control_sequence(horizon, Eigen::VectorXd::Zero(control_dim)); - - // Construct initial trajectory - std::vector X_init(horizon + 1, Eigen::VectorXd::Zero(state_dim)); - for (int t = 0; t < horizon + 1; ++t) - { - X_init[t] = initial_state; - } - - // Calculate initial cost - double J = 0.0; - for (int t = 0; t < horizon; ++t) - { - J += objective->running_cost(X_init[t], zero_control_sequence[t], t); - } - J += objective->terminal_cost(X_init[horizon]); - - // Create CDDP solver - cddp::CDDP cddp_solver(initial_state, goal_state, horizon, timestep); - cddp_solver.setDynamicalSystem(std::move(system)); - cddp_solver.setObjective(std::move(objective)); - - // Control constraints - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -10.0; // Maximum negative torque - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 10.0; // Maximum positive torque - - cddp_solver.addPathConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, control_upper_bound)); - - // Create CDDP Options - cddp::CDDPOptions options; - options.max_iterations = 100; - options.tolerance = 1e-3; // KKT/optimality tolerance - options.acceptable_tolerance = 1e-3; // Cost change tolerance - options.enable_parallel = false; - options.num_threads = 1; - options.verbose = true; - options.debug = false; - options.regularization.initial_value = 1e-2; - options.return_iteration_info = true; // Get detailed iteration history - - // Set options - cddp_solver.setOptions(options); - - // Set initial trajectory - std::vector X(horizon + 1, Eigen::VectorXd::Zero(state_dim)); - std::vector U(horizon, Eigen::VectorXd::Zero(control_dim)); - X[0] << initial_state; - for (int i = 0; i < horizon; ++i) - { - U[i] = Eigen::VectorXd::Zero(control_dim); - X[i] = initial_state; - } - X[horizon] << initial_state; - - cddp_solver.setInitialTrajectory(X, U); - - // Solve the problem - std::cout << "\n=== First solve (cold start) ===" << std::endl; - cddp::CDDPSolution solution = cddp_solver.solve("ASDDP"); - - // Check convergence - auto status_message = std::any_cast(solution.at("status_message")); - auto iterations_completed = std::any_cast(solution.at("iterations_completed")); - auto solve_time_ms = std::any_cast(solution.at("solve_time_ms")); - auto final_objective = std::any_cast(solution.at("final_objective")); - - std::cout << "\n=== Convergence Analysis ===" << std::endl; - std::cout << "Status: " << status_message << std::endl; - std::cout << "Converged: " << (status_message == "OptimalSolutionFound" || status_message == "AcceptableSolutionFound" ? "YES" : "NO") << std::endl; - std::cout << "Iterations: " << iterations_completed << std::endl; - std::cout << "Solve time: " << solve_time_ms << " ms" << std::endl; - std::cout << "Final cost: " << final_objective << std::endl; - - // Extract trajectories - auto X_sol = std::any_cast>(solution.at("state_trajectory")); - auto U_sol = std::any_cast>(solution.at("control_trajectory")); - auto t_sol = std::any_cast>(solution.at("time_points")); - - // Print final state - Eigen::VectorXd final_state = X_sol.back(); - std::cout << "Final state: [" << final_state.transpose() << "]" << std::endl; - std::cout << "Goal state: [" << goal_state.transpose() << "]" << std::endl; - std::cout << "Final error: " << (final_state - goal_state).norm() << std::endl; - - // Test assertions - EXPECT_TRUE(status_message == "OptimalSolutionFound" || status_message == "AcceptableSolutionFound") << "Algorithm should converge"; - EXPECT_GT(iterations_completed, 0) << "Should take at least one iteration"; - EXPECT_LT(final_objective, J) << "Final cost should be better than initial cost"; - - // ========================================================================= - // Test warm start capability - // ========================================================================= - std::cout << "\n=== Testing warm start ===" << std::endl; - - // Enable warm start and use previous solution as initial guess - cddp::CDDPOptions warm_options = options; - warm_options.warm_start = true; - warm_options.max_iterations = 10; // Fewer iterations for warm start - warm_options.verbose = false; // Less verbose for warm start test - warm_options.tolerance = 1e-3; // KKT/optimality tolerance - warm_options.acceptable_tolerance = 1e-3; // Cost change tolerance - warm_options.enable_parallel = false; - warm_options.num_threads = 1; - warm_options.debug = false; - - // Create a new solver for warm start test - auto hcw_system_warmstart = std::make_unique(timestep, length, mass, damping, integration_type); - - // Create new objective - std::vector empty_reference_states_warmstart; - auto objective_warmstart = std::make_unique(Q, R, Qf, goal_state, empty_reference_states_warmstart, timestep); - - cddp::CDDP warm_solver(initial_state, goal_state, horizon, timestep); - warm_solver.setDynamicalSystem(std::move(hcw_system_warmstart)); - warm_solver.setObjective(std::move(objective_warmstart)); - warm_solver.addPathConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, control_upper_bound)); - warm_solver.setOptions(warm_options); - - // Use previous solution as warm start - warm_solver.setInitialTrajectory(X_sol, U_sol); - - // Solve with warm start - auto start_time = std::chrono::high_resolution_clock::now(); - cddp::CDDPSolution warm_solution = warm_solver.solve("ASDDP"); - auto end_time = std::chrono::high_resolution_clock::now(); - auto warm_duration = std::chrono::duration_cast(end_time - start_time); - - // Extract warm start results - auto warm_status = std::any_cast(warm_solution.at("status_message")); - auto warm_iterations = std::any_cast(warm_solution.at("iterations_completed")); - auto warm_solve_time = std::any_cast(warm_solution.at("solve_time_ms")); - auto warm_final_cost = std::any_cast(warm_solution.at("final_objective")); - - std::cout << "Warm start status: " << warm_status << std::endl; - std::cout << "Warm start iterations: " << warm_iterations << std::endl; - std::cout << "Warm start solve time: " << warm_solve_time << " ms" << std::endl; - std::cout << "Warm start final cost: " << warm_final_cost << std::endl; - - // Warm start should converge faster or in fewer iterations - std::cout << "\n=== Performance Comparison ===" << std::endl; - std::cout << "Cold start: " << iterations_completed << " iterations, " << solve_time_ms << " ms" << std::endl; - std::cout << "Warm start: " << warm_iterations << " iterations, " << warm_solve_time << " ms" << std::endl; - - if (warm_iterations <= iterations_completed) - { - std::cout << "✓ Warm start used fewer or equal iterations" << std::endl; - } - else - { - std::cout << "✗ Warm start used more iterations (this can happen)" << std::endl; - } - - if (warm_solve_time <= solve_time_ms * 1.2) - { // Allow 20% tolerance - std::cout << "✓ Warm start was faster or comparable" << std::endl; - } - else - { - std::cout << "✗ Warm start was slower" << std::endl; - } - - // Both should converge - EXPECT_TRUE(warm_status == "OptimalSolutionFound" || warm_status == "AcceptableSolutionFound") << "Warm start should also converge"; - EXPECT_LE(warm_iterations, iterations_completed + 5) << "Warm start should not take significantly more iterations"; -} - -TEST(ASDDPTest, SolveUnicycle) { - // Problem parameters - int state_dim = 3; - int control_dim = 2; - int horizon = 100; - double timestep = 0.03; - std::string integration_type = "euler"; - - // Create a dubins car instance - std::unique_ptr system = std::make_unique(timestep, integration_type); // Create unique_ptr - - // Create objective function - Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); - Eigen::MatrixXd R = 0.5 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 50.0, 0.0, 0.0, - 0.0, 50.0, 0.0, - 0.0, 0.0, 10.0; - Qf = 0.5 * Qf; - Eigen::VectorXd goal_state(state_dim); - goal_state << 2.0, 2.0, M_PI/2.0; - - // Create an empty vector of Eigen::VectorXd - std::vector empty_reference_states; - auto objective = std::make_unique(Q, R, Qf, goal_state, empty_reference_states, timestep); - - // Initial and target states - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI/4.0; - - // Create CDDP Options - cddp::CDDPOptions options; - options.max_iterations = 20; - options.tolerance = 1e-2; - options.enable_parallel = true; - options.num_threads = 10; - options.verbose = true; - options.regularization.initial_value = 1e-2; - options.debug = false; - - // Create CDDP solver - cddp::CDDP cddp_solver(initial_state, goal_state, horizon, timestep); - cddp_solver.setDynamicalSystem(std::move(system)); - cddp_solver.setObjective(std::move(objective)); - - // Define constraints - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -1.0, -M_PI; - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 1.0, M_PI; - - // Add the constraint to the solver - cddp_solver.addPathConstraint("ControlBoxConstraint", std::make_unique(control_lower_bound, control_upper_bound)); - - // Set options - cddp_solver.setOptions(options); - - // Set initial trajectory - std::vector X(horizon + 1, Eigen::VectorXd::Zero(state_dim)); - std::vector U(horizon, Eigen::VectorXd::Zero(control_dim)); - cddp_solver.setInitialTrajectory(X, U); - - // Solve the problem - cddp::CDDPSolution solution = cddp_solver.solve("ASDDP"); - // cddp::CDDPSolution solution = cddp_solver.solveASDDP(); - - auto status = std::any_cast(solution.at("status_message")); - ASSERT_TRUE(status == "OptimalSolutionFound" || status == "AcceptableSolutionFound"); - - // Extract solution - auto X_sol = std::any_cast>(solution.at("state_trajectory")); // size: horizon + 1 - auto U_sol = std::any_cast>(solution.at("control_trajectory")); // size: horizon - auto t_sol = std::any_cast>(solution.at("time_points")); // size: horizon + 1 -} diff --git a/tests/cddp_core/test_boxqp.cpp b/tests/cddp_core/test_boxqp.cpp index 64343447..8b437cdc 100644 --- a/tests/cddp_core/test_boxqp.cpp +++ b/tests/cddp_core/test_boxqp.cpp @@ -236,53 +236,6 @@ int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } - -/* $ ./tests/test_qp_solvers -[==========] Running 1 test from 1 test suite. -[----------] Global test environment set-up. -[----------] 1 test from QPSolver -[ RUN ] QPSolver.ComparisonTest - -====== Comparing QP Solvers ====== - - ->>> Results from OSQP: -optimal sol: -8.58736e-05 1.66673 0 0.666634 1 -optimal obj: -5.33339 -elapsed time: 5.712e-06s -status: 0 ->>> End of OSQP test - ->>> Results from SDQP: -optimal sol: 0 1.66667 0 0.666667 1 -optimal obj: -5.33333 -elapsed time: 1.0261e-05s -status: 0 ->>> End of SDQP test - ->>> Results from BoxQP: -optimal sol: 0 1.66667 0 0.666667 1 -optimal obj: -5.33333 -elapsed time: 6.885e-06s -status: 5 ->>> End of BoxQP test -Set parameter Username -Academic license - for non-commercial use only - expires 2025-09-25 - ->>> Results from Gurobi: -optimal sol: 0 0 0 0 0 -optimal obj: -5.33333 -elapsed time: 0.000398952s -status: 2 ->>> End of Gurobi test - ->>> Results from QPSolver: -optimal sol: 0 1.66667 -0 0.666667 1 -optimal obj: -5.33333 -elapsed time: 6.922e-06s -status: 0 ->>> End of QPSolver test -[ OK ] QPSolver.ComparisonTest (1 ms) [----------] 1 test from QPSolver (1 ms total) [----------] Global test environment tear-down diff --git a/tests/dynamics_model/test_neural_pendulum.cpp b/tests/dynamics_model/test_neural_pendulum.cpp deleted file mode 100644 index d16c7535..00000000 --- a/tests/dynamics_model/test_neural_pendulum.cpp +++ /dev/null @@ -1,296 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include -#include "gmock/gmock.h" -#include "gtest/gtest.h" - -#include "cddp.hpp" -#include "cddp_core/neural_dynamical_system.hpp" - -using namespace cddp; - -class PendulumModel : public DynamicsModelInterface { -public: - PendulumModel(double length = 1.0, double mass = 1.0, double damping = 0.0); - - torch::Tensor forward(std::vector inputs) override; - -private: - void initialize_weights(); - - double length_, mass_, damping_; - torch::nn::Linear linear1{nullptr}, linear2{nullptr}, linear3{nullptr}; - torch::Device device_; -}; - -PendulumModel::PendulumModel(double length, double mass, double damping) - : length_(length), mass_(mass), damping_(damping), - device_(torch::cuda::is_available() ? torch::kCUDA : torch::kCPU) -{ - // Create linear layers - linear1 = register_module("linear1", torch::nn::Linear(3, 32)); - linear2 = register_module("linear2", torch::nn::Linear(32, 32)); - linear3 = register_module("linear3", torch::nn::Linear(32, 2)); - - // Move to device and set dtype - this->to(device_); - if (device_.is_cpu()) { - this->to(torch::kFloat64); - } else { - this->to(torch::kFloat32); - } - initialize_weights(); -} - -void PendulumModel::initialize_weights() -{ - torch::NoGradGuard no_grad; - - double angle_scale = 0.1; - double velocity_scale = 0.1; - - // Example: manually set a few weights in linear3 - auto w = linear3->weight.data(); - w[0][0] = angle_scale; - w[1][1] = velocity_scale; - linear3->weight.data() = w; - - auto b = linear3->bias.data(); - b[0] = 0.0; - b[1] = -9.81 / length_ * angle_scale; // a quick guess - linear3->bias.data() = b; -} - -torch::Tensor PendulumModel::forward(std::vector inputs) -{ - // Expect 2 inputs: [state, control] - auto state = inputs[0].to(device_); - auto control = inputs[1].to(device_); - - if (device_.is_cuda()) { - // If on GPU, we use float32 - state = state.to(torch::kFloat32); - control = control.to(torch::kFloat32); - } - - // Concatenate along dim=1 if shapes are [batch_size, 2] and [batch_size, 1] - auto x = torch::cat({state, control}, /*dim=*/1); - x = torch::tanh(linear1(x)); - x = torch::tanh(linear2(x)); - x = linear3(x); - - if (device_.is_cuda()) { - x = x.to(torch::kFloat64); // move back to double if desired - } - return x; -} - -void printTensorInfo(const torch::Tensor& tensor, const std::string& name) -{ - std::cout << name << ":\n" - << " - shape: [" << tensor.sizes() << "]\n" - << " - dtype: " << tensor.dtype() << "\n" - << " - device: " << tensor.device() << "\n" - << " - values: " << tensor << "\n" << std::endl; -} - -void printVectorInfo(const Eigen::VectorXd& vec, const std::string& name) -{ - std::cout << name << ":\n" - << " - size: " << vec.size() << "\n" - << " - values: " << vec.transpose() << "\n" << std::endl; -} - -//-----------------------------------------// -// TEST SUITE: TorchPendulumTest -//-----------------------------------------// -TEST(TorchPendulumTest, DiscreteDynamics) -{ - // Parameters - double timestep = 0.01; - double length = 1.0; - double mass = 1.0; - double damping = 0.0; - std::string integration_type = "rk4"; - - cddp::Pendulum analytical_pendulum(timestep, length, mass, damping, integration_type); - - auto model = std::make_shared(length, mass, damping); - torch::Device device(torch::cuda::is_available() ? torch::kCUDA : torch::kCPU); - NeuralDynamicalSystem torch_pendulum(/*state_dim=*/2, /*control_dim=*/1, - timestep, integration_type, model, device); - - // Create a test state/control - Eigen::VectorXd test_state(2); - test_state << M_PI / 4, 0.0; // 45 deg, no velocity - Eigen::VectorXd test_control(1); - test_control << 0.0; - - // Analytical next state - auto analytical_next = analytical_pendulum.getDiscreteDynamics(test_state, test_control, 0.0); - printVectorInfo(analytical_next, "Analytical Next State"); - - // Torch next state - auto torch_next = torch_pendulum.getDiscreteDynamics(test_state, test_control, 0.0); - printVectorInfo(torch_next, "Torch Next State"); - - // Compare errors - double error = (analytical_next - torch_next).norm(); - std::cout << "L2 error: " << error << std::endl; - - // Basic tests - ASSERT_EQ(torch_pendulum.getStateDim(), 2); - ASSERT_EQ(torch_pendulum.getControlDim(), 1); - ASSERT_DOUBLE_EQ(torch_pendulum.getTimestep(), timestep); - ASSERT_EQ(torch_pendulum.getIntegrationType(), integration_type); - - // Check if the error is within a tolerance - // (This is arbitrary; you may need a looser or tighter tolerance) - EXPECT_NEAR(error, 0.0, 0.1) - << "Discrete dynamics: model deviance too large"; - - // Test Jacobians - auto A = torch_pendulum.getStateJacobian(test_state, test_control, 0.0); - auto B = torch_pendulum.getControlJacobian(test_state, test_control, 0.0); - - std::cout << "State Jacobian A:\n" << A << std::endl; - std::cout << "Control Jacobian B:\n" << B << std::endl; - - // Verify shapes - ASSERT_EQ(A.rows(), 2); - ASSERT_EQ(A.cols(), 2); - ASSERT_EQ(B.rows(), 2); - ASSERT_EQ(B.cols(), 1); -} - -/** - * @brief Demonstrates a small training loop that tries to fit the Torch model - * to data from an analytical pendulum's discrete dynamics. - */ -TEST(NeuralPendulumTest, Training) -{ - // Parameters - double timestep = 0.01; - double length = 1.0; - double mass = 1.0; - double damping = 0.1; - - // Create an analytical pendulum and a Torch model - cddp::Pendulum analytical_pendulum(timestep, length, mass, damping, "rk4"); - auto model = std::make_shared(length, mass, damping); - bool use_gpu = torch::cuda::is_available(); - - torch::optim::Adam optimizer(model->parameters(), torch::optim::AdamOptions(1e-3)); - int num_epochs = 100; - int batch_size = 32; - - // Generate training data - int num_samples = 1000; - auto cpu_options = torch::TensorOptions().dtype(torch::kFloat64).device(torch::kCPU); - auto state_tensor = torch::zeros({num_samples, 2}, cpu_options); - auto control_tensor = torch::zeros({num_samples, 1}, cpu_options); - auto next_state_tensor = torch::zeros({num_samples, 2}, cpu_options); - - for (int i = 0; i < num_samples; ++i) { - Eigen::VectorXd state(2); - state << (2.0 * rand() / RAND_MAX - 1.0) * M_PI, - (2.0 * rand() / RAND_MAX - 1.0) * 5.0; - Eigen::VectorXd control(1); - control << (2.0 * rand() / RAND_MAX - 1.0) * 2.0; - - auto next_state = analytical_pendulum.getDiscreteDynamics(state, control, 0.0); - - // Copy to torch Tensors - state_tensor[i] = torch::from_blob(state.data(), {2}, cpu_options).clone(); - control_tensor[i] = torch::from_blob(control.data(), {1}, cpu_options).clone(); - next_state_tensor[i] = torch::from_blob(next_state.data(), {2}, cpu_options).clone(); - } - - // Move to GPU if available - torch::Device device(use_gpu ? torch::kCUDA : torch::kCPU); - if (use_gpu) { - state_tensor = state_tensor.to(device).to(torch::kFloat32); - control_tensor = control_tensor.to(device).to(torch::kFloat32); - next_state_tensor = next_state_tensor.to(device).to(torch::kFloat32); - } else { - state_tensor = state_tensor.to(device); - control_tensor = control_tensor.to(device); - next_state_tensor = next_state_tensor.to(device); - } - - // Training loop - for (int epoch = 0; epoch < num_epochs; ++epoch) { - double total_loss = 0.0; - int num_batches = num_samples / batch_size; - - for (int batch = 0; batch < num_batches; ++batch) { - auto indices = torch::randperm(num_samples, - torch::TensorOptions().dtype(torch::kLong).device(device)); - indices = indices.slice(0, 0, batch_size); - - auto batch_states = state_tensor.index_select(0, indices); - auto batch_controls = control_tensor.index_select(0, indices); - auto batch_next_states = next_state_tensor.index_select(0, indices); - - optimizer.zero_grad(); - auto pred_next_states = model->forward({batch_states, batch_controls}); - if (use_gpu) { - pred_next_states = pred_next_states.to(torch::kFloat32); - } - auto loss = torch::mse_loss(pred_next_states, batch_next_states); - loss.backward(); - optimizer.step(); - - total_loss += loss.item(); - } - - if (epoch % 10 == 0) { - std::cout << "Epoch " << epoch - << ", Avg Loss: " << total_loss / num_batches << std::endl; - } - } - - // Test final performance - Eigen::VectorXd test_state(2); - test_state << M_PI/4, 0.0; - Eigen::VectorXd test_control(1); - test_control << 0.0; - - // Move test inputs to torch - auto cpu_options_64 = torch::TensorOptions().dtype(torch::kFloat64).device(torch::kCPU); - auto test_state_tensor = torch::from_blob(test_state.data(), {1, 2}, cpu_options_64).clone(); - auto test_control_tensor = torch::from_blob(test_control.data(), {1, 1}, cpu_options_64).clone(); - if (use_gpu) { - test_state_tensor = test_state_tensor.to(device).to(torch::kFloat32); - test_control_tensor = test_control_tensor.to(device).to(torch::kFloat32); - } - auto pred = model->forward({test_state_tensor, test_control_tensor}); - pred = pred.to(torch::kCPU).to(torch::kFloat64); // back to CPU/double - - // Copy to Eigen - Eigen::VectorXd pred_eigen(2); - std::memcpy(pred_eigen.data(), pred.data_ptr(), sizeof(double) * 2); - - // Compare to analytical - auto analytical_next = analytical_pendulum.getDiscreteDynamics(test_state, test_control, 0.0); - double error = (analytical_next - pred_eigen).norm(); - std::cout << "Test error: " << error << std::endl; - EXPECT_LT(error, 0.1); -} - diff --git a/tests/sqp_core/test_sqp.cpp b/tests/sqp_core/test_sqp.cpp deleted file mode 100644 index 8fae83f8..00000000 --- a/tests/sqp_core/test_sqp.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#include -#include -#include - -#include "gmock/gmock.h" -#include "gtest/gtest.h" - -#include "cddp.hpp" -#include "sqp_core/sqp.hpp" - -TEST(SQPTest, SolveUnicycle) { - // Problem parameters - int state_dim = 3; - int control_dim = 2; - int horizon = 100; - double timestep = 0.03; - std::string integration_type = "euler"; - - // Create a unicycle instance - std::unique_ptr system = std::make_unique( - timestep, integration_type - ); - - // Create objective function - Eigen::MatrixXd Q = 5 * Eigen::MatrixXd::Zero(state_dim, state_dim); - Eigen::MatrixXd R = 0.5 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 1200.0, 0.0, 0.0, - 0.0, 1200.0, 0.0, - 0.0, 0.0, 700.0; - - // Goal state - Eigen::VectorXd goal_state(state_dim); - goal_state << 2.0, 2.0, M_PI/2.0; - - // Create reference trajectory - std::vector empty_reference_states; - auto objective = std::make_unique( - Q, R, Qf, goal_state, empty_reference_states, timestep - ); - - // Initial state - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI/4.0; - - // Create SQP solver - cddp::SQPOptions options; - options.max_iterations = 20; - options.min_iterations = 3; - options.ftol = 1e-6; - options.xtol = 1e-6; - options.gtol = 1e-6; - options.eta = 1.0; - options.trust_region_radius = 100.0; - options.merit_penalty = 100.0; - options.verbose = true; - options.osqp_verbose = true; - - // Create SQP solver - cddp::SQPSolver sqp_solver(initial_state, goal_state, horizon, timestep); - sqp_solver.setDynamicalSystem(std::move(system)); - sqp_solver.setObjective(std::move(objective)); - sqp_solver.setOptions(options); - - // Define control constraints - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -1.0, -M_PI; - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 1.0, M_PI; - - // Add control box constraint - sqp_solver.addConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, control_upper_bound) - ); - - auto constraint = sqp_solver.getConstraint("ControlBoxConstraint"); - Eigen::VectorXd lb = constraint->getLowerBound(); - ASSERT_NE(constraint, nullptr); - ASSERT_EQ(lb.size(), control_dim); - ASSERT_EQ(lb, control_lower_bound); - - // Set initial trajectory - std::vector X(horizon + 1, Eigen::VectorXd::Zero(state_dim)); - std::vector U(horizon, Eigen::VectorXd::Zero(control_dim)); - sqp_solver.setInitialTrajectory(X, U); - - // Solve the problem - cddp::SQPResult solution = sqp_solver.solve(); - - // Verify solution - // ASSERT_TRUE(solution.success); - EXPECT_GT(solution.iterations, 0); - // EXPECT_LT(solution.iterations, options.max_iterations); - EXPECT_GT(solution.solve_time, 0.0); - - // Verify trajectories - ASSERT_EQ(solution.X.size(), horizon + 1); - ASSERT_EQ(solution.U.size(), horizon); - - - // Check initial and final states - std::cout << "Initial state: " << initial_state.transpose() << std::endl; - std::cout << "Final state: " << solution.X.back().transpose() << std::endl; - std::cout << "Goal state: " << goal_state.transpose() << std::endl; - EXPECT_NEAR((solution.X.front() - initial_state).norm(), 0.0, 1e-3); - // EXPECT_NEAR((solution.X.back() - goal_state).norm(), 0.0, 0.1); - - // Extract trajectories for plotting - auto X_sol = solution.X; - auto U_sol = solution.U; - - // Extract states and controls - std::vector x_arr, y_arr, theta_arr; - std::vector v_arr, omega_arr; - - for (size_t i = 0; i < X_sol.size(); ++i) { - x_arr.push_back(X_sol[i](0)); - y_arr.push_back(X_sol[i](1)); - theta_arr.push_back(X_sol[i](2)); - - if (i < U_sol.size()) { - v_arr.push_back(U_sol[i](0)); - omega_arr.push_back(U_sol[i](1)); - } - } -} \ No newline at end of file diff --git a/tests/sqp_core/test_sqp_core.cpp b/tests/sqp_core/test_sqp_core.cpp deleted file mode 100644 index ccb84350..00000000 --- a/tests/sqp_core/test_sqp_core.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include - -#include "gmock/gmock.h" -#include "gtest/gtest.h" - -#include "cddp.hpp" -#include "sqp_core/sqp_core.hpp" - -TEST(SQPIPOPTTest, CheckPointers) { - // Problem parameters. - int state_dim = 3; - int control_dim = 2; - int horizon = 100; - double timestep = 0.03; - std::string integration_type = "euler"; - - // Create a unicycle dynamical system. - std::unique_ptr system = - std::make_unique(timestep, integration_type); - - // Create the objective function. - Eigen::MatrixXd Q = 5 * Eigen::MatrixXd::Zero(state_dim, state_dim); - Eigen::MatrixXd R = 0.5 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 1200.0, 0.0, 0.0, - 0.0, 1200.0, 0.0, - 0.0, 0.0, 700.0; - Eigen::VectorXd goal_state(state_dim); - goal_state << 2.0, 2.0, M_PI / 2.0; - - // Create reference trajectory (empty in this example). - std::vector empty_reference_states; - auto objective = std::make_unique( - Q, R, Qf, goal_state, empty_reference_states, timestep - ); - - // Initial state. - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI / 4.0; - - // Create IPOPT-specific SCP options. - cddp::SCPOptions options; - options.max_iterations = 3; - options.min_iterations = 3; - options.ftol = 1e-6; - options.xtol = 1e-6; - options.gtol = 1e-6; - options.merit_penalty = 100.0; - options.verbose = true; - options.trust_region_radius = 100.0; - options.ipopt_max_iter = 100; - - // Create the SCP solver (using IPOPT). - cddp::SCPSolver sqp_solver(initial_state, goal_state, horizon, timestep); - sqp_solver.setDynamicalSystem(std::move(system)); - sqp_solver.setObjective(std::move(objective)); - sqp_solver.setOptions(options); - - // Test that the dynamical system and objective are properly set. - ASSERT_NE(sqp_solver.getDynamicalSystem(), nullptr) - << "Dynamical system pointer is null."; - ASSERT_NE(sqp_solver.getObjective(), nullptr) - << "Objective pointer is null."; - Eigen::VectorXd ref_state = sqp_solver.getObjective()->getReferenceState(); - ASSERT_GT(ref_state.size(), 0) - << "Reference state is empty."; - std::cout << "[TEST] Objective's reference state: " << ref_state.transpose() << std::endl; -} - -TEST(SQPIPOPTTest, SolveUnicycle) { - // Problem parameters. - int state_dim = 3; - int control_dim = 2; - int horizon = 100; - double timestep = 0.03; - std::string integration_type = "euler"; - - // Create a unicycle dynamical system. - std::unique_ptr system = - std::make_unique(timestep, integration_type); - - // Create the objective function. - Eigen::MatrixXd Q = 5 * Eigen::MatrixXd::Zero(state_dim, state_dim); - Eigen::MatrixXd R = 0.5 * Eigen::MatrixXd::Identity(control_dim, control_dim); - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf << 1200.0, 0.0, 0.0, - 0.0, 1200.0, 0.0, - 0.0, 0.0, 700.0; - Eigen::VectorXd goal_state(state_dim); - goal_state << 2.0, 2.0, M_PI / 2.0; - - // Create reference trajectory (empty in this example). - std::vector empty_reference_states; - auto objective = std::make_unique( - Q, R, Qf, goal_state, empty_reference_states, timestep - ); - - // Initial state. - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI / 4.0; - - // Create IPOPT-specific SCP options. - cddp::SCPOptions options; - options.max_iterations = 5; - options.min_iterations = 3; - options.ftol = 1e-6; - options.xtol = 1e-6; - options.gtol = 1e-6; - options.merit_penalty = 100.0; - options.verbose = true; - options.trust_region_radius = 100.0; - options.ipopt_print_level = 5; - - // Create the SCP solver. - cddp::SCPSolver sqp_solver(initial_state, goal_state, horizon, timestep); - sqp_solver.setDynamicalSystem(std::move(system)); - sqp_solver.setObjective(std::move(objective)); - sqp_solver.setOptions(options); - - // Define control constraints. - Eigen::VectorXd control_lower_bound(control_dim); - control_lower_bound << -1.0, -M_PI; - Eigen::VectorXd control_upper_bound(control_dim); - control_upper_bound << 1.0, M_PI; - - // Add control box constraint. - sqp_solver.addConstraint("ControlBoxConstraint", - std::make_unique(control_lower_bound, control_upper_bound) - ); - - auto constraint = sqp_solver.getConstraint("ControlBoxConstraint"); - ASSERT_NE(constraint, nullptr); - ASSERT_EQ(constraint->getLowerBound().size(), control_dim); - ASSERT_EQ(constraint->getLowerBound(), control_lower_bound); - - // Set initial trajectory (all zeros). - std::vector X(horizon + 1, Eigen::VectorXd::Zero(state_dim)); - std::vector U(horizon, Eigen::VectorXd::Zero(control_dim)); - sqp_solver.setInitialTrajectory(X, U); - - // Solve the problem. - cddp::SCPResult solution = sqp_solver.solve(); - - // Verify basic solution properties. - EXPECT_GT(solution.iterations, 0); - EXPECT_GT(solution.solve_time, 0.0); - - // Verify trajectory sizes. - ASSERT_EQ(solution.X.size(), horizon + 1); - ASSERT_EQ(solution.U.size(), horizon); - - // Check initial and final states. - std::cout << "Initial state: " << initial_state.transpose() << std::endl; - std::cout << "Final state: " << solution.X.back().transpose() << std::endl; - std::cout << "Goal state: " << goal_state.transpose() << std::endl; - EXPECT_NEAR((solution.X.front() - initial_state).norm(), 0.0, 1e-3); - // Optionally check the final state closeness to the goal. - // EXPECT_NEAR((solution.X.back() - goal_state).norm(), 0.0, 0.1); -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/test_gurobi.cpp b/tests/test_gurobi.cpp deleted file mode 100644 index abc931ed..00000000 --- a/tests/test_gurobi.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* Copyright 2024, Gurobi Optimization, LLC */ - -/* This example formulates and solves the following simple QP model: - - minimize x^2 + x*y + y^2 + y*z + z^2 + 2 x - subject to x + 2 y + 3 z >= 4 - x + y >= 1 - x, y, z non-negative - - It solves it once as a continuous model, and once as an integer model. -*/ - -#include "gurobi_c++.h" -using namespace std; - -int -main(int argc, - char *argv[]) -{ - try { - GRBEnv env = GRBEnv(); - - GRBModel model = GRBModel(env); - - // Create variables - - GRBVar x = model.addVar(0.0, 1.0, 0.0, GRB_CONTINUOUS, "x"); - GRBVar y = model.addVar(0.0, 1.0, 0.0, GRB_CONTINUOUS, "y"); - GRBVar z = model.addVar(0.0, 1.0, 0.0, GRB_CONTINUOUS, "z"); - - // Set objective - - GRBQuadExpr obj = x*x + x*y + y*y + y*z + z*z + 2*x; - model.setObjective(obj); - - // Add constraint: x + 2 y + 3 z >= 4 - - model.addConstr(x + 2 * y + 3 * z >= 4, "c0"); - - // Add constraint: x + y >= 1 - - model.addConstr(x + y >= 1, "c1"); - - // Optimize model - - model.optimize(); - - cout << x.get(GRB_StringAttr_VarName) << " " - << x.get(GRB_DoubleAttr_X) << endl; - cout << y.get(GRB_StringAttr_VarName) << " " - << y.get(GRB_DoubleAttr_X) << endl; - cout << z.get(GRB_StringAttr_VarName) << " " - << z.get(GRB_DoubleAttr_X) << endl; - - cout << "Obj: " << model.get(GRB_DoubleAttr_ObjVal) << endl; - - // Change variable types to integer - - x.set(GRB_CharAttr_VType, GRB_INTEGER); - y.set(GRB_CharAttr_VType, GRB_INTEGER); - z.set(GRB_CharAttr_VType, GRB_INTEGER); - - // Optimize model - - model.optimize(); - - cout << x.get(GRB_StringAttr_VarName) << " " - << x.get(GRB_DoubleAttr_X) << endl; - cout << y.get(GRB_StringAttr_VarName) << " " - << y.get(GRB_DoubleAttr_X) << endl; - cout << z.get(GRB_StringAttr_VarName) << " " - << z.get(GRB_DoubleAttr_X) << endl; - - cout << "Obj: " << model.get(GRB_DoubleAttr_ObjVal) << endl; - - } catch(GRBException e) { - cout << "Error code = " << e.getErrorCode() << endl; - cout << e.getMessage() << endl; - } catch(...) { - cout << "Exception during optimization" << endl; - } - - return 0; -} diff --git a/tests/test_qp_solvers.cpp b/tests/test_qp_solvers.cpp deleted file mode 100644 index c93ffbf4..00000000 --- a/tests/test_qp_solvers.cpp +++ /dev/null @@ -1,273 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#include -#include "gmock/gmock.h" -#include "gtest/gtest.h" -#include - -#include "cddp_core/qp_solver.hpp" -#include "cddp_core/boxqp.hpp" -#include "osqp++.h" -#include "gurobi_c++.h" - -using namespace std; -using namespace Eigen; -using namespace cddp; - -// Helper function to print solver results -void printResults(const std::string& solver_name, - const Eigen::VectorXd& solution, - double objective_value, - double elapsed_time, - int status, - double constraint_violation = 0.0) { - std::cout << "\n>>> Results from " << solver_name << ":" << std::endl; - std::cout << "optimal sol: " << solution.transpose() << std::endl; - std::cout << "optimal obj: " << objective_value << std::endl; - std::cout << "elapsed time: " << elapsed_time << "s" << std::endl; - std::cout << "status: " << status << std::endl; - if (constraint_violation != 0.0) { - std::cout << "constraint violation: " << constraint_violation << std::endl; - } - std::cout << ">>> End of " << solver_name << " test" << std::endl; -} - -TEST(QPSolver, ComparisonTest) { - // Problem setup - const int n = 5; // variables - const int m = 10; // constraints - - // Define QP problem - Matrix Q; - Matrix q; - Matrix A(m, 5); - VectorXd b(m); - VectorXd lb(m), ub(m); - - // Setup quadratic term and linear term - Q << 4.0, 1.0, 0.0, 0.0, 0.0, - 1.0, 2.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 3.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 2.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 2.0; - q << -1.0, -4.0, 0.0, -3.0, -2.0; - - // Setup constraints with both lower and upper bounds - A << 1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 1.0, - -1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, -1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -1.0, 0.0, 0.0, - 0.0, 0.0, 0.0, -1.0, 0.0, - 0.0, 0.0, 0.0, 0.0, -1.0; - b << 2.0, 2.0, 2.0, 2.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0; - lb = VectorXd::Constant(m/2, 0.0); - ub = VectorXd::Constant(m/2, 2.0); - - std::cout << "\n====== Comparing QP Solvers ======\n" << std::endl; - // 1. Test OSQP - { - // Convert to sparse matrices - SparseMatrix P(n, n); - SparseMatrix G(m/2, n); - // The first half of b is the upper bound, the second half is the lower bound - VectorXd uppper = b.head(m/2); - VectorXd lower = b.tail(m/2); - - // Convert Q to sparse - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - if (Q(i,j) != 0) { - P.insert(i, j) = Q(i,j); - } - } - } - P.makeCompressed(); - - // Convert A to sparse - for (int i = 0; i < m / 2; i++) { - for (int j = 0; j < n; j++) { - if (A(i,j) != 0) { - G.insert(i, j) = A(i,j); - } - } - } - G.makeCompressed(); - - osqp::OsqpSolver osqp_solver; - osqp::OsqpInstance instance; - osqp::OsqpSettings settings; - - instance.objective_matrix = P; - instance.objective_vector = q; - instance.constraint_matrix = G; - instance.upper_bounds = ub; - instance.lower_bounds = lb; - - settings.verbose = false; - - osqp_solver.Init(instance, settings); - - auto start_time = std::chrono::high_resolution_clock::now(); - auto status = osqp_solver.Solve(); - auto end_time = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end_time - start_time; - - printResults("OSQP", osqp_solver.primal_solution(), - osqp_solver.objective_value(), elapsed.count(), - static_cast(status)); - } - - // 3. Test BoxQP for comparison - only uses simple bounds - { - BoxQPOptions options; - options.verbose = false; - BoxQPSolver solver(options); - - VectorXd box_lb = VectorXd::Constant(n, 0.0); - VectorXd box_ub = VectorXd::Constant(n, 2.0); - - auto start_time = std::chrono::high_resolution_clock::now(); - BoxQPResult result = solver.solve(Q, q, box_lb, box_ub); - auto end_time = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end_time - start_time; - - printResults("BoxQP", result.x, result.final_value, - elapsed.count(), static_cast(result.status)); - } - - // // 4. Test Gurobi - // { - // GRBEnv env = GRBEnv(); - // GRBModel model = GRBModel(env); - - // // Create 5 variables with box constraints: 0 ≤ x ≤ 2 - // GRBVar x[5]; - // for (int i = 0; i < n; ++i) { - // x[i] = model.addVar(0.0, 2.0, 0.0, GRB_CONTINUOUS, "x" + std::to_string(i+1)); - // } - - // // Set objective: - // GRBQuadExpr obj = 0.0; - // // Add quadratic terms - // for (int i = 0; i < n; i++) { - // // diagonal terms - // obj += 0.5 * Q(i,i) * x[i] * x[i]; - // // off-diagonal terms (take only j > i to avoid double-counting) - // for (int j = i+1; j < n; j++) { - // if (Q(i,j) != 0.0) { - // // Add (Q(i,j) + Q(j,i))/2 * x_i * x_j because Q is symmetric - // double val = 0.5 * (Q(i,j) + Q(j,i)); - // obj += val * x[i] * x[j]; - // } - // } - // } - - // // Add linear terms - // for (int i = 0; i < n; i++) { - // if (q(i) != 0.0) { - // obj += q(i) * x[i]; - // } - // } - // model.setObjective(obj, GRB_MINIMIZE); - // model.getEnv().set(GRB_IntParam_OutputFlag, 0); - - // auto start_time = std::chrono::high_resolution_clock::now(); - // model.optimize(); - // auto end_time = std::chrono::high_resolution_clock::now(); - // std::chrono::duration elapsed = end_time - start_time; - - // printResults("Gurobi", VectorXd::Zero(n), model.get(GRB_DoubleAttr_ObjVal), - // elapsed.count(), model.get(GRB_IntAttr_Status)); - // } - - // 5. Test our QP solver - { - QPSolverOptions options; - options.verbose = false; - QPSolver solver(options); - - solver.setDimensions(n, m); - solver.setHessian(Q); - solver.setGradient(q); - solver.setConstraints(A, b); - - auto start_time = std::chrono::high_resolution_clock::now(); - QPResult result = solver.solve(); - auto end_time = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end_time - start_time; - - printResults("QPSolver", result.x, result.objective_value, - elapsed.count(), static_cast(result.status)); - - } -} - -/* $ ./tests/test_qp_solvers -[==========] Running 1 test from 1 test suite. -[----------] Global test environment set-up. -[----------] 1 test from QPSolver -[ RUN ] QPSolver.ComparisonTest - -====== Comparing QP Solvers ====== - - ->>> Results from OSQP: -optimal sol: -8.58736e-05 1.66673 0 0.666634 1 -optimal obj: -5.33339 -elapsed time: 5.712e-06s -status: 0 ->>> End of OSQP test - ->>> Results from SDQP: -optimal sol: 0 1.66667 0 0.666667 1 -optimal obj: -5.33333 -elapsed time: 1.0261e-05s -status: 0 ->>> End of SDQP test - ->>> Results from BoxQP: -optimal sol: 0 1.66667 0 0.666667 1 -optimal obj: -5.33333 -elapsed time: 6.885e-06s -status: 5 ->>> End of BoxQP test -Set parameter Username -Academic license - for non-commercial use only - expires 2025-09-25 - ->>> Results from Gurobi: -optimal sol: 0 0 0 0 0 -optimal obj: -5.33333 -elapsed time: 0.000398952s -status: 2 ->>> End of Gurobi test - ->>> Results from QPSolver: -optimal sol: 0 1.66667 -0 0.666667 1 -optimal obj: -5.33333 -elapsed time: 6.922e-06s -status: 0 ->>> End of QPSolver test -[ OK ] QPSolver.ComparisonTest (1 ms) -[----------] 1 test from QPSolver (1 ms total) - -[----------] Global test environment tear-down -[==========] 1 test from 1 test suite ran. (1 ms total) -[ PASSED ] 1 test. -*/ diff --git a/tests/test_torch.cpp b/tests/test_torch.cpp deleted file mode 100644 index 14ec69ce..00000000 --- a/tests/test_torch.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include -#include -#include -#include - -int main() { - // Define large, mid and small matrix dimensions - int large_rows = 1000; - int large_cols = 1000; - int mid_rows = 100; - int mid_cols = 100; - int small_rows = 10; - int small_cols = 10; - - // Create large, mid and small torch::Tensors and Eigen matrices - torch::Tensor large_torch_tensor = torch::rand({large_rows, large_cols}); - torch::Tensor mid_torch_tensor = torch::rand({mid_rows, mid_cols}); - torch::Tensor small_torch_tensor = torch::rand({small_rows, small_cols}); - Eigen::MatrixXd large_eigen_matrix = Eigen::MatrixXd::Random(large_rows, large_cols); - Eigen::MatrixXd mid_eigen_matrix = Eigen::MatrixXd::Random(mid_rows, mid_cols); - Eigen::MatrixXd small_eigen_matrix = Eigen::MatrixXd::Random(small_rows, small_cols); - - // Get the default device (CPU or CUDA if available) - torch::Device device = torch::cuda::is_available() ? torch::kCUDA : torch::kCPU; - // Print the default device - std::cout << "Default device: " << device << std::endl; - - // Move torch::Tensors to the default device - large_torch_tensor = large_torch_tensor.to(device); - mid_torch_tensor = mid_torch_tensor.to(device); - small_torch_tensor = small_torch_tensor.to(device); - - // Benchmark large torch::Tensor matrix multiplication - auto start_large_torch_matmul = std::chrono::high_resolution_clock::now(); - // pre-allocate memory for the result - torch::Tensor large_torch_result = torch::empty({large_rows, large_rows}, device); - large_torch_result = torch::matmul(large_torch_tensor, large_torch_tensor.transpose(0, 1)); - auto end_large_torch_matmul = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_large_torch_matmul = end_large_torch_matmul - start_large_torch_matmul; - - // Benchmark large Eigen matrix multiplication - auto start_large_eigen_matmul = std::chrono::high_resolution_clock::now(); - Eigen::MatrixXd large_eigen_result = large_eigen_matrix * large_eigen_matrix.transpose(); - auto end_large_eigen_matmul = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_large_eigen_matmul = end_large_eigen_matmul - start_large_eigen_matmul; - - // Benchmark mid torch::Tensor matrix multiplication - auto start_mid_torch_matmul = std::chrono::high_resolution_clock::now(); - // pre-allocate memory for the result - torch::Tensor mid_torch_result = torch::empty({mid_rows, mid_rows}, device); - mid_torch_result = torch::matmul(mid_torch_tensor, mid_torch_tensor.transpose(0, 1)); - auto end_mid_torch_matmul = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_mid_torch_matmul = end_mid_torch_matmul - start_mid_torch_matmul; - - // Benchmark mid Eigen matrix multiplication - auto start_mid_eigen_matmul = std::chrono::high_resolution_clock::now(); - Eigen::MatrixXd mid_eigen_result = mid_eigen_matrix * mid_eigen_matrix.transpose(); - auto end_mid_eigen_matmul = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_mid_eigen_matmul = end_mid_eigen_matmul - start_mid_eigen_matmul; - - // Benchmark small torch::Tensor matrix multiplication - auto start_small_torch_matmul = std::chrono::high_resolution_clock::now(); - // pre-allocate memory for the result - torch::Tensor small_torch_result = torch::empty({small_rows, small_rows}, device); - small_torch_result = torch::matmul(small_torch_tensor, small_torch_tensor.transpose(0, 1)); - auto end_small_torch_matmul = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_small_torch_matmul = end_small_torch_matmul - start_small_torch_matmul; - - // Benchmark small Eigen matrix multiplication - auto start_small_eigen_matmul = std::chrono::high_resolution_clock::now(); - Eigen::MatrixXd small_eigen_result = small_eigen_matrix * small_eigen_matrix.transpose(); - auto end_small_eigen_matmul = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_small_eigen_matmul = end_small_eigen_matmul - start_small_eigen_matmul; - - // Benchmark small torch::Tensor inverse - auto start_small_torch_inverse = std::chrono::high_resolution_clock::now(); - // pre-allocate memory for the result - torch::Tensor small_torch_inverse = torch::empty({small_rows, small_cols}, device); - small_torch_inverse = torch::inverse(small_torch_tensor); - auto end_small_torch_inverse = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_small_torch_inverse = end_small_torch_inverse - start_small_torch_inverse; - - // Benchmark small Eigen matrix inverse - auto start_small_eigen_inverse = std::chrono::high_resolution_clock::now(); - Eigen::MatrixXd small_eigen_inverse = small_eigen_matrix.inverse(); - auto end_small_eigen_inverse = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed_small_eigen_inverse = end_small_eigen_inverse - start_small_eigen_inverse; - - // Print results - std::cout << "Large Torch::Tensor matrix multiplication time: " << elapsed_large_torch_matmul.count() << " seconds" << std::endl; - std::cout << "Large Eigen matrix multiplication time: " << elapsed_large_eigen_matmul.count() << " seconds" << std::endl; - std::cout << "Mid Torch::Tensor matrix multiplication time: " << elapsed_mid_torch_matmul.count() << " seconds" << std::endl; - std::cout << "Mid Eigen matrix multiplication time: " << elapsed_mid_eigen_matmul.count() << " seconds" << std::endl; - std::cout << "Small Torch::Tensor matrix multiplication time: " << elapsed_small_torch_matmul.count() << " seconds" << std::endl; - std::cout << "Small Eigen matrix multiplication time: " << elapsed_small_eigen_matmul.count() << " seconds" << std::endl; - std::cout << "Small Torch::Tensor inverse time: " << elapsed_small_torch_inverse.count() << " seconds" << std::endl; - std::cout << "Small Eigen matrix inverse time: " << elapsed_small_eigen_inverse.count() << " seconds" << std::endl; - - return 0; -} - -// PC specs: -// - CPU: 13th Gen Intel® Core™ i7-13620H × 16 -// - GPU: NVIDIA GeForce RTX 4060 -// ``` -// $ ./test_torch_eigen -// Default device: cuda -// Large Torch::Tensor matrix multiplication time: 0.0187716 seconds -// Large Eigen matrix multiplication time: 0.121851 seconds -// Mid Torch::Tensor matrix multiplication time: 0.00302689 seconds -// Mid Eigen matrix multiplication time: 0.000178552 seconds -// Small Torch::Tensor matrix multiplication time: 0.00202544 seconds -// Small Eigen matrix multiplication time: 2.229e-06 seconds -// Small Torch::Tensor inverse time: 0.0824715 seconds -// Small Eigen matrix inverse time: 7.489e-06 seconds -// ``` \ No newline at end of file From 4fad7375923d0355607958d7f8b87eb2eb43801b Mon Sep 17 00:00:00 2001 From: Tomo Sasaki Date: Sat, 28 Mar 2026 16:29:10 -0400 Subject: [PATCH 2/4] Fix boxqp test compile error Remove the leftover test-output lines at the end of test_boxqp.cpp. This restores a clean build so the unit test suite runs successfully again. --- tests/cddp_core/test_boxqp.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/cddp_core/test_boxqp.cpp b/tests/cddp_core/test_boxqp.cpp index 8b437cdc..2f051db1 100644 --- a/tests/cddp_core/test_boxqp.cpp +++ b/tests/cddp_core/test_boxqp.cpp @@ -236,9 +236,3 @@ int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } -[----------] 1 test from QPSolver (1 ms total) - -[----------] Global test environment tear-down -[==========] 1 test from 1 test suite ran. (1 ms total) -[ PASSED ] 1 test. -*/ From 593d39c99c8f4df903660b8218ae996011808d06 Mon Sep 17 00:00:00 2001 From: Tomo Sasaki Date: Sat, 28 Mar 2026 16:31:56 -0400 Subject: [PATCH 3/4] Remove SNOPT and neural-dynamics remnants Drop the remaining SNOPT build/example wiring and delete leftover neural-dynamics model artifacts. Keep the trimmed repository aligned with the supported build and test surface. --- CMakeLists.txt | 53 +- README.md | 1 - examples/CMakeLists.txt | 6 - .../neural_models/pendulum_compare.png | Bin 42818 -> 0 bytes .../neural_models/pendulum_model.pt | Bin 13380 -> 0 bytes .../neural_models/training_loss.png | Bin 19159 -> 0 bytes examples/snopt_unicycle.cpp | 469 ------------------ 7 files changed, 1 insertion(+), 528 deletions(-) delete mode 100644 examples/neural_dynamics/neural_models/pendulum_compare.png delete mode 100644 examples/neural_dynamics/neural_models/pendulum_model.pt delete mode 100644 examples/neural_dynamics/neural_models/training_loss.png delete mode 100644 examples/snopt_unicycle.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0414a29f..adc57864 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,17 +47,6 @@ option(CDDP_CPP_BUILD_TESTS "Whether to build tests." ON) # CasADi Configuration option(CDDP_CPP_CASADI "Whether to use CasADi" OFF) -# SNOPT Configuration -# If you want to install SNOPT, please follow the instructions at: -# https://ccom.ucsd.edu/~optimizers/ -option(CDDP_CPP_SNOPT "Whether to use SNOPT solver." OFF) -option(SNOPT_ROOT "Path to SNOPT installation" "") -if(APPLE) - set(SNOPT_ROOT /usr/local/lib/snopt) # macOS default path -else() - set(SNOPT_ROOT /home/astomodynamics/.local/lib/snopt) # Linux default path -endif() - # ACADOS Configuration # For ACADOS installation, see: https://docs.acados.org/ option(CDDP_CPP_ACADOS "Whether to use ACADOS solver." OFF) @@ -87,7 +76,7 @@ if (CDDP_CPP_CASADI) # Assuming that CasADi is installed in /usr/local/include/casadi by: # https://github.com/casadi/casadi/wiki/InstallationLinux # $ echo "export LD_LIBRARY_PATH=/home//casadi/build/lib:$LD_LIBRARY_PATH" >> ~/.bashrc && source ~/.bashrc - # If you use Ipopt or SNOPT, you need to set flags for CasADi installation. + # If you use Ipopt, you may need to set flags for CasADi installation. find_package(casadi REQUIRED) set(CASADI_INCLUDE_DIR /usr/local/include/casadi) @@ -208,46 +197,6 @@ if (CDDP_CPP_CASADI) target_link_libraries(${PROJECT_NAME} ${CASADI_LIBRARIES}) endif() - -# SNOPT -if (CDDP_CPP_SNOPT) - if (NOT SNOPT_ROOT) - message(FATAL_ERROR "Please set SNOPT_ROOT to your SNOPT installation directory.") - endif() - - # Set SNOPT paths - set(SNOPT_INCLUDE_DIRS ${SNOPT_ROOT}/include) - set(SNOPT_LIB_DIR ${SNOPT_ROOT}/lib) - - # Find SNOPT libraries - find_library(SNOPT_LIBRARY NAMES snopt7 snopt snopt7_cpp PATHS ${SNOPT_LIB_DIR}) - - # Find header files - find_path(SNOPT_INCLUDE_DIR snopt.h PATHS ${SNOPT_INCLUDE_DIRS}) - - # Set up library list - set(SNOPT_LIBRARIES) - if(SNOPT_LIBRARY) - list(APPEND SNOPT_LIBRARIES ${SNOPT_LIBRARY}) - endif() - - if (SNOPT_LIBRARIES AND SNOPT_INCLUDE_DIR) - message(STATUS "Found SNOPT libraries: ${SNOPT_LIBRARIES}") - message(STATUS "SNOPT include directory: ${SNOPT_INCLUDE_DIR}") - - include_directories(${SNOPT_INCLUDE_DIR}) - link_directories(${SNOPT_LIB_DIR}) - target_link_libraries(${PROJECT_NAME} ${SNOPT_LIBRARIES}) - - # Add preprocessor definition to enable SNOPT in code - target_compile_definitions(${PROJECT_NAME} PRIVATE CDDP_CPP_SNOPT_ENABLED=1) - - message(STATUS "Successfully linked SNOPT.") - else() - message(FATAL_ERROR "Could not find SNOPT libraries. Please check SNOPT_ROOT: ${SNOPT_ROOT}") - endif() -endif() - # ACADOS if (CDDP_CPP_ACADOS) if (NOT ACADOS_ROOT) diff --git a/README.md b/README.md index 438f04fb..3f7d2059 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,6 @@ This library also uses the following open-source libraries for optional features * [Ipopt](https://github.com/coin-or/Ipopt) (EPL License) * [CasADi](https://web.casadi.org/) (GPL License) -* [SNOPT](https://ccom.ucsd.edu/~optimizers/) (GPL License) ## Citing diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2ce08a13..af030631 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -121,9 +121,3 @@ if (CDDP_CPP_CASADI) add_executable(ipopt_spacecrat_linear_fuel ipopt_spacecrat_linear_fuel.cpp) target_link_libraries(ipopt_spacecrat_linear_fuel cddp) endif() - -# SNOPT examples -if (CDDP_CPP_CASADI AND CDDP_CPP_SNOPT) - add_executable(snopt_unicycle snopt_unicycle.cpp) - target_link_libraries(snopt_unicycle cddp) -endif() diff --git a/examples/neural_dynamics/neural_models/pendulum_compare.png b/examples/neural_dynamics/neural_models/pendulum_compare.png deleted file mode 100644 index d009969b94d142abd2f8995f4059f0f26af6aec0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42818 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 LlOulq!?XVn4AoF_ diff --git a/examples/neural_dynamics/neural_models/training_loss.png b/examples/neural_dynamics/neural_models/training_loss.png deleted file mode 100644 index ad302d9b377c1ef6ca8a591d7e15aa7b055adcef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19159 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 diff --git a/examples/snopt_unicycle.cpp b/examples/snopt_unicycle.cpp deleted file mode 100644 index b1c3df92..00000000 --- a/examples/snopt_unicycle.cpp +++ /dev/null @@ -1,469 +0,0 @@ -/* - Copyright 2025 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#include -#include -#include -#include -#include -#include -#include - -#include "cddp.hpp" -#include "matplot/matplot.h" - -using namespace matplot; -namespace fs = std::filesystem; - -int main() { - ////////// Problem Setup ////////// - const int state_dim = 3; // [x, y, theta] - const int control_dim = 2; // [v, omega] - const int horizon = 100; // Number of control intervals - const double timestep = 0.03; // Time step - - // Define initial and goal states - Eigen::VectorXd initial_state(state_dim); - initial_state << 0.0, 0.0, M_PI/4.0; - - Eigen::VectorXd goal_state(state_dim); - goal_state << 2.0, 2.0, M_PI/2.0; - // Define cost weighting matrices - Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); - - Eigen::MatrixXd R = 0.5 * Eigen::MatrixXd::Identity(control_dim, control_dim); - - // Terminal cost weight (optional if using terminal constraint) - Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); - Qf(0, 0) = 100.0; // x position - Qf(1, 1) = 100.0; // y position - Qf(2, 2) = 100.0; // heading - - - casadi::DM Q_dm(Q.rows(), Q.cols()); - for (int i = 0; i < Q.rows(); i++) { - for (int j = 0; j < Q.cols(); j++) { - Q_dm(i, j) = Q(i, j); - } - } - casadi::DM R_dm(R.rows(), R.cols()); - for (int i = 0; i < R.rows(); i++) { - for (int j = 0; j < R.cols(); j++) { - R_dm(i, j) = R(i, j); - } - } - casadi::DM Qf_dm(Qf.rows(), Qf.cols()); - for (int i = 0; i < Qf.rows(); i++) { - for (int j = 0; j < Qf.cols(); j++) { - Qf_dm(i, j) = Qf(i, j); - } - } - - // Define control bounds (for example, v ∈ [-1, 1] and ω ∈ [-π, π]) - Eigen::VectorXd u_min(control_dim), u_max(control_dim); - u_min << -1.0, -M_PI; - u_max << 1.0, M_PI; - - const int n_states = (horizon + 1) * state_dim; - const int n_controls = horizon * control_dim; - const int n_dec = n_states + n_controls; - - // Define symbolic variables for states and controls - casadi::MX X = casadi::MX::sym("X", n_states); - casadi::MX U = casadi::MX::sym("U", n_controls); - casadi::MX z = casadi::MX::vertcat({X, U}); - - // Helper lambdas to extract the state and control at time step t - auto X_t = [=](int t) -> casadi::MX { - return X(casadi::Slice(t * state_dim, (t + 1) * state_dim)); - }; - auto U_t = [=](int t) -> casadi::MX { - return U(casadi::Slice(t * control_dim, (t + 1) * control_dim)); - }; - - auto unicycle_dynamics = [=](casadi::MX x, casadi::MX u) -> casadi::MX { - casadi::MX x_next = casadi::MX::zeros(state_dim, 1); - casadi::MX theta = x(2); - casadi::MX v = u(0); - casadi::MX omega = u(1); - - // Use casadi's trigonometric functions - using casadi::cos; - using casadi::sin; - casadi::MX ctheta = cos(theta); - casadi::MX stheta = sin(theta); - // Euler integration discretization - x_next(0) = x(0) + v * ctheta * timestep; - x_next(1) = x(1) + v * stheta * timestep; - x_next(2) = x(2) + omega * timestep; - return x_next; - }; - - casadi::MX g; - - // Initial state constraint: X₀ = initial_state - casadi::DM init_state_dm(std::vector(initial_state.data(), initial_state.data() + state_dim)); - g = casadi::MX::vertcat({g, X_t(0) - init_state_dm}); - - // Dynamics constraints: - for (int t = 0; t < horizon; t++) { - casadi::MX x_next_expr = unicycle_dynamics(X_t(t), U_t(t)); - g = casadi::MX::vertcat({g, X_t(t + 1) - x_next_expr}); - } - - // --- Terminal Condition Constraint --- - casadi::DM goal_dm(std::vector(goal_state.data(), goal_state.data() + state_dim)); - casadi::MX terminal_constr = X_t(horizon) - goal_dm; - g = casadi::MX::vertcat({g, terminal_constr}); - - ////////// Cost Function ////////// - casadi::MX cost = casadi::MX::zeros(1, 1); - for (int t = 0; t < horizon; t++) { - casadi::MX x_diff = X_t(t) - goal_dm; - casadi::MX u_diff = U_t(t); - casadi::MX state_cost = casadi::MX::mtimes({x_diff.T(), Q_dm, x_diff}); - casadi::MX control_cost = casadi::MX::mtimes({u_diff.T(), R_dm, u_diff}); - cost = cost + state_cost + control_cost; - } - // Terminal cost - casadi::MX x_diff_final = X_t(horizon) - goal_dm; - casadi::MX terminal_cost = casadi::MX::mtimes({x_diff_final.T(), Qf_dm, x_diff_final}); - cost = cost + terminal_cost; - - ////////// Variable Bounds and Initial Guess ////////// - std::vector lbx(n_dec, -1e20); - std::vector ubx(n_dec, 1e20); - // Apply control bounds for the control segments. - for (int t = 0; t < horizon; t++) { - for (int i = 0; i < control_dim; i++) { - lbx[n_states + t * control_dim + i] = u_min(i); - ubx[n_states + t * control_dim + i] = u_max(i); - } - } - - // The complete set of constraints (g) must be equal to zero. - const int n_g = static_cast(g.size1()); - std::vector lbg(n_g, 0.0); - std::vector ubg(n_g, 0.0); - - // Provide an initial guess for the decision vector. - std::vector x0(n_dec, 0.0); - // Set the initial state portion. - for (int i = 0; i < state_dim; i++) { - x0[i] = initial_state(i); - } - // Linearly interpolate the state trajectory from initial_state to goal_state. - for (int t = 1; t <= horizon; t++) { - for (int i = 0; i < state_dim; i++) { - x0[t * state_dim + i] = initial_state(i) + (goal_state(i) - initial_state(i)) * (double)t / horizon; - } - } - // The control part of the initial guess remains zero. - - ////////// NLP Definition and SNOPT Solver Setup ////////// - std::map nlp; - nlp["x"] = z; - nlp["f"] = cost; - nlp["g"] = g; - - casadi::Dict solver_opts; - solver_opts["print_time"] = true; - // SNOPT-specific options - solver_opts["snopt.print_level"] = 1; - solver_opts["snopt.major_iterations_limit"] = 500; - solver_opts["snopt.minor_iterations_limit"] = 500; - // solver_opts["snopt.major_optimality_tolerance"] = 1e-6; - // solver_opts["snopt.major_feasibility_tolerance"] = 1e-6; - // solver_opts["snopt.minor_feasibility_tolerance"] = 1e-6; - // solver_opts["snopt.verify_level"] = 0; // 0 = no verification, -1 = cheap test, 1 = individual gradients - // solver_opts["start"] = "cold"; // cold or warm start - - // Create the NLP solver instance using SNOPT. - casadi::Function solver = casadi::nlpsol("solver", "snopt", nlp, solver_opts); - - // Convert the initial guess and bounds into DM objects. - casadi::DM x0_dm = casadi::DM(x0); - casadi::DM lbx_dm = casadi::DM(lbx); - casadi::DM ubx_dm = casadi::DM(ubx); - casadi::DM lbg_dm = casadi::DM(lbg); - casadi::DM ubg_dm = casadi::DM(ubg); - - casadi::DMDict arg({ - {"x0", x0_dm}, - {"lbx", lbx_dm}, - {"ubx", ubx_dm}, - {"lbg", lbg_dm}, - {"ubg", ubg_dm} - }); - - ////////// Solve the NLP ////////// - auto start_time = std::chrono::high_resolution_clock::now(); - casadi::DMDict res = solver(arg); - auto end_time = std::chrono::high_resolution_clock::now(); - std::chrono::duration elapsed = end_time - start_time; - std::cout << "SNOPT Solver elapsed time: " << elapsed.count() << " s" << std::endl; - - ////////// Extract and Display the Solution ////////// - // The result 'res["x"]' is a DM vector with the optimized decision variables. - std::vector sol = std::vector(res.at("x")); - - // Convert to state and control trajectories - std::vector X_sol(horizon + 1, Eigen::VectorXd(state_dim)); - std::vector U_sol(horizon, Eigen::VectorXd(control_dim)); - std::vector t_sol(horizon + 1); - for (int t = 0; t <= horizon; t++) { - t_sol[t] = t * timestep; - } - - for (int t = 0; t <= horizon; t++) - { - for (int i = 0; i < state_dim; i++) - { - X_sol[t](i) = sol[t * state_dim + i]; - } - } - - for (int t = 0; t < horizon; t++) - { - for (int i = 0; i < control_dim; i++) - { - U_sol[t](i) = sol[n_states + t * control_dim + i]; - } - } - - // Create directory for saving plot (if it doesn't exist) - const std::string plotDirectory = "../results/tests"; - if (!fs::exists(plotDirectory)) { - fs::create_directory(plotDirectory); - } - - // Plot the solution (x-y plane) - std::vector x_arr, y_arr, theta_arr; - for (const auto& x : X_sol) { - x_arr.push_back(x(0)); - y_arr.push_back(x(1)); - theta_arr.push_back(x(2)); - } - - // Plot the solution (control inputs) - std::vector v_arr, omega_arr; - for (const auto& u : U_sol) { - v_arr.push_back(u(0)); - omega_arr.push_back(u(1)); - } - - // ----------------------------- - // Plot states and controls - // ----------------------------- - auto f1 = figure(); - f1->size(1200, 800); - - // First subplot: Position Trajectory - auto ax1 = subplot(3, 1, 0); - auto plot_handle = plot(ax1, x_arr, y_arr, "-b"); - plot_handle->line_width(3); - title(ax1, "Position Trajectory (SNOPT)"); - xlabel(ax1, "x [m]"); - ylabel(ax1, "y [m]"); - - // Second subplot: Heading Angle vs Time - auto ax2 = subplot(3, 1, 1); - auto heading_plot_handle = plot(ax2, t_sol, theta_arr); - heading_plot_handle->line_width(3); - title(ax2, "Heading Angle (SNOPT)"); - xlabel(ax2, "Time [s]"); - ylabel(ax2, "theta [rad]"); - - // Fourth subplot: Control Inputs - auto ax4 = subplot(3, 1, 2); - auto p1 = plot(ax4, v_arr, "--b"); - p1->line_width(3); - p1->display_name("Acceleration"); - - hold(ax4, true); - auto p2 = plot(ax4, omega_arr, "--r"); - p2->line_width(3); - p2->display_name("Steering"); - - title(ax4, "Control Inputs (SNOPT)"); - xlabel(ax4, "Step"); - ylabel(ax4, "Control"); - matplot::legend(ax4); - - f1->draw(); - f1->save(plotDirectory + "/unicycle_snopt_results.png"); - - // ----------------------------- - // Animation: unicycle Trajectory - // ----------------------------- - auto f2 = figure(); - f2->size(800, 600); - auto ax_anim = f2->current_axes(); - if (!ax_anim) - { - ax_anim = axes(); - } - - double car_length = 0.35; - double car_width = 0.15; - - for (size_t i = 0; i < X_sol.size(); ++i) - { - if (i % 10 == 0) - { - ax_anim->clear(); - hold(ax_anim, true); - - double x = x_arr[i]; - double y = y_arr[i]; - double theta = theta_arr[i]; - - // Compute unicycle rectangle corners - std::vector car_x(5), car_y(5); - car_x[0] = x + car_length / 2 * cos(theta) - car_width / 2 * sin(theta); - car_y[0] = y + car_length / 2 * sin(theta) + car_width / 2 * cos(theta); - car_x[1] = x + car_length / 2 * cos(theta) + car_width / 2 * sin(theta); - car_y[1] = y + car_length / 2 * sin(theta) - car_width / 2 * cos(theta); - car_x[2] = x - car_length / 2 * cos(theta) + car_width / 2 * sin(theta); - car_y[2] = y - car_length / 2 * sin(theta) - car_width / 2 * cos(theta); - car_x[3] = x - car_length / 2 * cos(theta) - car_width / 2 * sin(theta); - car_y[3] = y - car_length / 2 * sin(theta) + car_width / 2 * cos(theta); - car_x[4] = car_x[0]; - car_y[4] = car_y[0]; - - auto car_line = plot(ax_anim, car_x, car_y); - car_line->color("black"); - car_line->line_style("solid"); - car_line->line_width(2); - car_line->display_name("Car"); - - // Plot trajectory up to current frame - std::vector traj_x(x_arr.begin(), x_arr.begin() + i + 1); - std::vector traj_y(y_arr.begin(), y_arr.begin() + i + 1); - auto traj_line = plot(ax_anim, traj_x, traj_y); - traj_line->color("blue"); - traj_line->line_style("solid"); - traj_line->line_width(1.5); - traj_line->display_name("Trajectory"); - - title(ax_anim, "Unicycle Trajectory (SNOPT)"); - xlabel(ax_anim, "x [m]"); - ylabel(ax_anim, "y [m]"); - xlim(ax_anim, {-1, 2.2}); - ylim(ax_anim, {-1, 2.2}); - // legend(ax_anim); - - std::string filename = plotDirectory + "/unicycle_snopt_frame_" + std::to_string(i) + ".png"; - f2->draw(); - f2->save(filename); - std::this_thread::sleep_for(std::chrono::milliseconds(80)); - } - } - - // ----------------------------- - // Generate GIF from frames using ImageMagick - // ----------------------------- - std::string gif_command = "convert -delay 30 " + plotDirectory + "/unicycle_snopt_frame_*.png " + plotDirectory + "/unicycle_snopt.gif"; - std::system(gif_command.c_str()); - - std::string cleanup_command = "rm " + plotDirectory + "/unicycle_snopt_frame_*.png"; - std::system(cleanup_command.c_str()); - - std::cout << "GIF animation created successfully: " << plotDirectory + "/unicycle_snopt.gif" << std::endl; - - return 0; -} - -// :~/github/cddp-cpp/build$ ./examples/snopt_unicycle -// ============================== -// SNOPT C interface 2.2.0 -// ============================== -// S N O P T 7.7.7 (Feb 2021) -// ============================== - -// SNMEMB EXIT 100 -- finished successfully -// SNMEMB INFO 104 -- memory requirements estimated - -// Trial version of SNOPT -- for evaluation or academic purposes only - - - -// Nonlinear constraints 306 Linear constraints 0 -// Nonlinear variables 503 Linear variables 0 -// Jacobian variables 503 Objective variables 503 -// Total constraints 306 Total variables 503 - - - -// The user has defined 1106 out of 1106 constraint gradients. -// The user has defined 503 out of 503 objective gradients. - - -// Minor NumInf FP mult FP step rgNorm SumInf nS -// 100 1 1.3E-01 3.2E-01 5.1588740E+01 - -// Minor NonOpt QP mult QP step rgNorm Elastic QP obj nS -// 200 102 -4.1E-01 1.0E+00 2.9E-11 9.0584333E+04 72 - -// Major Minors Step nCon Feasible Optimal MeritFunction nS Penalty -// 0 227 1 1.0E-02 2.2E-02 0.0000000E+00 99 r iT -// 1 64 1.0E+00 2 8.2E-02 2.7E+00 9.1086436E+04 109 4.8E+07 rl -// 2 4 8.9E-01 3 8.3E-03 1.4E+00 1.0615859E+04 108 2.7E+04 s l -// 3 56 1.0E+00 4 7.7E-04 3.9E+00 6.7037234E+01 163 9.4E+02 -// 4 24 1.0E+00 5 (1.4E-07) 1.3E-02 6.1572688E+01 140 1.8E+02 -// 5 27 1.0E+00 6 (5.3E-08) 2.5E-02 6.1489200E+01 114 7.8E+01 -// 6 3 1.0E+00 7 (7.0E-07) 2.9E-02 6.1150122E+01 112 7.8E+01 -// 7 1 1.0E+00 8 1.3E-05 6.1E-02 6.0022814E+01 112 7.8E+01 -// 8 2 1.0E+00 9 3.8E-06 1.8E-02 5.9907766E+01 113 7.8E+01 -// 9 1 1.0E+00 10 (2.7E-09) 7.3E-03 5.9906665E+01 113 7.8E+01 - -// Major Minors Step nCon Feasible Optimal MeritFunction nS Penalty -// 10 1 1.0E+00 11 (9.1E-07) 4.3E-03 5.9880339E+01 113 7.8E+01 -// 11 1 1.0E+00 12 3.7E-06 1.7E-02 5.9858922E+01 113 7.8E+01 -// 12 1 1.0E+00 13 (4.0E-09) 2.8E-04 5.9858653E+01 113 7.8E+01 R -// 13 2 1.0E+00 14 (4.9E-11) 3.9E-04 5.9858608E+01 112 7.8E+01 s -// 14 1 1.0E+00 15 (3.1E-07) 4.7E-06 5.9857118E+01 112 7.8E+01 -// 15 1 1.0E+00 16 (1.2E-11)(7.7E-07) 5.9857118E+01 112 7.8E+01 - -// SNOPTC EXIT 0 -- finished successfully -// SNOPTC INFO 1 -- optimality conditions satisfied - -// Problem name solver -// No. of iterations 416 Objective 5.9857118027E+01 -// No. of major iterations 15 Linear obj. term 0.0000000000E+00 -// Penalty parameter 7.838E+01 Nonlinear obj. term 5.9857118027E+01 -// User function calls (total) 17 -// No. of superbasics 112 No. of basic nonlinears 306 -// No. of degenerate steps 1 Percentage 0.24 -// Max x 301 2.0E+00 Max pi 250 4.2E+01 -// Max Primal infeas 786 2.5E-11 Max Dual infeas 501 3.3E-05 -// Nonlinear constraint violn 2.5E-11 - - - -// Solution printed on file 10 - -// Time for MPS input 0.00 seconds -// Time for solving problem 0.03 seconds -// Time for solution output 0.00 seconds -// Time for constraint functions 0.00 seconds -// Time for objective function 0.00 seconds -// solver : t_proc (avg) t_wall (avg) n_eval -// nlp_jac_f | 627.00us ( 33.00us) 625.49us ( 32.92us) 19 -// nlp_jac_g | 2.19ms (115.26us) 2.19ms (115.40us) 19 -// total | 31.82ms ( 31.82ms) 31.82ms ( 31.82ms) 1 -// SNOPT Solver elapsed time: 0.0319228 s -// GIF animation created successfully: ../results/tests/unicycle_snopt.gif \ No newline at end of file From 55aa4909605f830713b87aef08398b65c2e36d69 Mon Sep 17 00:00:00 2001 From: Tomo Sasaki Date: Sat, 28 Mar 2026 16:37:27 -0400 Subject: [PATCH 4/4] Remove ALDDP and DBAS-DDP solvers Drop the ALDDP solver from the build, public API, factory, and tests. Remove the dormant DBAS-DDP files and option plumbing to keep the solver surface consistent. --- CMakeLists.txt | 1 - include/cddp-cpp/cddp.hpp | 1 - include/cddp-cpp/cddp_core/alddp_solver.hpp | 142 --- include/cddp-cpp/cddp_core/cddp_core.hpp | 3 +- .../cddp-cpp/cddp_core/dbas_ddp_solver.hpp | 289 ------ include/cddp-cpp/cddp_core/options.hpp | 147 --- src/cddp_core/alddp_solver.cpp | 911 ------------------ src/cddp_core/cddp_core.cpp | 7 +- src/cddp_core/dbas_ddp_solver.cpp | 0 tests/CMakeLists.txt | 4 - tests/cddp_core/test_alddp_solver.cpp | 560 ----------- 11 files changed, 2 insertions(+), 2063 deletions(-) delete mode 100644 include/cddp-cpp/cddp_core/alddp_solver.hpp delete mode 100644 include/cddp-cpp/cddp_core/dbas_ddp_solver.hpp delete mode 100644 src/cddp_core/alddp_solver.cpp delete mode 100644 src/cddp_core/dbas_ddp_solver.cpp delete mode 100644 tests/cddp_core/test_alddp_solver.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index adc57864..1cb1ac82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,7 +143,6 @@ set(cddp_core_srcs src/cddp_core/logddp_solver.cpp src/cddp_core/ipddp_solver.cpp src/cddp_core/msipddp_solver.cpp - src/cddp_core/alddp_solver.cpp ) set(dynamics_model_srcs diff --git a/include/cddp-cpp/cddp.hpp b/include/cddp-cpp/cddp.hpp index b41d4584..1bfe61ad 100644 --- a/include/cddp-cpp/cddp.hpp +++ b/include/cddp-cpp/cddp.hpp @@ -32,7 +32,6 @@ #include "cddp_core/logddp_solver.hpp" #include "cddp_core/ipddp_solver.hpp" #include "cddp_core/msipddp_solver.hpp" -#include "cddp_core/alddp_solver.hpp" #include "cddp_core/helper.hpp" #include "cddp_core/boxqp.hpp" #include "cddp_core/qp_solver.hpp" diff --git a/include/cddp-cpp/cddp_core/alddp_solver.hpp b/include/cddp-cpp/cddp_core/alddp_solver.hpp deleted file mode 100644 index f9e3d704..00000000 --- a/include/cddp-cpp/cddp_core/alddp_solver.hpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2024 Tomo Sasaki - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef CDDP_ALDDP_SOLVER_HPP -#define CDDP_ALDDP_SOLVER_HPP - -#include "cddp_core/cddp_core.hpp" -#include -#include -#include - -namespace cddp { - -/** - * @brief Augmented Lagrangian Differential Dynamic Programming (ALDDP) solver - * implementation. - * - * This class implements the ISolverAlgorithm interface to provide - * a simplified augmented lagrangian-based DDP solver for handling constraints. - */ -class AlddpSolver : public ISolverAlgorithm { -public: - /** - * @brief Default constructor. - */ - AlddpSolver(); - - /** - * @brief Initialize the solver with the given CDDP context. - * @param context Reference to the CDDP instance containing problem data and - * options. - */ - void initialize(CDDP &context) override; - - /** - * @brief Execute the ALDDP algorithm and return the solution. - * @param context Reference to the CDDP instance containing problem data and - * options. - * @return CDDPSolution containing the results. - */ - CDDPSolution solve(CDDP &context) override; - - /** - * @brief Get the name of the solver algorithm. - * @return String identifier "ALDDP". - */ - std::string getSolverName() const override; - -private: - // Control law parameters - std::vector k_u_; ///< Feedforward control gains - std::vector K_u_; ///< Feedback control gains - - // Dynamics storage - std::vector F_; ///< Dynamics evaluations - - // ALDDP-specific variables (constraint name -> time trajectory) - std::map> - Y_; ///< Dual variables (Lagrange multipliers) - std::vector - Lambda_; ///< Lagrange multipliers for defect constraints - - // Penalty parameters (simplified for ALDDP) - double - rho_defect_; ///< Defect constraint penalty parameter (scalar, constant) - std::map - rho_path_; ///< Path constraint penalty parameters (constraint name -> - ///< scalar) - - double cost_; ///< Current total cost - double constraint_violation_; ///< Current constraint violation measure - double - lagrangian_value_; ///< Augmented Lagrangian value (cost + penalty terms) - double optimality_gap_; ///< Norm of the gradient of the Lagrangian - - /** - * @brief Evaluate the trajectory, computing cost, dynamics, and augmented - * lagrangian. - * @param context Reference to the CDDP context. - */ - void evaluateTrajectory(CDDP &context); - - /** - * @brief Perform the backward pass (Riccati recursion with augmented - * Lagrangian terms). - * @param context Reference to the CDDP context. - * @return True if the backward pass succeeds, false otherwise. - */ - bool backwardPass(CDDP &context); - - /** - * @brief Perform the forward pass with line search (single-shooting only). - * @param context Reference to the CDDP context. - * @return The result of the best forward pass. - */ - ForwardPassResult performForwardPass(CDDP &context); - - /** - * @brief Perform a single forward pass with a given step size alpha - * (single-shooting). - * @param context Reference to the CDDP context. - * @param alpha The step size for the forward pass. - * @return The result of the forward pass. - */ - ForwardPassResult forwardPass(CDDP &context, double alpha); - - /** - * @brief Update augmented Lagrangian parameters (simplified ALDDP approach). - * @param context Reference to the CDDP context. - */ - void updateAugmentedLagrangian(CDDP &context); - - /** - * @brief Print iteration information to the console. - */ - void printIteration(int iter, double cost, double lagrangian, - double grad_norm, double regularization, double alpha, - double mu, double constraint_violation) const; - - /** - * @brief Print a summary of the final solution. - * @param solution The solution to print. - */ - void printSolutionSummary(const CDDPSolution &solution) const; -}; - -} // namespace cddp - -#endif // CDDP_ALDDP_SOLVER_HPP \ No newline at end of file diff --git a/include/cddp-cpp/cddp_core/cddp_core.hpp b/include/cddp-cpp/cddp_core/cddp_core.hpp index 58b37df6..1f775b81 100644 --- a/include/cddp-cpp/cddp_core/cddp_core.hpp +++ b/include/cddp-cpp/cddp_core/cddp_core.hpp @@ -46,8 +46,7 @@ enum class SolverType { CLDDP, ///< Control-Limited Differential Dynamic Programming LogDDP, ///< Log-Barrier Differential Dynamic Programming IPDDP, ///< Interior Point Differential Dynamic Programming - MSIPDDP, ///< Multi-Shooting Interior Point Differential Dynamic Programming - ALDDP ///< Augmented Lagrangian Differential Dynamic Programming + MSIPDDP ///< Multi-Shooting Interior Point Differential Dynamic Programming }; /** diff --git a/include/cddp-cpp/cddp_core/dbas_ddp_solver.hpp b/include/cddp-cpp/cddp_core/dbas_ddp_solver.hpp deleted file mode 100644 index 5caae34d..00000000 --- a/include/cddp-cpp/cddp_core/dbas_ddp_solver.hpp +++ /dev/null @@ -1,289 +0,0 @@ -/* - Copyright 2024 Tomo Sasaki - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#ifndef CDDP_DBAS_DDP_SOLVER_HPP -#define CDDP_DBAS_DDP_SOLVER_HPP - -#include "cddp_core/cddp_core.hpp" -#include "cddp_core/barrier.hpp" -#include -#include -#include -#include - -namespace cddp -{ - - /** - * @brief Discrete Barrier State DDP (DBAS-DDP) solver implementation. - * - * This class implements the ISolverAlgorithm interface to provide - * a discrete barrier state based DDP solver for handling inequality constraints - * with explicit barrier state augmentation and discrete barrier transitions. - * - * Note: DBAS-DDP is incompatible with multi-shooting optimization because: - * - Barrier states require continuous evolution throughout the entire trajectory - * - State augmentation needs to maintain temporal relationships across the full horizon - * - Barrier states carry memory of past constraint violations that would be lost in segmented approaches - * - * Note: DBAS-DDP requires feasible initial guesses because barrier states are part of the - * augmented state space and need proper initialization. Therefore, conventional line search - * (focusing on cost reduction) is used instead of filter-based line search. - */ - class DbasDdpSolver : public ISolverAlgorithm - { - public: - /** - * @brief Default constructor. - */ - DbasDdpSolver(); - - /** - * @brief Initialize the solver with the given CDDP context. - * @param context Reference to the CDDP instance containing problem data and options. - */ - void initialize(CDDP &context) override; - - /** - * @brief Execute the DBAS-DDP algorithm and return the solution. - * @param context Reference to the CDDP instance containing problem data and options. - * @return CDDPSolution containing the results. - */ - CDDPSolution solve(CDDP &context) override; - - /** - * @brief Get the name of the solver algorithm. - * @return String identifier "DBAS-DDP". - */ - std::string getSolverName() const override; - - private: - // Augmented state dimensions including barrier states - int augmented_state_dim_; ///< Total augmented state dimension (original + barrier states) - int original_state_dim_; ///< Original state dimension - int barrier_state_dim_; ///< Number of barrier state variables - - // Dynamics storage for augmented system - std::vector F_aug_; ///< Augmented dynamics evaluations - std::vector F_x_aug_; ///< Augmented state jacobians - std::vector F_u_aug_; ///< Augmented control jacobians - std::vector> F_xx_aug_; ///< Augmented state hessians - std::vector> F_uu_aug_; ///< Augmented control hessians - std::vector> F_ux_aug_; ///< Augmented mixed hessians - - // Augmented trajectories - std::vector X_aug_; ///< Augmented state trajectory - std::vector barrier_states_; ///< Barrier state trajectory - - // Control law parameters for augmented system - std::vector k_u_; ///< Feedforward control gains - std::vector K_u_; ///< Feedback control gains - Eigen::Vector2d dV_; ///< Expected value function change - - // Discrete barrier state management - std::map> barrier_state_values_; ///< Barrier state values for each constraint - std::map> G_; ///< Constraint values - std::map> discrete_barrier_managers_; ///< Barrier state managers for each constraint - std::unique_ptr relaxed_log_barrier_; ///< Log barrier object - double mu_; ///< Barrier parameter - double relaxation_delta_; ///< Relaxation parameter - - // Discrete barrier state parameters (loaded from options) - double barrier_state_weight_; ///< Weight for barrier state dynamics penalty - double barrier_state_init_value_; ///< Initial value for barrier states - double barrier_state_decay_rate_; ///< Decay rate for barrier state updates - bool use_adaptive_barrier_weights_; ///< Flag for adaptive barrier state weighting - double max_barrier_state_norm_; ///< Maximum allowed barrier state norm - double barrier_state_regularization_; ///< Regularization for barrier state dynamics - - // Conventional line search (no filter needed since we require feasible initialization) - double constraint_violation_; ///< Current constraint violation measure - double previous_barrier_state_norm_; ///< Previous iteration barrier state norm for convergence checking - - // === Initialization and Validation Methods === - - /** - * @brief Validate DBAS-DDP options and throw if invalid. - * @param options DBAS-DDP algorithm options to validate. - */ - void validateOptions(const DbasDdpAlgorithmOptions &options) const; - - /** - * @brief Initialize augmented state space including barrier states. - * @param context Reference to the CDDP context. - */ - void initializeAugmentedStateSpace(CDDP &context); - - // === Barrier State Management Methods === - - /** - * @brief Update barrier states based on current constraint violations. - * @param context Reference to the CDDP context. - * @param time_step Current time step. - */ - void updateBarrierStates(CDDP &context, int time_step); - - // === Augmented Dynamics Methods === - - /** - * @brief Construct augmented dynamics including barrier state dynamics. - * @param context Reference to the CDDP context. - * @param time_step Current time step. - * @param x_orig Original state. - * @param u Control input. - * @param barrier_state Current barrier state. - * @return Augmented dynamics evaluation. - */ - Eigen::VectorXd evaluateAugmentedDynamics(CDDP &context, int time_step, - const Eigen::VectorXd &x_orig, - const Eigen::VectorXd &u, - const Eigen::VectorXd &barrier_state); - - /** - * @brief Compute augmented system jacobians. - * @param context Reference to the CDDP context. - * @param time_step Current time step. - * @param x_orig Original state. - * @param u Control input. - * @param barrier_state Current barrier state. - * @return Tuple of (F_x_aug, F_u_aug). - */ - std::tuple computeAugmentedJacobians( - CDDP &context, int time_step, - const Eigen::VectorXd &x_orig, - const Eigen::VectorXd &u, - const Eigen::VectorXd &barrier_state); - - /** - * @brief Pre-compute augmented dynamics jacobians and hessians for all time steps. - * @param context Reference to the CDDP context. - */ - void precomputeAugmentedDynamicsDerivatives(CDDP &context); - - // === Cost and Trajectory Evaluation Methods === - - /** - * @brief Evaluate augmented trajectory including barrier states. - * @param context Reference to the CDDP context. - */ - void evaluateAugmentedTrajectory(CDDP &context); - - /** - * @brief Compute cost including barrier state penalties. - * @param context Reference to the CDDP context. - */ - void computeCost(CDDP &context); - - // === Core DDP Algorithm Methods === - - /** - * @brief Perform backward pass (Riccati recursion) for augmented system. - * @param context Reference to the CDDP context. - * @return True if backward pass succeeds, false otherwise. - */ - bool backwardPassAugmented(CDDP &context); - - /** - * @brief Perform forward pass with conventional line search for augmented system. - * @param context Reference to the CDDP context. - * @return Best forward pass result. - */ - ForwardPassResult performAugmentedForwardPass(CDDP &context); - - /** - * @brief Perform single forward pass with given step size for augmented system. - * @param context Reference to the CDDP context. - * @param alpha Step size for the forward pass. - * @return Forward pass result. - */ - ForwardPassResult forwardPassAugmented(CDDP &context, double alpha); - - // === Parameter Update Methods === - - /** - * @brief Update barrier parameters and barrier state weights. - * @param context Reference to the CDDP context. - * @param forward_pass_success Whether the forward pass was successful. - * @param termination_metric Current termination metric. - */ - void updateBarrierParameters(CDDP &context, bool forward_pass_success, double termination_metric); - - // === State Space Conversion Methods === - - /** - * @brief Extract original state trajectory from augmented trajectory. - * @param X_aug Augmented state trajectory. - * @return Original state trajectory. - */ - std::vector extractOriginalTrajectory(const std::vector &X_aug); - - /** - * @brief Extract barrier state trajectory from augmented trajectory. - * @param X_aug Augmented state trajectory. - * @return Barrier state trajectory. - */ - std::vector extractBarrierStateTrajectory(const std::vector &X_aug); - - /** - * @brief Combine original and barrier states into augmented state. - * @param x_orig Original state. - * @param barrier_state Barrier state. - * @return Augmented state vector. - */ - Eigen::VectorXd combineStates(const Eigen::VectorXd &x_orig, const Eigen::VectorXd &barrier_state); - - // === Convergence and Health Checking Methods === - - /** - * @brief Check if barrier states have converged. - * @param context Reference to the CDDP context. - * @return True if barrier states have converged within tolerance. - */ - bool checkBarrierStateConvergence(CDDP &context) const; - - /** - * @brief Compute total barrier state norm across all constraints. - * @param context Reference to the CDDP context. - * @return Total norm of all barrier states. - */ - double computeBarrierStateNorm(CDDP &context) const; - - /** - * @brief Check if barrier states are numerically healthy. - * @param context Reference to the CDDP context. - * @return True if barrier states are within acceptable bounds. - */ - bool checkBarrierStateHealth(CDDP &context) const; - - // === Utility and Display Methods === - - /** - * @brief Print iteration information. - */ - void printIteration(int iter, double cost, double merit_function, double inf_du, - double regularization, double alpha, double mu, double constraint_violation, - double barrier_state_norm) const; - - /** - * @brief Print solution summary. - * @param solution The solution to print. - */ - void printSolutionSummary(const CDDPSolution &solution) const; - }; - -} // namespace cddp - -#endif // CDDP_DBAS_DDP_SOLVER_HPP \ No newline at end of file diff --git a/include/cddp-cpp/cddp_core/options.hpp b/include/cddp-cpp/cddp_core/options.hpp index 1cb85432..3e6ff6e1 100644 --- a/include/cddp-cpp/cddp_core/options.hpp +++ b/include/cddp-cpp/cddp_core/options.hpp @@ -195,150 +195,6 @@ namespace cddp SolverSpecificBarrierOptions barrier; ///< Barrier method parameters }; - /** - * @brief Comprehensive options specifically for the ALTRO (Augmented Lagrangian - * Trajectory Optimizer) algorithm. ALTRO uses augmented Lagrangian methods to - * handle constraints through penalty terms and dual variable updates. - */ - struct AltroAlgorithmOptions - { - // Penalty parameters - double penalty_scaling = 10.0; ///< Initial penalty scaling parameter (rho) - ///< for augmented Lagrangian. - double penalty_scaling_increase_factor = - 10.0; ///< Factor to increase penalty when constraints are violated. - double penalty_scaling_max = - 1e8; ///< Maximum allowed penalty scaling parameter. - double penalty_scaling_min = - 1e-6; ///< Minimum allowed penalty scaling parameter. - - // Dual variable parameters - double dual_var_init_scale = - 0.1; ///< Initial scale for dual variables (Lagrange multipliers). - double dual_var_max = 1e6; ///< Maximum allowed magnitude for dual variables. - double dual_var_min = -1e6; ///< Minimum allowed magnitude for dual variables. - double dual_update_factor = 1.0; ///< Factor for dual variable updates. - - // Defect constraint parameters - double defect_dual_init_scale = - 0.01; ///< Initial scale for defect constraint dual variables. - double defect_penalty_scaling = - 1.0; ///< Penalty scaling for dynamics defect constraints. - - // Convergence parameters - double constraint_tolerance = 1e-6; ///< Tolerance for constraint violation. - double dual_feasibility_tolerance = 1e-6; ///< Tolerance for dual feasibility. - double complementarity_tolerance = - 1e-6; ///< Tolerance for complementarity conditions. - - // Penalty update strategy - bool adaptive_penalty_update = - true; ///< Use adaptive penalty parameter updates. - double penalty_update_threshold = - 0.25; ///< Threshold for penalty parameter updates (relative to constraint - ///< violation reduction). - int max_penalty_increases = - 5; ///< Maximum number of consecutive penalty increases per iteration. - - // AL-specific convergence criteria - double al_convergence_tolerance = - 1e-4; ///< Convergence tolerance for augmented Lagrangian subproblems. - int max_al_iterations = - 1; ///< Maximum iterations for augmented Lagrangian outer loop. - bool use_constraint_norm_termination = - true; ///< Use constraint norm for termination criteria. - - // Multiple-shooting parameters - bool use_multiple_shooting = - false; ///< Enable multiple-shooting approach for forward pass. - int segment_length = - 5; ///< Number of shooting intervals before a gap-closing constraint. - std::string rollout_type = - "nonlinear"; ///< Rollout type: "nonlinear", "hybrid". - }; - - /** - * @brief Comprehensive options specifically for the DBAS-DDP (Discrete Barrier - * State DDP) algorithm. DBAS-DDP augments the state space with explicit barrier - * states to handle inequality constraints. - * - * Key Design Principles: - * - Barrier states evolve as: s_{k+1} = decay_factor * s_k + weight * violation - * * dt - * - Cost includes: original_cost + barrier_log_terms + barrier_state_penalty - * - Requires feasible initial guess since state space is augmented - */ - struct DbasDdpAlgorithmOptions - { - // Barrier state initialization parameters - double barrier_state_init_value = - 0.1; ///< Initial value for barrier state variables (smaller values - ///< encourage faster constraint satisfaction). - double barrier_state_weight = - 10.0; ///< Weight for barrier state dynamics penalty in the cost function - ///< (higher values respond faster to violations). - double barrier_state_decay_rate = - 0.05; ///< Decay rate for barrier state evolution dynamics (smaller values - ///< provide longer memory). - - // Adaptive parameters - bool use_adaptive_barrier_weights = - true; ///< Enable adaptive adjustment of barrier state weights based on - ///< constraint satisfaction. - double barrier_weight_increase_factor = - 1.5; ///< Factor to increase barrier weights when constraints are - ///< consistently violated. - double barrier_weight_decrease_factor = - 0.9; ///< Factor to decrease barrier weights when making consistent - ///< progress. - double barrier_weight_max = - 1e4; ///< Maximum allowed barrier weight (prevents numerical issues). - double barrier_weight_min = 1e-2; ///< Minimum allowed barrier weight - ///< (maintains constraint awareness). - - // Log-barrier integration parameters - double mu_initial = 1e-2; ///< Initial barrier coefficient for log-barrier - ///< terms (smaller values are more aggressive). - double mu_min_value = - 1e-6; ///< Minimum allowed value for barrier coefficient. - double mu_update_factor = - 0.2; ///< Factor to reduce barrier coefficient (smaller values reduce mu - ///< more aggressively). - double relaxed_log_barrier_delta = - 1e-4; ///< Relaxation delta for relaxed log-barrier method (balance - ///< between accuracy and robustness). - - // Constraint violation handling - double constraint_violation_tolerance = - 1e-6; ///< Tolerance for constraint violations in barrier state updates. - double barrier_state_convergence_tol = - 1e-4; ///< Convergence tolerance for barrier state changes (when to - ///< consider barrier states converged). - double max_barrier_state_norm = 100.0; ///< Maximum allowed norm for barrier - ///< states (prevents runaway growth). - - // Cost function parameters - bool penalize_barrier_state_deviation = - true; ///< Penalize deviation of barrier states from zero in the cost - ///< function. - double barrier_state_reference_weight = - 1.0; ///< Weight for barrier state reference tracking cost (drives barrier - ///< states toward zero). - - // Numerical stability parameters - double min_barrier_state_value = - 1e-8; ///< Minimum allowed value for barrier state variables (prevents - ///< numerical issues). - double max_barrier_state_value = - 1e6; ///< Maximum allowed value for barrier state variables (prevents - ///< overflow). - bool enable_barrier_state_regularization = - true; ///< Add small regularization to barrier state dynamics for - ///< numerical stability. - double barrier_state_regularization = - 1e-6; ///< Regularization value for barrier state dynamics. - }; - /** * @brief Main options structure for the CDDP solver. * @@ -385,9 +241,6 @@ namespace cddp IPDDPAlgorithmOptions ipddp; ///< Comprehensive options for the IPDDP solver. MSIPDDPAlgorithmOptions msipddp; ///< Comprehensive options for the MSIPDDP solver. - AltroAlgorithmOptions altro; ///< Comprehensive options for the ALTRO solver. - DbasDdpAlgorithmOptions - dbas_ddp; ///< Comprehensive options for the DBAS-DDP solver. // Constructor with defaults (relies on member initializers) CDDPOptions() = default; diff --git a/src/cddp_core/alddp_solver.cpp b/src/cddp_core/alddp_solver.cpp deleted file mode 100644 index c434fced..00000000 --- a/src/cddp_core/alddp_solver.cpp +++ /dev/null @@ -1,911 +0,0 @@ -/* - * Copyright 2024 Tomo Sasaki - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "cddp_core/alddp_solver.hpp" -#include -#include -#include -#include -#include -#include - -namespace cddp { - -AlddpSolver::AlddpSolver() - : cost_(0.0), constraint_violation_(0.0), lagrangian_value_(0.0), - optimality_gap_(0.0) {} - -void AlddpSolver::initialize(CDDP &context) { - const CDDPOptions &options = context.getOptions(); - const int state_dim = context.getStateDim(); - const int control_dim = context.getControlDim(); - const int horizon = context.getHorizon(); - - // For warm starts, verify that existing state is valid - if (options.warm_start) { - bool valid_warm_start = (k_u_.size() == static_cast(horizon) && - K_u_.size() == static_cast(horizon) && - Lambda_.size() == static_cast(horizon)); - - if (valid_warm_start && !k_u_.empty()) { - for (int t = 0; t < horizon; ++t) { - if (k_u_[t].size() != control_dim || K_u_[t].rows() != control_dim || - K_u_[t].cols() != state_dim || Lambda_[t].size() != state_dim) { - valid_warm_start = false; - break; - } - } - } else { - valid_warm_start = false; - } - - // Check dual variables validity for warm start - if (valid_warm_start) { - const auto &constraint_set = context.getConstraintSet(); - for (const auto &constraint_pair : constraint_set) { - const std::string &constraint_name = constraint_pair.first; - int dual_dim = constraint_pair.second->getDualDim(); - - if (Y_.find(constraint_name) == Y_.end() || - Y_[constraint_name].size() != static_cast(horizon)) { - valid_warm_start = false; - break; - } - - for (int t = 0; t < horizon; ++t) { - if (Y_[constraint_name][t].size() != dual_dim) { - valid_warm_start = false; - break; - } - } - if (!valid_warm_start) - break; - } - } - - if (valid_warm_start) { - if (options.verbose) { - std::cout << "ALDDP: Using warm start with existing control gains, " - "dual variables, and defect multipliers" - << std::endl; - } - // Initialize dynamics storage for warm start - F_.resize(horizon, Eigen::VectorXd::Zero(state_dim)); - evaluateTrajectory(context); - return; - } else if (options.verbose) { - std::cout << "ALDDP: Warning - warm start requested but no valid solver " - "state found. " - << "Falling back to cold start initialization." << std::endl; - } - } - - // Cold start: full initialization - k_u_.resize(horizon); - K_u_.resize(horizon); - - for (int t = 0; t < horizon; ++t) { - k_u_[t] = Eigen::VectorXd::Zero(control_dim); - K_u_[t] = Eigen::MatrixXd::Zero(control_dim, state_dim); - } - - // Initialize dynamics storage - F_.resize(horizon, Eigen::VectorXd::Zero(state_dim)); - - // Initialize Lagrange multipliers for defect constraints - Lambda_.resize(horizon); - for (int t = 0; t < horizon; ++t) { - Lambda_[t] = Eigen::VectorXd::Constant( - state_dim, options.altro.defect_dual_init_scale); - } - - // Initialize dual variables for constraints - Y_.clear(); - const auto &constraint_set = context.getConstraintSet(); - - for (const auto &constraint_pair : constraint_set) { - const std::string &constraint_name = constraint_pair.first; - const auto &constraint = constraint_pair.second; - int dual_dim = constraint->getDualDim(); - - Y_[constraint_name].resize(horizon, Eigen::VectorXd::Zero(dual_dim)); - - // Initialize dual variables to small positive values - for (int t = 0; t < horizon; ++t) { - Y_[constraint_name][t] = Eigen::VectorXd::Constant( - dual_dim, options.altro.dual_var_init_scale); - } - - // Initialize path constraint penalty parameter (scalar for ALDDP) - rho_path_[constraint_name] = options.altro.penalty_scaling; - } - - // Initialize defect constraint penalty parameter (scalar for ALDDP) - rho_defect_ = options.altro.defect_penalty_scaling; - - // Initialize regularization - context.regularization_ = options.regularization.initial_value; - - // ALDDP uses single-shooting only (simplified approach) - if (options.verbose) { - std::cout << "ALDDP: Single-shooting mode (standard dynamics propagation)" - << std::endl; - } -} - -std::string AlddpSolver::getSolverName() const { return "ALDDP"; } - -CDDPSolution AlddpSolver::solve(CDDP &context) { - const CDDPOptions &options = context.getOptions(); - - // Print solver header if requested - if (options.print_solver_header) { - context.printSolverInfo(); - } - - // Print solver options if requested - if (options.print_solver_options) { - context.printOptions(options); - } - - // Prepare solution map - CDDPSolution solution; - solution["solver_name"] = getSolverName(); - solution["status_message"] = std::string("Running"); - solution["iterations_completed"] = 0; - solution["solve_time_ms"] = 0.0; - - // Initialize history vectors only if requested - std::vector history_objective; - std::vector history_lagrangian; - std::vector history_step_length_primal; - std::vector history_dual_infeasibility; - std::vector history_primal_infeasibility; - std::vector history_penalty_parameter; - - // Initial trajectory evaluation - evaluateTrajectory(context); - - if (options.return_iteration_info) { - const size_t expected_size = - static_cast(options.max_iterations + 1); - history_objective.reserve(expected_size); - history_lagrangian.reserve(expected_size); - history_step_length_primal.reserve(expected_size); - history_dual_infeasibility.reserve(expected_size); - history_primal_infeasibility.reserve(expected_size); - history_penalty_parameter.reserve(expected_size); - - // Initial iteration values - history_objective.push_back(cost_); - history_lagrangian.push_back(lagrangian_value_); - history_step_length_primal.push_back(1.0); - history_dual_infeasibility.push_back(optimality_gap_); - history_primal_infeasibility.push_back(constraint_violation_); - history_penalty_parameter.push_back(options.altro.penalty_scaling); - } - - // Start timer - auto start_time = std::chrono::high_resolution_clock::now(); - int iter = 0; - bool converged = false; - std::string termination_reason = "MaxIterationsReached"; - - if (options.verbose) { - printIteration(0, cost_, lagrangian_value_, 0.0, context.regularization_, - 1.0, options.altro.penalty_scaling, constraint_violation_); - } - - // Main ALTRO loop - while (iter < options.max_iterations) { - ++iter; - - // Check maximum CPU time - if (options.max_cpu_time > 0) { - auto current_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast( - current_time - start_time); - if (duration.count() > options.max_cpu_time * 1000) { - termination_reason = "MaxCpuTimeReached"; - if (options.verbose) { - std::cerr - << "ALDDP: Maximum CPU time reached. Returning current solution" - << std::endl; - } - break; - } - } - - // 1. Backward pass - bool backward_pass_success = false; - while (!backward_pass_success) { - backward_pass_success = backwardPass(context); - - if (!backward_pass_success) { - context.increaseRegularization(); - if (context.isRegularizationLimitReached()) { - termination_reason = "RegularizationLimitReached_NotConverged"; - if (options.verbose) { - std::cerr << "ALDDP: Backward pass regularization limit reached" - << std::endl; - } - break; - } - } - } - - if (!backward_pass_success) - break; - - // 2. Forward pass - ForwardPassResult best_result = performForwardPass(context); - - // Update solution if forward pass succeeded - if (best_result.success) { - if (options.debug) { - std::cout << "[ALDDP: Forward pass] " << std::endl; - std::cout << " cost: " << best_result.cost << std::endl; - std::cout << " merit_function: " << best_result.merit_function - << std::endl; - std::cout << " alpha: " << best_result.alpha_pr << std::endl; - std::cout << " cv_err: " << best_result.constraint_violation - << std::endl; - } - - context.X_ = best_result.state_trajectory; - context.U_ = best_result.control_trajectory; - if (best_result.dynamics_trajectory) { - F_ = *best_result.dynamics_trajectory; - } - - double dJ = cost_ - best_result.cost; - double dL = lagrangian_value_ - best_result.merit_function; - cost_ = best_result.cost; - lagrangian_value_ = best_result.merit_function; - context.alpha_pr_ = best_result.alpha_pr; - constraint_violation_ = best_result.constraint_violation; - - // Store history only if requested - if (options.return_iteration_info) { - history_objective.push_back(cost_); - history_lagrangian.push_back(lagrangian_value_); - history_step_length_primal.push_back(context.alpha_pr_); - history_dual_infeasibility.push_back(optimality_gap_); - history_primal_infeasibility.push_back(constraint_violation_); - history_penalty_parameter.push_back(options.altro.penalty_scaling); - } - - context.decreaseRegularization(); - - // Check convergence using proper gradient norm (optimality_gap_ is set by - // backwardPass) Note: optimality_gap_ contains the gradient norm (Qu_err) - // from backward pass - double cost_change = std::abs(dJ); - double lagrangian_change = std::abs(dL); - - if (optimality_gap_ <= options.tolerance && - constraint_violation_ <= options.altro.constraint_tolerance) { - converged = true; - termination_reason = "OptimalSolutionFound"; - break; - } - - if (cost_change < options.acceptable_tolerance && - constraint_violation_ <= options.altro.constraint_tolerance) { - converged = true; - termination_reason = "AcceptableSolutionFound"; - break; - } - } else { - context.increaseRegularization(); - - if (context.isRegularizationLimitReached()) { - termination_reason = "RegularizationLimitReached_NotConverged"; - converged = false; - if (options.verbose) { - std::cerr << "ALDDP: Regularization limit reached. Not converged." - << std::endl; - } - break; - } - } - - // Print iteration info - if (options.verbose) { - printIteration(iter, cost_, lagrangian_value_, optimality_gap_, - context.regularization_, context.alpha_pr_, - options.altro.penalty_scaling, constraint_violation_); - } - - // Update augmented Lagrangian parameters - updateAugmentedLagrangian(context); - } - - // Compute final timing - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast( - end_time - start_time); - - // Populate final solution - solution["status_message"] = termination_reason; - solution["iterations_completed"] = iter; - solution["solve_time_ms"] = static_cast(duration.count()); - solution["final_objective"] = cost_; - solution["final_step_length"] = context.alpha_pr_; - - // Add trajectories - std::vector time_points; - time_points.reserve(static_cast(context.getHorizon() + 1)); - for (int t = 0; t <= context.getHorizon(); ++t) { - time_points.push_back(t * context.getTimestep()); - } - solution["time_points"] = time_points; - solution["state_trajectory"] = context.X_; - solution["control_trajectory"] = context.U_; - - // Add iteration history if requested - if (options.return_iteration_info) { - solution["history_objective"] = history_objective; - solution["history_lagrangian"] = history_lagrangian; - solution["history_step_length_primal"] = history_step_length_primal; - solution["history_dual_infeasibility"] = history_dual_infeasibility; - solution["history_primal_infeasibility"] = history_primal_infeasibility; - solution["history_penalty_parameter"] = history_penalty_parameter; - } - - // Add control gains - solution["control_feedback_gains_K"] = K_u_; - - // Final metrics - solution["final_regularization"] = context.regularization_; - solution["final_penalty_parameter"] = options.altro.penalty_scaling; - solution["final_primal_infeasibility"] = constraint_violation_; - solution["final_dual_infeasibility"] = optimality_gap_; - solution["final_lagrangian"] = lagrangian_value_; - - if (options.verbose) { - printSolutionSummary(solution); - } - - - return solution; -} - -void AlddpSolver::evaluateTrajectory(CDDP &context) { - const auto &X = context.X_; - const auto &U = context.U_; - const auto &objective = context.getObjective(); - const auto &system = context.getSystem(); - const auto &constraint_set = context.getConstraintSet(); - const int horizon = context.getHorizon(); - const double timestep = context.getTimestep(); - const double penalty_scaling = context.getOptions().altro.penalty_scaling; - const double defect_penalty_scaling = - context.getOptions().altro.defect_penalty_scaling; - - // Compute cost - cost_ = 0.0; - for (int t = 0; t < horizon; ++t) { - cost_ += objective.running_cost(X[t], U[t], t); - - // Store dynamics - F_[t] = system.getDiscreteDynamics(X[t], U[t], t * timestep); - } - cost_ += objective.terminal_cost(X.back()); - - // Compute constraint violation and augmented Lagrangian terms - constraint_violation_ = 0.0; - double penalty_cost = 0.0; - - // Add defect constraint Lagrangian terms - for (int t = 0; t < horizon; ++t) { - // Compute defect: d_t = x_{t+1} - f(x_t, u_t) - Eigen::VectorXd defect = X[t + 1] - F_[t]; - - // Add Lagrangian term: λ_t^T * d_t - penalty_cost += Lambda_[t].dot(defect); - - // Add quadratic penalty term: 0.5 * ρ_defect * ||d_t||^2 (simplified for - // ALDDP) - penalty_cost += 0.5 * rho_defect_ * defect.squaredNorm(); - - // Update constraint violation with defect norm - constraint_violation_ += defect.norm(); - } - - // Add path constraint terms - for (const auto &constraint_pair : constraint_set) { - const std::string &constraint_name = constraint_pair.first; - const auto &constraint = constraint_pair.second; - - for (int t = 0; t < horizon; ++t) { - // Evaluate constraint - Eigen::VectorXd g = - constraint->evaluate(X[t], U[t]) - constraint->getUpperBound(); - const Eigen::VectorXd &y = Y_[constraint_name][t]; - const double rho_path = rho_path_[constraint_name]; - - // Update constraint violation - constraint_violation_ += std::max(0.0, g.maxCoeff()); - - // Simplified augmented Lagrangian terms for ALDDP - for (int i = 0; i < g.size(); ++i) { - const double constraint_tolerance = - 1e-12; // Small tolerance for numerical stability - - if (g(i) > constraint_tolerance) { - // Active constraint: add linear and quadratic penalty terms - penalty_cost += y(i) * g(i) + 0.5 * rho_path * g(i) * g(i); - } else { - // Inactive constraint: only quadratic penalty if multiplier is large - double projected_multiplier = std::max(0.0, y(i) + rho_path * g(i)); - if (projected_multiplier > constraint_tolerance) { - penalty_cost += 0.5 * rho_path * g(i) * g(i); - } - } - } - } - } - - lagrangian_value_ = cost_ + penalty_cost; -} - -bool AlddpSolver::backwardPass(CDDP &context) { - const auto &options = context.getOptions(); - - const auto &X = context.X_; - const auto &U = context.U_; - const auto &objective = context.getObjective(); - const auto &system = context.getSystem(); - const auto &constraint_set = context.getConstraintSet(); - const int horizon = context.getHorizon(); - const int state_dim = context.getStateDim(); - const int control_dim = context.getControlDim(); - const double timestep = context.getTimestep(); - const double penalty_scaling = context.getOptions().altro.penalty_scaling; - const double defect_penalty_scaling = - context.getOptions().altro.defect_penalty_scaling; - const bool is_ilqr = context.getOptions().use_ilqr; - - double Qu_err = 0.0; - - // Terminal cost derivatives - Eigen::VectorXd V_x = objective.getFinalCostGradient(X.back()); - Eigen::MatrixXd V_xx = objective.getFinalCostHessian(X.back()); - V_xx = 0.5 * (V_xx + V_xx.transpose()); - - // Backward recursion - for (int t = horizon - 1; t >= 0; --t) { - const Eigen::VectorXd &x = X[t]; - const Eigen::VectorXd &u = U[t]; - const Eigen::VectorXd &f = F_[t]; - const Eigen::VectorXd &d = f - context.X_[t + 1]; // Defect - const Eigen::VectorXd &lambda = Lambda_[t]; - - // Get dynamics derivatives - const auto [Fx, Fu] = system.getJacobians(x, u, t * timestep); - Eigen::MatrixXd A = - Eigen::MatrixXd::Identity(state_dim, state_dim) + timestep * Fx; - Eigen::MatrixXd B = timestep * Fu; - - // Cost derivatives at (x_t, u_t) - auto [l_x, l_u] = objective.getRunningCostGradients(x, u, t); - auto [l_xx, l_uu, l_ux] = objective.getRunningCostHessians(x, u, t); - - // Initialize Q-function with cost and dynamics terms (simplified for ALDDP) - Eigen::VectorXd Q_x = - l_x + A.transpose() * V_x + A.transpose() * (lambda + rho_defect_ * d); - Eigen::VectorXd Q_u = - l_u + B.transpose() * V_x + B.transpose() * (lambda + rho_defect_ * d); - Eigen::MatrixXd Q_xx = - l_xx + A.transpose() * V_xx * A + rho_defect_ * A.transpose() * A; - Eigen::MatrixXd Q_ux = - l_ux + B.transpose() * V_xx * A + rho_defect_ * B.transpose() * A; - Eigen::MatrixXd Q_uu = - l_uu + B.transpose() * V_xx * B + rho_defect_ * B.transpose() * B; - - // Add path constraint terms to Q-function - for (const auto &constraint_pair : constraint_set) { - const std::string &constraint_name = constraint_pair.first; - const auto &constraint = constraint_pair.second; - const Eigen::VectorXd &y = Y_[constraint_name][t]; - const double rho_path = rho_path_[constraint_name]; - - // Evaluate constraint and its derivatives - Eigen::VectorXd g = - constraint->evaluate(x, u) - constraint->getUpperBound(); - Eigen::MatrixXd g_x = constraint->getStateJacobian(x, u); - Eigen::MatrixXd g_u = constraint->getControlJacobian(x, u); - - for (int i = 0; i < g.size(); ++i) { - const double constraint_tolerance = - 1e-12; // Small tolerance for numerical stability - - if (g(i) > constraint_tolerance || - (g(i) <= constraint_tolerance && - y(i) + rho_path * g(i) > constraint_tolerance)) { - // Add first-order terms to Q-function (simplified for ALDDP) - double weight = y(i) + rho_path * g(i); - Q_x += weight * g_x.row(i).transpose(); - Q_u += weight * g_u.row(i).transpose(); - - // Add second-order terms (Gauss-Newton approximation) - Q_xx += rho_path * g_x.row(i).transpose() * g_x.row(i); - Q_ux += rho_path * g_u.row(i).transpose() * g_x.row(i); - Q_uu += rho_path * g_u.row(i).transpose() * g_u.row(i); - } - } - } - - // Regularization - double reg = context.regularization_; - Eigen::MatrixXd Q_uu_reg = Q_uu; - Q_uu_reg.diagonal().array() += reg; - Q_uu_reg = 0.5 * (Q_uu_reg + Q_uu_reg.transpose()); - - // Solve for control law - Eigen::LDLT ldlt(Q_uu_reg); - if (ldlt.info() != Eigen::Success) { - if (options.debug) { - std::cerr << "ALDDP: Backward pass failed at time " << t << std::endl; - } - return false; - } - - Eigen::MatrixXd bigRHS(control_dim, 1 + state_dim); - bigRHS.col(0) = Q_u; - Eigen::MatrixXd M = Q_ux; - for (int col = 0; col < state_dim; col++) { - bigRHS.col(col + 1) = M.col(col); - } - - Eigen::MatrixXd kK = -ldlt.solve(bigRHS); - - // parse out feedforward (ku) and feedback (Ku) - Eigen::VectorXd k_u = kK.col(0); - Eigen::MatrixXd K_u(control_dim, state_dim); - for (int col = 0; col < state_dim; col++) { - K_u.col(col) = kK.col(col + 1); - } - - // Save gains - k_u_[t] = k_u; - K_u_[t] = K_u; - - // Update value function - V_x = Q_x + K_u_[t].transpose() * Q_u + Q_ux.transpose() * k_u_[t] + - K_u_[t].transpose() * Q_uu * k_u_[t]; - V_xx = Q_xx + K_u_[t].transpose() * Q_ux + Q_ux.transpose() * K_u_[t] + - K_u_[t].transpose() * Q_uu * K_u_[t]; - V_xx = 0.5 * (V_xx + V_xx.transpose()); - - // Compute optimality gap (Inf-norm) for convergence check - Qu_err = std::max(Qu_err, Q_u.lpNorm()); - } - - optimality_gap_ = Qu_err; - context.inf_du_ = optimality_gap_; - - if (options.debug) { - std::cout << "[ALDDP Backward Pass]\n" - << " Qu_err: " << std::scientific << std::setprecision(4) - << Qu_err << std::endl; - } - - return true; -} - -ForwardPassResult AlddpSolver::performForwardPass(CDDP &context) { - const auto &options = context.getOptions(); - - ForwardPassResult best_result; - best_result.success = false; - best_result.merit_function = std::numeric_limits::infinity(); - - // Try different step sizes from the context - for (double alpha : context.alphas_) { - ForwardPassResult result = forwardPass(context, alpha); - - if (result.success && result.merit_function < best_result.merit_function) { - best_result = result; - } - - // Early termination if we found a good step - if (result.success && result.merit_function < lagrangian_value_) { - break; - } - } - - return best_result; -} - -ForwardPassResult AlddpSolver::forwardPass(CDDP &context, double alpha) { - const auto &options = context.getOptions(); - - ForwardPassResult result; - result.success = false; - result.alpha_pr = alpha; - result.cost = std::numeric_limits::infinity(); - result.merit_function = std::numeric_limits::infinity(); - - const auto &X = context.X_; - const auto &U = context.U_; - const auto &system = context.getSystem(); - const auto &objective = context.getObjective(); - const auto &constraint_set = context.getConstraintSet(); - const int horizon = context.getHorizon(); - const int state_dim = context.getStateDim(); - const double timestep = context.getTimestep(); - - // Initialize new trajectories - std::vector X_new = X; - std::vector U_new = U; - std::vector F_new(horizon); - - // Set initial state - X_new[0] = context.getInitialState(); - - // Forward rollout with control law (with optional multiple-shooting) - for (int t = 0; t < horizon; ++t) { - Eigen::VectorXd dx = X_new[t] - X[t]; - - // Apply control law: u_new = u + α*k + K*dx - U_new[t] = U[t] + alpha * k_u_[t] + K_u_[t] * dx; - - // Check for numerical issues - if (!U_new[t].allFinite()) { - if (options.debug) { - std::cerr << "ALDDP: Forward pass - control NaN/Inf at time " << t - << std::endl; - } - return result; - } - - // Integrate dynamics - F_new[t] = system.getDiscreteDynamics(X_new[t], U_new[t], t * timestep); - - if (!F_new[t].allFinite()) { - if (options.debug) { - std::cerr << "ALDDP: Forward pass - dynamics NaN/Inf at time " << t - << std::endl; - } - return result; - } - - // ALDDP uses single-shooting only: direct dynamics propagation - X_new[t + 1] = F_new[t]; - - // Check for numerical issues - if (!X_new[t + 1].allFinite()) { - if (options.debug) { - std::cerr << "ALDDP: Forward pass - state NaN/Inf at time " << t - << std::endl; - } - return result; - } - } - - // Evaluate new trajectory - double cost_new = 0.0; - double constraint_violation_new = 0.0; - double penalty_cost = 0.0; - - // 1. Compute cost - for (int t = 0; t < horizon; ++t) { - cost_new += objective.running_cost(X_new[t], U_new[t], t); - } - cost_new += objective.terminal_cost(X_new.back()); - - // 2. Add defect constraint Lagrangian terms - for (int t = 0; t < horizon; ++t) { - // Compute defect for new trajectory: d_t = x_{t+1} - f(x_t, u_t) - Eigen::VectorXd defect_new = X_new[t + 1] - F_new[t]; - const Eigen::VectorXd &lambda = Lambda_[t]; - - // Lagrangian term: λ_t^T * defect_new - penalty_cost += lambda.dot(defect_new); - - // Quadratic penalty: 0.5 * ρ_defect * ||defect_new||^2 (simplified for - // ALDDP) - penalty_cost += 0.5 * rho_defect_ * defect_new.squaredNorm(); - - // Update constraint violation - constraint_violation_new += defect_new.norm(); - } - - // 3. Add path constraint Lagrangian terms - for (const auto &constraint_pair : constraint_set) { - const std::string &constraint_name = constraint_pair.first; - const auto &constraint = constraint_pair.second; - - for (int t = 0; t < horizon; ++t) { - Eigen::VectorXd g = constraint->evaluate(X_new[t], U_new[t]) - - constraint->getUpperBound(); - const Eigen::VectorXd &y = Y_[constraint_name][t]; - const double rho_path = rho_path_[constraint_name]; - - constraint_violation_new += std::max(0.0, g.maxCoeff()); - - // Apply simplified augmented Lagrangian terms for ALDDP - for (int i = 0; i < g.size(); ++i) { - const double constraint_tolerance = - 1e-12; // Small tolerance for numerical stability - - if (g(i) > constraint_tolerance) { - // Active constraint: linear + quadratic penalty terms - penalty_cost += y(i) * g(i) + 0.5 * rho_path * g(i) * g(i); - } else { - // Inactive constraint: only quadratic penalty if projected multiplier - // is positive - double projected_multiplier = std::max(0.0, y(i) + rho_path * g(i)); - if (projected_multiplier > constraint_tolerance) { - penalty_cost += 0.5 * rho_path * g(i) * g(i); - } - } - } - } - } - - double merit_function_new = cost_new + penalty_cost; - - // Acceptance test: accept if merit function improves or constraint violation - // decreases - if (merit_function_new < lagrangian_value_ || - constraint_violation_new < constraint_violation_) { - result.success = true; - result.state_trajectory = X_new; - result.control_trajectory = U_new; - result.dynamics_trajectory = F_new; - result.cost = cost_new; - result.merit_function = merit_function_new; - result.constraint_violation = constraint_violation_new; - - if (options.debug) { - std::cout << "[ALDDP Forward Pass]\n" - << " alpha: " << std::fixed << std::setprecision(4) << alpha - << "\n" - << " cost: " << std::scientific << std::setprecision(4) - << cost_new << "\n" - << " merit: " << std::scientific << std::setprecision(4) - << merit_function_new << "\n" - << " cv_err: " << std::scientific << std::setprecision(4) - << constraint_violation_new << std::endl; - } - } - - return result; -} - -void AlddpSolver::updateAugmentedLagrangian(CDDP &context) { - const auto &options = context.getOptions(); - const auto &X = context.X_; - const auto &U = context.U_; - const auto &constraint_set = context.getConstraintSet(); - const int horizon = context.getHorizon(); - - double max_defect_violation = 0.0; - double max_path_violation = 0.0; - - // 1. Update defect constraint Lagrange multipliers (simplified for ALDDP) - for (int t = 0; t < horizon; ++t) { - // Compute defect: d_t = x_{t+1} - f(x_t, u_t) - Eigen::VectorXd defect = X[t + 1] - F_[t]; - - // Update multipliers: λ_new = λ_old + ρ_defect * defect (scalar penalty) - Lambda_[t] += rho_defect_ * defect; - - // Track maximum defect violation for debugging - max_defect_violation = std::max(max_defect_violation, defect.norm()); - } - - // 2. Update path constraint dual variables (Lagrange multipliers) - for (const auto &constraint_pair : constraint_set) { - const std::string &constraint_name = constraint_pair.first; - const auto &constraint = constraint_pair.second; - - for (int t = 0; t < horizon; ++t) { - Eigen::VectorXd g = - constraint->evaluate(X[t], U[t]) - constraint->getUpperBound(); - Eigen::VectorXd &y = Y_[constraint_name][t]; - const double rho_path = rho_path_[constraint_name]; - - // Update multipliers: y_new = max(0, y_old + rho_path * g) (simplified - // for ALDDP) - for (int i = 0; i < g.size(); ++i) { - y(i) = std::max(0.0, y(i) + rho_path * g(i)); - - // Track maximum path constraint violation - if (g(i) > 0.0) { - max_path_violation = std::max(max_path_violation, g(i)); - } - } - } - } - - if (options.debug) { - std::cout << "[ALDDP Multiplier Update]\n" - << " max_defect_viol: " << std::scientific - << std::setprecision(4) << max_defect_violation << "\n" - << " max_path_viol: " << std::scientific - << std::setprecision(4) << max_path_violation << std::endl; - } -} - -void AlddpSolver::printIteration(int iter, double cost, double lagrangian, - double grad_norm, double regularization, - double alpha, double mu, - double constraint_violation) const { - if (iter == 0) { - std::cout << std::setw(4) << "iter" << std::setw(12) << "cost" - << std::setw(12) << "lagrangian" << std::setw(12) << "grad_norm" - << std::setw(8) << "alpha" << std::setw(10) << "penalty" - << std::setw(12) << "viol" << std::endl; - std::cout << std::string(70, '-') << std::endl; - } - - std::cout << std::setw(4) << iter << std::setw(12) << std::scientific - << std::setprecision(3) << cost << std::setw(12) << std::scientific - << std::setprecision(3) << lagrangian << std::setw(12) - << std::scientific << std::setprecision(3) << grad_norm - << std::setw(8) << std::fixed << std::setprecision(2) << alpha - << std::setw(10) << std::scientific << std::setprecision(2) << mu - << std::setw(12) << std::scientific << std::setprecision(3) - << constraint_violation << std::endl; -} - -void AlddpSolver::printSolutionSummary(const CDDPSolution &solution) const { - std::cout << "\n=== ALDDP Solution Summary ===" << std::endl; - auto status_it = solution.find("status_message"); - auto iterations_it = solution.find("iterations_completed"); - auto solve_time_it = solution.find("solve_time_ms"); - auto final_cost_it = solution.find("final_objective"); - auto final_lagrangian_it = solution.find("final_lagrangian"); - auto final_alpha_it = solution.find("final_step_length"); - - std::cout << "Status: " - << (status_it != solution.end() - ? std::any_cast(status_it->second) - : "N/A") - << std::endl; - std::cout << "Iterations: " - << (iterations_it != solution.end() - ? std::any_cast(iterations_it->second) - : -1) - << std::endl; - std::cout << "Solve time: " - << (solve_time_it != solution.end() - ? std::any_cast(solve_time_it->second) * 1e-3 - : -1.0) - << " seconds" << std::endl; - std::cout << "Final cost: " - << (final_cost_it != solution.end() - ? std::any_cast(final_cost_it->second) - : -1.0) - << std::endl; - std::cout << "Final lagrangian: " - << (final_lagrangian_it != solution.end() - ? std::any_cast(final_lagrangian_it->second) - : -1.0) - << std::endl; - std::cout << "Final step size: " - << (final_alpha_it != solution.end() - ? std::any_cast(final_alpha_it->second) - : -1.0) - << std::endl; - std::cout << "================================\n" << std::endl; -} - -} // namespace cddp diff --git a/src/cddp_core/cddp_core.cpp b/src/cddp_core/cddp_core.cpp index 403a50df..c2ffe7e4 100644 --- a/src/cddp_core/cddp_core.cpp +++ b/src/cddp_core/cddp_core.cpp @@ -15,7 +15,6 @@ */ #include "cddp_core/cddp_core.hpp" // For CDDP class declaration -#include "cddp_core/alddp_solver.hpp" // For AlddpSolver #include "cddp_core/clddp_solver.hpp" // For CLDDPSolver #include "cddp_core/ipddp_solver.hpp" // For IPDDPSolver #include "cddp_core/logddp_solver.hpp" // For LogDDPSolver @@ -263,8 +262,6 @@ std::string solverTypeToString(SolverType solver_type) { return "IPDDP"; case SolverType::MSIPDDP: return "MSIPDDP"; - case SolverType::ALDDP: - return "ALDDP"; default: return "CLDDP"; // Default fallback } @@ -293,8 +290,6 @@ CDDP::createSolver(const std::string &solver_type) { return std::make_unique(); } else if (solver_type == "MSIPDDP") { return std::make_unique(); - } else if (solver_type == "ALDDP") { - return std::make_unique(); } return nullptr; // Solver not found @@ -332,7 +327,7 @@ CDDPSolution CDDP::solve(const std::string &solver_type) { for (const auto &name : available) { std::cout << name << " "; } - std::cout << "CLDDP LogDDP IPDDP MSIPDDP ALDDP" << std::endl; + std::cout << "CLDDP LogDDP IPDDP MSIPDDP" << std::endl; } return solution; diff --git a/src/cddp_core/dbas_ddp_solver.cpp b/src/cddp_core/dbas_ddp_solver.cpp deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7de0b40e..da67e7de 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -143,10 +143,6 @@ add_executable(test_msipddp_solver cddp_core/test_msipddp_solver.cpp) target_link_libraries(test_msipddp_solver gtest gmock gtest_main cddp) gtest_discover_tests(test_msipddp_solver) -add_executable(test_alddp_solver cddp_core/test_alddp_solver.cpp) -target_link_libraries(test_alddp_solver gtest gmock gtest_main cddp) -gtest_discover_tests(test_alddp_solver) - # add_executable(test_logcddp_core cddp_core/test_logcddp_core.cpp) # target_link_libraries(test_logcddp_core gtest gmock gtest_main cddp) # gtest_discover_tests(test_logcddp_core) diff --git a/tests/cddp_core/test_alddp_solver.cpp b/tests/cddp_core/test_alddp_solver.cpp deleted file mode 100644 index 55f902be..00000000 --- a/tests/cddp_core/test_alddp_solver.cpp +++ /dev/null @@ -1,560 +0,0 @@ -// /* -// Copyright 2025 Tomo Sasaki - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at - -// https://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// */ -// #include -// #include -// #include -// #include -// #include -// #include -// #include - -// #include "gmock/gmock.h" -// #include "gtest/gtest.h" - -// #include "cddp.hpp" - -// TEST(ALDDPTest, SolvePendulum) -// { -// int state_dim = 2; -// int control_dim = 1; -// int horizon = 500; -// double timestep = 0.05; -// // Create a pendulum instance -// double mass = 1.0; -// double length = 1.0; -// double damping = 0.00; -// std::string integration_type = "euler"; - -// std::unique_ptr system = std::make_unique(timestep, length, mass, damping, integration_type); - -// // Cost matrices -// Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); -// Eigen::MatrixXd R = 0.1 * Eigen::MatrixXd::Identity(control_dim, control_dim); -// Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); -// Qf << 100.0, 0.0, -// 0.0, 100.0; - -// Eigen::VectorXd goal_state(state_dim); -// goal_state << 0.0, 0.0; // Upright position with zero velocity - -// std::vector empty_reference_states; -// // empty_reference_states.back() << 0.0, 0.0; -// auto objective = std::make_unique(Q, R, Qf, goal_state, empty_reference_states, timestep); - -// // Initial state (pendulum pointing down) -// Eigen::VectorXd initial_state(state_dim); -// initial_state << M_PI, 0.0; // Zero angle and angular velocity - -// // Construct zero control sequence -// std::vector zero_control_sequence(horizon, Eigen::VectorXd::Zero(control_dim)); - -// // Construct initial trajectory -// std::vector X_init(horizon + 1, Eigen::VectorXd::Zero(state_dim)); -// for (int t = 0; t < horizon + 1; ++t) -// { -// X_init[t] = initial_state; -// } - -// // Calculate initial cost -// double J = 0.0; -// for (int t = 0; t < horizon; ++t) -// { -// J += objective->running_cost(X_init[t], zero_control_sequence[t], t); -// } -// J += objective->terminal_cost(X_init[horizon]); - -// // Create CDDP solver -// cddp::CDDP cddp_solver(initial_state, goal_state, horizon, timestep); -// cddp_solver.setDynamicalSystem(std::move(system)); -// cddp_solver.setObjective(std::move(objective)); - -// // Control constraints -// Eigen::VectorXd control_lower_bound(control_dim); -// control_lower_bound << -10.0; // Maximum negative torque -// Eigen::VectorXd control_upper_bound(control_dim); -// control_upper_bound << 10.0; // Maximum positive torque - -// cddp_solver.addPathConstraint("ControlConstraint", -// std::make_unique( control_upper_bound)); - -// // Create CDDP Options -// cddp::CDDPOptions options; -// options.max_iterations = 100; -// options.tolerance = 1e-3; // KKT/optimality tolerance -// options.acceptable_tolerance = 1e-4; // Cost change tolerance -// options.enable_parallel = false; -// options.num_threads = 1; -// options.verbose = true; -// options.debug = false; -// options.regularization.initial_value = 1e-6; -// options.return_iteration_info = true; // Get detailed iteration history - -// // Set options -// cddp_solver.setOptions(options); - -// // Set initial trajectory -// std::vector X(horizon + 1, Eigen::VectorXd::Zero(state_dim)); -// std::vector U(horizon, Eigen::VectorXd::Zero(control_dim)); -// X[0] << initial_state; -// for (int i = 0; i < horizon; ++i) -// { -// U[i] = Eigen::VectorXd::Zero(control_dim); -// X[i] = initial_state; -// } -// X[horizon] << initial_state; - -// cddp_solver.setInitialTrajectory(X, U); - -// // Solve the problem -// std::cout << "\n=== First solve (cold start) ===" << std::endl; -// cddp::CDDPSolution solution = cddp_solver.solve("ALDDP"); - -// // Check convergence -// auto status_message = std::any_cast(solution.at("status_message")); -// auto iterations_completed = std::any_cast(solution.at("iterations_completed")); -// auto solve_time_ms = std::any_cast(solution.at("solve_time_ms")); -// auto final_objective = std::any_cast(solution.at("final_objective")); - -// std::cout << "\n=== Convergence Analysis ===" << std::endl; -// std::cout << "Status: " << status_message << std::endl; -// std::cout << "Converged: " << (status_message == "OptimalSolutionFound" || status_message == "AcceptableSolutionFound" ? "YES" : "NO") << std::endl; -// std::cout << "Iterations: " << iterations_completed << std::endl; -// std::cout << "Solve time: " << solve_time_ms << " ms" << std::endl; -// std::cout << "Final cost: " << final_objective << std::endl; - -// // Extract trajectories -// auto X_sol = std::any_cast>(solution.at("state_trajectory")); -// auto U_sol = std::any_cast>(solution.at("control_trajectory")); -// auto t_sol = std::any_cast>(solution.at("time_points")); - -// // Print final state -// Eigen::VectorXd final_state = X_sol.back(); -// std::cout << "Final state: [" << final_state.transpose() << "]" << std::endl; -// std::cout << "Goal state: [" << goal_state.transpose() << "]" << std::endl; -// std::cout << "Final error: " << (final_state - goal_state).norm() << std::endl; - -// // Test assertions -// EXPECT_TRUE(status_message == "OptimalSolutionFound" || status_message == "AcceptableSolutionFound") << "Algorithm should converge"; -// EXPECT_GT(iterations_completed, 0) << "Should take at least one iteration"; -// EXPECT_LT(final_objective, J) << "Final cost should be better than initial cost"; - -// // ========================================================================= -// // Test warm start capability -// // ========================================================================= -// std::cout << "\n=== Testing warm start ===" << std::endl; - -// // Enable warm start and use previous solution as initial guess -// cddp::CDDPOptions warm_options = options; -// warm_options.warm_start = true; -// warm_options.max_iterations = 10; // Fewer iterations for warm start -// warm_options.verbose = false; // Less verbose for warm start test -// warm_options.tolerance = 1e-3; // KKT/optimality tolerance -// warm_options.acceptable_tolerance = 1e-4; // Cost change tolerance -// warm_options.enable_parallel = false; -// warm_options.num_threads = 1; -// warm_options.debug = false; - -// // Create a new solver for warm start test -// auto hcw_system_warmstart = std::make_unique(timestep, length, mass, damping, integration_type); - -// // Create new objective -// std::vector empty_reference_states_warmstart; -// auto objective_warmstart = std::make_unique(Q, R, Qf, goal_state, empty_reference_states_warmstart, timestep); - -// cddp::CDDP warm_solver(initial_state, goal_state, horizon, timestep); -// warm_solver.setDynamicalSystem(std::move(hcw_system_warmstart)); -// warm_solver.setObjective(std::move(objective_warmstart)); -// warm_solver.addPathConstraint("ControlConstraint", -// std::make_unique( control_upper_bound)); -// warm_solver.setOptions(warm_options); - -// // Use previous solution as warm start -// warm_solver.setInitialTrajectory(X_sol, U_sol); - -// // Solve with warm start -// auto start_time = std::chrono::high_resolution_clock::now(); -// cddp::CDDPSolution warm_solution = warm_solver.solve("ALDDP"); -// auto end_time = std::chrono::high_resolution_clock::now(); -// auto warm_duration = std::chrono::duration_cast(end_time - start_time); - -// // Extract warm start results -// auto warm_status = std::any_cast(warm_solution.at("status_message")); -// auto warm_iterations = std::any_cast(warm_solution.at("iterations_completed")); -// auto warm_solve_time = std::any_cast(warm_solution.at("solve_time_ms")); -// auto warm_final_cost = std::any_cast(warm_solution.at("final_objective")); - -// std::cout << "Warm start status: " << warm_status << std::endl; -// std::cout << "Warm start iterations: " << warm_iterations << std::endl; -// std::cout << "Warm start solve time: " << warm_solve_time << " ms" << std::endl; -// std::cout << "Warm start final cost: " << warm_final_cost << std::endl; - -// // Warm start should converge faster or in fewer iterations -// std::cout << "\n=== Performance Comparison ===" << std::endl; -// std::cout << "Cold start: " << iterations_completed << " iterations, " << solve_time_ms << " ms" << std::endl; -// std::cout << "Warm start: " << warm_iterations << " iterations, " << warm_solve_time << " ms" << std::endl; - -// if (warm_iterations <= iterations_completed) -// { -// std::cout << "✓ Warm start used fewer or equal iterations" << std::endl; -// } -// else -// { -// std::cout << "✗ Warm start used more iterations (this can happen)" << std::endl; -// } - -// if (warm_solve_time <= solve_time_ms * 1.2) -// { // Allow 20% tolerance -// std::cout << "✓ Warm start was faster or comparable" << std::endl; -// } -// else -// { -// std::cout << "✗ Warm start was slower" << std::endl; -// } - -// // Both should converge -// EXPECT_TRUE(warm_status == "OptimalSolutionFound" || warm_status == "AcceptableSolutionFound") << "Warm start should also converge"; -// EXPECT_LE(warm_iterations, iterations_completed + 5) << "Warm start should not take significantly more iterations"; -// } - -// TEST(ALDDPTest, SolveUnicycle) { -// // Problem parameters -// int state_dim = 3; -// int control_dim = 2; -// int horizon = 100; -// double timestep = 0.03; -// std::string integration_type = "euler"; - -// // Create a dubins car instance -// std::unique_ptr system = std::make_unique(timestep, integration_type); // Create unique_ptr - -// // Create objective function -// Eigen::MatrixXd Q = Eigen::MatrixXd::Zero(state_dim, state_dim); -// Eigen::MatrixXd R = 0.5 * Eigen::MatrixXd::Identity(control_dim, control_dim); -// Eigen::MatrixXd Qf = Eigen::MatrixXd::Identity(state_dim, state_dim); -// Qf << 50.0, 0.0, 0.0, -// 0.0, 50.0, 0.0, -// 0.0, 0.0, 10.0; -// Qf = 0.5 * Qf; -// Eigen::VectorXd goal_state(state_dim); -// goal_state << 2.0, 2.0, M_PI/2.0; - -// // Create an empty vector of Eigen::VectorXd -// std::vector empty_reference_states; -// auto objective = std::make_unique(Q, R, Qf, goal_state, empty_reference_states, timestep); - -// // Initial and target states -// Eigen::VectorXd initial_state(state_dim); -// initial_state << 0.0, 0.0, M_PI/4.0; - -// // Create CDDP Options -// cddp::CDDPOptions options; -// options.max_iterations = 20; -// options.tolerance = 1e-2; -// options.enable_parallel = true; -// options.num_threads = 10; -// options.verbose = true; -// options.debug = false; - -// // Create CDDP solver -// cddp::CDDP cddp_solver(initial_state, goal_state, horizon, timestep); -// cddp_solver.setDynamicalSystem(std::move(system)); -// cddp_solver.setObjective(std::move(objective)); - -// // Define constraints -// Eigen::VectorXd control_lower_bound(control_dim); -// control_lower_bound << -1.0, -M_PI; -// Eigen::VectorXd control_upper_bound(control_dim); -// control_upper_bound << 1.0, M_PI; - -// // Add the constraint to the solver -// cddp_solver.addPathConstraint("ControlConstraint", std::make_unique(control_upper_bound)); - -// // Set options -// cddp_solver.setOptions(options); - -// // Set initial trajectory -// std::vector X(horizon + 1, Eigen::VectorXd::Zero(state_dim)); -// std::vector U(horizon, Eigen::VectorXd::Zero(control_dim)); -// cddp_solver.setInitialTrajectory(X, U); - -// // Solve the problem -// cddp::CDDPSolution solution = cddp_solver.solve("ALDDP"); - -// auto status = std::any_cast(solution.at("status_message")); -// ASSERT_TRUE(status == "OptimalSolutionFound" || status == "AcceptableSolutionFound"); - -// // Extract solution -// auto X_sol = std::any_cast>(solution.at("state_trajectory")); // size: horizon + 1 -// auto U_sol = std::any_cast>(solution.at("control_trajectory")); // size: horizon -// auto t_sol = std::any_cast>(solution.at("time_points")); // size: horizon + 1 -// } - -// namespace cddp -// { -// class CarParkingObjective : public NonlinearObjective -// { -// public: -// CarParkingObjective(const Eigen::VectorXd &goal_state, double timestep) -// : NonlinearObjective(timestep), reference_state_(goal_state) -// { -// // Control cost coefficients: cu = 1e-2*[1 .01] -// cu_ = Eigen::Vector2d(1e-2, 1e-4); - -// // Final cost coefficients: cf = [.1 .1 1 .3] -// cf_ = Eigen::Vector4d(0.1, 0.1, 1.0, 0.3); - -// // Smoothness scales for final cost: pf = [.01 .01 .01 1] -// pf_ = Eigen::Vector4d(0.01, 0.01, 0.01, 1.0); - -// // Running cost coefficients: cx = 1e-3*[1 1] -// cx_ = Eigen::Vector2d(1e-3, 1e-3); - -// // Smoothness scales for running cost: px = [.1 .1] -// px_ = Eigen::Vector2d(0.1, 0.1); -// } - -// double running_cost(const Eigen::VectorXd &state, -// const Eigen::VectorXd &control, -// int index) const override -// { -// // Control cost: lu = cu*u.^2 -// double lu = cu_.dot(control.array().square().matrix()); - -// // Running cost on distance from origin: lx = cx*sabs(x(1:2,:),px) -// Eigen::VectorXd xy_state = state.head(2); -// double lx = cx_.dot(sabs(xy_state, px_)); - -// return lu + lx; -// } - -// double terminal_cost(const Eigen::VectorXd &final_state) const override -// { -// // Final state cost: llf = cf*sabs(x(:,final),pf); -// return cf_.dot(sabs(final_state, pf_)) + running_cost(final_state, Eigen::VectorXd::Zero(2), 0); -// } - -// private: -// // Helper function for smooth absolute value (pseudo-Huber) -// Eigen::VectorXd sabs(const Eigen::VectorXd &x, const Eigen::VectorXd &p) const -// { -// return ((x.array().square() / p.array().square() + 1.0).sqrt() * p.array() - p.array()).matrix(); -// } - -// Eigen::VectorXd reference_state_; -// Eigen::Vector2d cu_; // Control cost coefficients -// Eigen::Vector4d cf_; // Final cost coefficients -// Eigen::Vector4d pf_; // Smoothness scales for final cost -// Eigen::Vector2d cx_; // Running cost coefficients -// Eigen::Vector2d px_; // Smoothness scales for running cost -// }; -// } // namespace cddp - -// // TEST(ALDDPTest, SolveCar) -// // { -// // int state_dim = 4; // [x y theta v] -// // int control_dim = 2; // [wheel_angle acceleration] -// // int horizon = 500; -// // double timestep = 0.03; -// // std::string integration_type = "euler"; - -// // // Create car instance -// // double wheelbase = 2.0; -// // std::unique_ptr system = -// // std::make_unique(timestep, wheelbase, integration_type); - -// // // Initial and goal states -// // Eigen::VectorXd initial_state(state_dim); -// // initial_state << 1.0, 1.0, 1.5 * M_PI, 0.0; // Start at (1,1) facing backwards - -// // Eigen::VectorXd goal_state(state_dim); -// // goal_state << 0.0, 0.0, 0.0, 0.0; // Park at origin facing forward - -// // // Create the nonlinear objective -// // auto objective = std::make_unique(goal_state, timestep); - -// // // Construct initial control sequence -// // std::vector initial_control_sequence(horizon, Eigen::VectorXd::Zero(control_dim)); -// // for (auto &u : initial_control_sequence) -// // { -// // u << 0.01, 0.01; // Small initial controls -// // } - -// // // Construct initial trajectory -// // std::vector X_init(horizon + 1, Eigen::VectorXd::Zero(state_dim)); -// // X_init[0] = initial_state; - -// // // Forward simulate initial trajectory -// // for (int t = 0; t < horizon; ++t) -// // { -// // X_init[t + 1] = system->getDiscreteDynamics(X_init[t], initial_control_sequence[t], t * timestep); -// // } - -// // // Calculate initial cost -// // double J = 0.0; -// // for (int t = 0; t < horizon; ++t) -// // { -// // J += objective->running_cost(X_init[t], initial_control_sequence[t], t); -// // } -// // J += objective->terminal_cost(X_init[horizon]); - -// // // Create CDDP solver -// // cddp::CDDP cddp_solver(initial_state, goal_state, horizon, timestep); -// // cddp_solver.setDynamicalSystem(std::move(system)); -// // cddp_solver.setObjective(std::move(objective)); - -// // // Control constraints -// // Eigen::VectorXd control_lower_bound(control_dim); -// // control_lower_bound << -0.5, -2.0; // [steering_angle, acceleration] -// // Eigen::VectorXd control_upper_bound(control_dim); -// // control_upper_bound << 0.5, 2.0; - -// // cddp_solver.addPathConstraint("ControlConstraint", -// // std::make_unique(control_upper_bound)); - -// // // Create CDDP Options -// // cddp::CDDPOptions options; -// // options.max_iterations = 300; // Reasonable number for testing -// // options.tolerance = 1e-6; // KKT/optimality tolerance -// // options.acceptable_tolerance = 1e-6; // Cost change tolerance -// // options.enable_parallel = false; -// // options.num_threads = 1; -// // options.verbose = true; -// // options.debug = true; -// // options.regularization.initial_value = 1e-4; -// // options.return_iteration_info = true; // Get detailed iteration history - -// // // Set options -// // cddp_solver.setOptions(options); - -// // // Set initial trajectory -// // cddp_solver.setInitialTrajectory(X_init, initial_control_sequence); - -// // // Solve the problem -// // std::cout << "\n=== First solve (cold start) ===" << std::endl; -// // cddp::CDDPSolution solution = cddp_solver.solve("ALTRO"); - -// // // Check convergence -// // auto status_message = std::any_cast(solution.at("status_message")); -// // auto iterations_completed = std::any_cast(solution.at("iterations_completed")); -// // auto solve_time_ms = std::any_cast(solution.at("solve_time_ms")); -// // auto final_objective = std::any_cast(solution.at("final_objective")); - -// // std::cout << "\n=== Convergence Analysis ===" << std::endl; -// // std::cout << "Status: " << status_message << std::endl; -// // std::cout << "Converged: " << (status_message == "OptimalSolutionFound" || status_message == "AcceptableSolutionFound" ? "YES" : "NO") << std::endl; -// // std::cout << "Iterations: " << iterations_completed << std::endl; -// // std::cout << "Solve time: " << solve_time_ms << " ms" << std::endl; -// // std::cout << "Initial cost: " << J << std::endl; -// // std::cout << "Final cost: " << final_objective << std::endl; - -// // // Extract trajectories -// // auto X_sol = std::any_cast>(solution.at("state_trajectory")); -// // auto U_sol = std::any_cast>(solution.at("control_trajectory")); -// // auto t_sol = std::any_cast>(solution.at("time_points")); - -// // // Print final state -// // Eigen::VectorXd final_state = X_sol.back(); -// // std::cout << "Initial state: [" << initial_state.transpose() << "]" << std::endl; -// // std::cout << "Final state: [" << final_state.transpose() << "]" << std::endl; -// // std::cout << "Goal state: [" << goal_state.transpose() << "]" << std::endl; -// // std::cout << "Final error: " << (final_state - goal_state).norm() << std::endl; - -// // // Test assertions -// // EXPECT_TRUE(status_message == "OptimalSolutionFound" || status_message == "AcceptableSolutionFound") -// // << "Algorithm should converge"; -// // EXPECT_GT(iterations_completed, 0) << "Should take at least one iteration"; -// // EXPECT_LT(final_objective, J) << "Final cost should be better than initial cost"; - -// // // ========================================================================= -// // // Test warm start capability -// // // ========================================================================= -// // std::cout << "\n=== Testing warm start ===" << std::endl; - -// // // Enable warm start and use previous solution as initial guess -// // cddp::CDDPOptions warm_options = options; -// // warm_options.warm_start = true; -// // warm_options.max_iterations = 1; // Fewer iterations for warm start -// // warm_options.verbose = false; // Less verbose for warm start test - -// // // Create a new solver for warm start test -// // auto car_system_warmstart = std::make_unique(timestep, wheelbase, integration_type); - -// // // Create new objective -// // auto objective_warmstart = std::make_unique(goal_state, timestep); - -// // cddp::CDDP warm_solver(initial_state, goal_state, horizon, timestep); -// // warm_solver.setDynamicalSystem(std::move(car_system_warmstart)); -// // warm_solver.setObjective(std::move(objective_warmstart)); -// // warm_solver.addPathConstraint("ControlConstraint", -// // std::make_unique(control_upper_bound)); -// // warm_solver.setOptions(warm_options); - -// // // Use previous solution as warm start -// // warm_solver.setInitialTrajectory(X_sol, U_sol); - -// // // Solve with warm start -// // auto start_time = std::chrono::high_resolution_clock::now(); -// // cddp::CDDPSolution warm_solution = warm_solver.solve("ALTRO"); -// // auto end_time = std::chrono::high_resolution_clock::now(); -// // auto warm_duration = std::chrono::duration_cast(end_time - start_time); - -// // // Extract warm start results -// // auto warm_status = std::any_cast(warm_solution.at("status_message")); -// // auto warm_iterations = std::any_cast(warm_solution.at("iterations_completed")); -// // auto warm_solve_time = std::any_cast(warm_solution.at("solve_time_ms")); -// // auto warm_final_cost = std::any_cast(warm_solution.at("final_objective")); - -// // std::cout << "Warm start status: " << warm_status << std::endl; -// // std::cout << "Warm start iterations: " << warm_iterations << std::endl; -// // std::cout << "Warm start solve time: " << warm_solve_time << " ms" << std::endl; -// // std::cout << "Warm start final cost: " << warm_final_cost << std::endl; - -// // // Warm start should converge faster or in fewer iterations -// // std::cout << "\n=== Performance Comparison ===" << std::endl; -// // std::cout << "Cold start: " << iterations_completed << " iterations, " << solve_time_ms << " ms" << std::endl; -// // std::cout << "Warm start: " << warm_iterations << " iterations, " << warm_solve_time << " ms" << std::endl; - -// // if (warm_iterations <= iterations_completed) -// // { -// // std::cout << "✓ Warm start used fewer or equal iterations" << std::endl; -// // } -// // else -// // { -// // std::cout << "✗ Warm start used more iterations (this can happen)" << std::endl; -// // } - -// // if (warm_solve_time <= solve_time_ms * 1.2) -// // { // Allow 20% tolerance -// // std::cout << "✓ Warm start was faster or comparable" << std::endl; -// // } -// // else -// // { -// // std::cout << "✗ Warm start was slower" << std::endl; -// // } - -// // // Both should converge -// // EXPECT_TRUE(warm_status == "OptimalSolutionFound" || warm_status == "AcceptableSolutionFound") -// // << "Warm start should also converge"; -// // EXPECT_LE(warm_iterations, iterations_completed + 10) << "Warm start should not take significantly more iterations"; - -// // // Verify that the car moves towards the goal -// // double initial_distance = (initial_state.head(2) - goal_state.head(2)).norm(); -// // double final_distance = (final_state.head(2) - goal_state.head(2)).norm(); -// // EXPECT_LT(final_distance, initial_distance) << "Car should move closer to the goal position"; - -// // // Check that final position is reasonably close to goal (within 0.5 units) -// // EXPECT_LT(final_distance, 0.5) << "Car should park reasonably close to the goal"; -// // } \ No newline at end of file