From 285d9feda53a39399f083062e4e25066a27246bf Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:07:20 -0400 Subject: [PATCH 01/18] fast observables --- CMakeLists.txt | 15 + setup.py | 2 + src/pybamm/__init__.py | 2 +- src/pybamm/plotting/quick_plot.py | 25 + src/pybamm/solvers/c_solvers/idaklu.cpp | 47 + .../c_solvers/idaklu/IDAKLUSolverOpenMP.hpp | 49 +- .../c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 221 ++++- .../solvers/c_solvers/idaklu/Options.cpp | 1 + .../solvers/c_solvers/idaklu/Options.hpp | 1 + .../solvers/c_solvers/idaklu/Solution.hpp | 6 +- .../solvers/c_solvers/idaklu/SolutionData.cpp | 34 +- .../solvers/c_solvers/idaklu/SolutionData.hpp | 9 + .../solvers/c_solvers/idaklu/common.hpp | 5 + .../solvers/c_solvers/idaklu/observe.cpp | 194 ++++ .../solvers/c_solvers/idaklu/observe.hpp | 340 +++++++ src/pybamm/solvers/idaklu_solver.py | 33 +- src/pybamm/solvers/processed_variable.py | 850 +++++++++++++----- .../solvers/processed_variable_computed.py | 54 +- src/pybamm/solvers/solution.py | 169 +++- .../test_models/standard_output_comparison.py | 1 + tests/unit/test_plotting/test_quick_plot.py | 28 +- tests/unit/test_solvers/test_idaklu_solver.py | 15 +- .../test_solvers/test_processed_variable.py | 70 +- .../test_processed_variable_computed.py | 8 +- 24 files changed, 1787 insertions(+), 392 deletions(-) create mode 100644 src/pybamm/solvers/c_solvers/idaklu/observe.cpp create mode 100644 src/pybamm/solvers/c_solvers/idaklu/observe.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a7f68ce7a0..fce51a0d62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,19 @@ cmake_minimum_required(VERSION 3.13) cmake_policy(SET CMP0074 NEW) set(CMAKE_VERBOSE_MAKEFILE ON) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE RelWithDebInfo) # Default to RelWithDebInfo build type if not set +endif() + +# Set optimization flags for Release builds +if(CMAKE_BUILD_TYPE STREQUAL "Release") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") # Max optimization +endif() + +# Alternatively, you can also set optimization flags for different build types +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g") # No optimization, with debugging info +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -O2") # Optimization with debugging info + if(DEFINED ENV{VCPKG_ROOT_DIR} AND NOT DEFINED VCPKG_ROOT_DIR) set(VCPKG_ROOT_DIR "$ENV{VCPKG_ROOT_DIR}" CACHE STRING "Vcpkg root directory") @@ -105,6 +118,8 @@ pybind11_add_module(idaklu src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp + src/pybamm/solvers/c_solvers/idaklu/observe.hpp + src/pybamm/solvers/c_solvers/idaklu/observe.cpp # IDAKLU expressions - concrete implementations ${IDAKLU_EXPR_CASADI_SOURCE_FILES} ${IDAKLU_EXPR_IREE_SOURCE_FILES} diff --git a/setup.py b/setup.py index 74de1baca4..8a49bfd715 100644 --- a/setup.py +++ b/setup.py @@ -327,6 +327,8 @@ def compile_KLU(): "src/pybamm/solvers/c_solvers/idaklu/Solution.hpp", "src/pybamm/solvers/c_solvers/idaklu/Options.hpp", "src/pybamm/solvers/c_solvers/idaklu/Options.cpp", + "src/pybamm/solvers/c_solvers/idaklu/observe.hpp", + "src/pybamm/solvers/c_solvers/idaklu/observe.cpp", "src/pybamm/solvers/c_solvers/idaklu.cpp", ], ) diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py index 36ad0b137a..51c7f49969 100644 --- a/src/pybamm/__init__.py +++ b/src/pybamm/__init__.py @@ -157,7 +157,7 @@ # Solver classes from .solvers.solution import Solution, EmptySolution, make_cycle_solution -from .solvers.processed_variable import ProcessedVariable +from .solvers.processed_variable import ProcessedVariable, process_variable from .solvers.processed_variable_computed import ProcessedVariableComputed from .solvers.base_solver import BaseSolver from .solvers.dummy_solver import DummySolver diff --git a/src/pybamm/plotting/quick_plot.py b/src/pybamm/plotting/quick_plot.py index 39dc974f9b..cba4128658 100644 --- a/src/pybamm/plotting/quick_plot.py +++ b/src/pybamm/plotting/quick_plot.py @@ -84,6 +84,13 @@ class QuickPlot: variable_limits : str or dict of str, optional How to set the axis limits (for 0D or 1D variables) or colorbar limits (for 2D variables). Options are: + N_t_max: int, optonal + The maximum number of time points to plot. If the number of time points is + greater than this, the time points are downsampled to fit. + N_t_linear: int, optional + The number of linearly spaced time points added to the t axis when the number of + time points is less than N_t_max. + Note: this is only used if the solution has hermite interpolation enabled. - "fixed" (default): keep all axes fixes so that all data is visible - "tight": make axes tight to plot at each time @@ -105,6 +112,8 @@ def __init__( time_unit=None, spatial_unit="um", variable_limits="fixed", + N_t_max=10000, + N_t_linear=100, ): solutions = self.preprocess_solutions(solutions) @@ -169,6 +178,22 @@ def __init__( min_t = np.min([t[0] for t in self.ts_seconds]) max_t = np.max([t[-1] for t in self.ts_seconds]) + N_t = sum(len(t) for t in self.ts_seconds) + hermite_interp = all(sol.hermite_interpolation for sol in solutions) + + if hermite_interp and ( + N_t + hermite_interp * N_t_linear * len(solutions) <= N_t_max + ): + + def t_evenly_sample(sol): + t_linspace = np.linspace(sol.t[0], sol.t[-1], N_t_linear)[1:-1] + return np.union1d(sol.t, t_linspace) + + self.ts_seconds = [t_evenly_sample(sol) for sol in solutions] + else: + # Linearly spaced time points + self.ts_seconds = [sol.t for sol in solutions] + # Set timescale if time_unit is None: # defaults depend on how long the simulation is diff --git a/src/pybamm/solvers/c_solvers/idaklu.cpp b/src/pybamm/solvers/c_solvers/idaklu.cpp index db7147feb2..aafa266804 100644 --- a/src/pybamm/solvers/c_solvers/idaklu.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu.cpp @@ -9,6 +9,7 @@ #include #include "idaklu/idaklu_solver.hpp" +#include "idaklu/observe.hpp" #include "idaklu/IDAKLUSolverGroup.hpp" #include "idaklu/IdakluJax.hpp" #include "idaklu/common.hpp" @@ -27,6 +28,7 @@ casadi::Function generate_casadi_function(const std::string &data) namespace py = pybind11; PYBIND11_MAKE_OPAQUE(std::vector); +PYBIND11_MAKE_OPAQUE(std::vector); PYBIND11_MAKE_OPAQUE(std::vector); PYBIND11_MODULE(idaklu, m) @@ -34,6 +36,7 @@ PYBIND11_MODULE(idaklu, m) m.doc() = "sundials solvers"; // optional module docstring py::bind_vector>(m, "VectorNdArray"); + py::bind_vector>(m, "VectorRealtypeNdArray"); py::bind_vector>(m, "VectorSolution"); py::class_(m, "IDAKLUSolverGroup") @@ -72,6 +75,27 @@ PYBIND11_MODULE(idaklu, m) py::arg("options"), py::return_value_policy::take_ownership); + m.def("observe_ND", &observe_ND, + "Observe ND variables", + py::arg("ts_np"), + py::arg("ys_np"), + py::arg("inputs_np"), + py::arg("funcs"), + py::arg("is_f_contiguous"), + py::arg("sizes"), + py::return_value_policy::take_ownership); + + m.def("observe_hermite_interp_ND", &observe_hermite_interp_ND, + "Observe ND variables", + py::arg("t_interp_np"), + py::arg("ts_np"), + py::arg("ys_np"), + py::arg("yps_np"), + py::arg("inputs_np"), + py::arg("funcs"), + py::arg("sizes"), + py::return_value_policy::take_ownership); + #ifdef IREE_ENABLE m.def("create_iree_solver_group", &create_idaklu_solver_group, "Create a group of iree idaklu solver objects", @@ -98,6 +122,27 @@ PYBIND11_MODULE(idaklu, m) py::arg("dvar_dp_fcns"), py::arg("options"), py::return_value_policy::take_ownership); + + m.def("observe_ND", &observe_ND, + "Observe ND variables", + py::arg("ts_np"), + py::arg("ys_np"), + py::arg("inputs_np"), + py::arg("funcs"), + py::arg("is_f_contiguous"), + py::arg("sizes"), + py::return_value_policy::take_ownership); + + m.def("observe_hermite_interp_ND", &observe_hermite_interp_ND, + "Observe ND variables", + py::arg("t_interp_np"), + py::arg("ts_np"), + py::arg("ys_np"), + py::arg("yps_np"), + py::arg("inputs_np"), + py::arg("funcs"), + py::arg("sizes"), + py::return_value_policy::take_ownership); #endif m.def("generate_function", &generate_casadi_function, @@ -167,7 +212,9 @@ PYBIND11_MODULE(idaklu, m) py::class_(m, "solution") .def_readwrite("t", &Solution::t) .def_readwrite("y", &Solution::y) + .def_readwrite("yp", &Solution::yp) .def_readwrite("yS", &Solution::yS) + .def_readwrite("ypS", &Solution::ypS) .def_readwrite("y_term", &Solution::y_term) .def_readwrite("flag", &Solution::flag); } diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp index 92eede3643..9997e537fe 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp @@ -54,9 +54,9 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver int const number_of_events; // cppcheck-suppress unusedStructMember int number_of_timesteps; int precon_type; // cppcheck-suppress unusedStructMember - N_Vector yy, yp, y_cache, avtol; // y, y', y cache vector, and absolute tolerance + N_Vector yy, yyp, y_cache, avtol; // y, y', y cache vector, and absolute tolerance N_Vector *yyS; // cppcheck-suppress unusedStructMember - N_Vector *ypS; // cppcheck-suppress unusedStructMember + N_Vector *yypS; // cppcheck-suppress unusedStructMember N_Vector id; // rhs_alg_id realtype rtol; int const jac_times_cjmass_nnz; // cppcheck-suppress unusedStructMember @@ -70,11 +70,14 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver vector res_dvar_dp; bool const sensitivity; // cppcheck-suppress unusedStructMember bool const save_outputs_only; // cppcheck-suppress unusedStructMember + bool save_hermite; // cppcheck-suppress unusedStructMember bool is_ODE; // cppcheck-suppress unusedStructMember int length_of_return_vector; // cppcheck-suppress unusedStructMember vector t; // cppcheck-suppress unusedStructMember vector> y; // cppcheck-suppress unusedStructMember + vector> yp; // cppcheck-suppress unusedStructMember vector>> yS; // cppcheck-suppress unusedStructMember + vector>> ypS; // cppcheck-suppress unusedStructMember SetupOptions const setup_opts; SolverOptions const solver_opts; @@ -144,6 +147,11 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver */ void InitializeStorage(int const N); + /** + * @brief Initialize the storage for Hermite interpolation + */ + void InitializeHermiteStorage(int const N); + /** * @brief Apply user-configurable IDA options */ @@ -190,14 +198,21 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver */ void ExtendAdaptiveArrays(); + /** + * @brief Extend the Hermite interpolation info by 1 + */ + void ExtendHermiteArrays(); + /** * @brief Set the step values */ void SetStep( - realtype &t_val, - realtype *y_val, - vector const &yS_val, - int &i_save + realtype &tval, + realtype *y_val, + realtype *yp_val, + vector const &yS_val, + vector const &ypS_val, + int &i_save ); /** @@ -211,7 +226,9 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver realtype &t_prev, realtype const &t_next, realtype *y_val, + realtype *yp_val, vector const &yS_val, + vector const &ypS_val, int &i_save ); @@ -255,6 +272,26 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver int &i_save ); + /** + * @brief Save the output function results at the requested time + */ + void SetStepHermite( + realtype &t_val, + realtype *yp_val, + const vector &ypS_val, + int &i_save + ); + + /** + * @brief Save the output function sensitivities at the requested time + */ + void SetStepHermiteSensitivities( + realtype &t_val, + realtype *yp_val, + const vector &ypS_val, + int &i_save + ); + }; #include "IDAKLUSolverOpenMP.inl" diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index 56f546facf..6139e2570f 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -2,6 +2,9 @@ #include "sundials_functions.hpp" #include +// import the IDAMem class +#include + #include "common.hpp" #include "SolutionData.hpp" @@ -48,7 +51,7 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( AllocateVectors(); if (sensitivity) { yyS = N_VCloneVectorArray(number_of_parameters, yy); - ypS = N_VCloneVectorArray(number_of_parameters, yp); + yypS = N_VCloneVectorArray(number_of_parameters, yyp); } // set initial values realtype *atval = N_VGetArrayPointer(avtol); @@ -58,14 +61,14 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( for (int is = 0; is < number_of_parameters; is++) { N_VConst(RCONST(0.0), yyS[is]); - N_VConst(RCONST(0.0), ypS[is]); + N_VConst(RCONST(0.0), yypS[is]); } // create Matrix objects SetMatrix(); // initialise solver - IDAInit(ida_mem, residual_eval, 0, yy, yp); + IDAInit(ida_mem, residual_eval, 0, yy, yyp); // set tolerances rtol = RCONST(rel_tol); @@ -95,14 +98,14 @@ void IDAKLUSolverOpenMP::AllocateVectors() { // Create vectors if (setup_opts.num_threads == 1) { yy = N_VNew_Serial(number_of_states, sunctx); - yp = N_VNew_Serial(number_of_states, sunctx); + yyp = N_VNew_Serial(number_of_states, sunctx); y_cache = N_VNew_Serial(number_of_states, sunctx); avtol = N_VNew_Serial(number_of_states, sunctx); id = N_VNew_Serial(number_of_states, sunctx); } else { DEBUG("IDAKLUSolverOpenMP::AllocateVectors OpenMP"); yy = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); - yp = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); + yyp = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); y_cache = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); avtol = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); id = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx); @@ -127,6 +130,26 @@ void IDAKLUSolverOpenMP::InitializeStorage(int const N) { vector(length_of_return_vector, 0.0) ) ); + + if (save_hermite) { + InitializeHermiteStorage(N); + } +} + +template +void IDAKLUSolverOpenMP::InitializeHermiteStorage(int const N) { + yp = vector>( + N, + vector(number_of_states, 0.0) + ); + + ypS = vector>>( + N, + vector>( + number_of_parameters, + vector(number_of_states, 0.0) + ) + ); } template @@ -285,7 +308,7 @@ void IDAKLUSolverOpenMP::Initialize() { if (sensitivity) { CheckErrors(IDASensInit(ida_mem, number_of_parameters, IDA_SIMULTANEOUS, - sensitivities_eval, yyS, ypS)); + sensitivities_eval, yyS, yypS)); CheckErrors(IDASensEEtolerances(ida_mem)); } @@ -321,13 +344,13 @@ IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() { SUNMatDestroy(J); N_VDestroy(avtol); N_VDestroy(yy); - N_VDestroy(yp); + N_VDestroy(yyp); N_VDestroy(y_cache); N_VDestroy(id); if (sensitivity) { N_VDestroyVectorArray(yyS, number_of_parameters); - N_VDestroyVectorArray(ypS, number_of_parameters); + N_VDestroyVectorArray(yypS, number_of_parameters); } IDAFree(&ida_mem); @@ -349,9 +372,13 @@ SolutionData IDAKLUSolverOpenMP::solve( const int number_of_evals = t_eval.size(); const int number_of_interps = t_interp.size(); - if (t.size() < number_of_evals + number_of_interps) { - InitializeStorage(number_of_evals + number_of_interps); - } + save_hermite = ( + solver_opts.hermite_interpolation && + save_adaptive_steps && + !save_outputs_only + ); + + InitializeStorage(number_of_evals + number_of_interps); int i_save = 0; @@ -378,12 +405,12 @@ SolutionData IDAKLUSolverOpenMP::solve( // Setup consistent initialization realtype *y_val = N_VGetArrayPointer(yy); - realtype *yp_val = N_VGetArrayPointer(yp); + realtype *yp_val = N_VGetArrayPointer(yyp); vector yS_val(number_of_parameters); vector ypS_val(number_of_parameters); for (int p = 0 ; p < number_of_parameters; p++) { yS_val[p] = N_VGetArrayPointer(yyS[p]); - ypS_val[p] = N_VGetArrayPointer(ypS[p]); + ypS_val[p] = N_VGetArrayPointer(yypS[p]); for (int i = 0; i < number_of_states; i++) { yS_val[p][i] = y0[i + (p + 1) * number_of_states]; ypS_val[p][i] = yp0[i + (p + 1) * number_of_states]; @@ -409,23 +436,27 @@ SolutionData IDAKLUSolverOpenMP::solve( ConsistentInitialization(t0, t_eval_next, init_type); } + // Set the initial stop time + IDASetStopTime(ida_mem, t_eval_next); + + // Progress one step. This must be done before the while loop to ensure + // that we can run IDAGetDky at t0 for dky = 1 + int retval = IDASolve(ida_mem, tf, &t_val, yy, yyp, IDA_ONE_STEP); + + // Store consistent initialization + CheckErrors(IDAGetDky(ida_mem, t0, 0, yy)); if (sensitivity) { - CheckErrors(IDAGetSensDky(ida_mem, t_val, 0, yyS)); + CheckErrors(IDAGetSensDky(ida_mem, t0, 0, yyS)); } - // Store Consistent initialization - SetStep(t0, y_val, yS_val, i_save); + SetStep(t0, y_val, yp_val, yS_val, ypS_val, i_save); - // Set the initial stop time - IDASetStopTime(ida_mem, t_eval_next); + // Reset the states at t = t_val. Sensitivities are handled in the while-loop + CheckErrors(IDAGetDky(ida_mem, t_val, 0, yy)); // Solve the system - int retval; DEBUG("IDASolve"); while (true) { - // Progress one step - retval = IDASolve(ida_mem, tf, &t_val, yy, yp, IDA_ONE_STEP); - if (retval < 0) { // failed break; @@ -448,18 +479,21 @@ SolutionData IDAKLUSolverOpenMP::solve( if (hit_tinterp) { // Save the interpolated state at t_prev < t < t_val, for all t in t_interp - SetStepInterp(i_interp, + SetStepInterp( + i_interp, t_interp_next, t_interp, t_val, t_prev, t_eval_next, y_val, + yp_val, yS_val, + ypS_val, i_save); } - if (hit_adaptive || hit_teval || hit_event) { + if (hit_adaptive || hit_teval || hit_event || hit_final_time) { if (hit_tinterp) { // Reset the states and sensitivities at t = t_val CheckErrors(IDAGetDky(ida_mem, t_val, 0, yy)); @@ -469,11 +503,16 @@ SolutionData IDAKLUSolverOpenMP::solve( } // Save the current state at t_val - if (hit_adaptive) { - // Dynamically allocate memory for the adaptive step - ExtendAdaptiveArrays(); + // First, check to make sure that the t_val is not equal to the current t value + // If it is, we don't want to save the current state twice + if (!hit_tinterp || t_val != t.back()) { + if (hit_adaptive) { + // Dynamically allocate memory for the adaptive step + ExtendAdaptiveArrays(); + } + + SetStep(t_val, y_val, yp_val, yS_val, ypS_val, i_save); } - SetStep(t_val, y_val, yS_val, i_save); } if (hit_final_time || hit_event) { @@ -481,7 +520,7 @@ SolutionData IDAKLUSolverOpenMP::solve( break; } else if (hit_teval) { // Set the next stop time - i_eval += 1; + i_eval++; t_eval_next = t_eval[i_eval]; CheckErrors(IDASetStopTime(ida_mem, t_eval_next)); @@ -491,6 +530,9 @@ SolutionData IDAKLUSolverOpenMP::solve( } t_prev = t_val; + + // Progress one step + retval = IDASolve(ida_mem, tf, &t_val, yy, yyp, IDA_ONE_STEP); } int const length_of_final_sv_slice = save_outputs_only ? number_of_states : 0; @@ -547,7 +589,50 @@ SolutionData IDAKLUSolverOpenMP::solve( } } - return SolutionData(retval, number_of_timesteps, length_of_return_vector, arg_sens0, arg_sens1, arg_sens2, length_of_final_sv_slice, t_return, y_return, yS_return, yterm_return); + realtype *yp_return = new realtype[(save_hermite ? 1 : 0) * (number_of_timesteps * number_of_states)]; + realtype *ypS_return = new realtype[(save_hermite ? 1 : 0) * (arg_sens0 * arg_sens1 * arg_sens2)]; + if (save_hermite) { + count = 0; + for (size_t i = 0; i < number_of_timesteps; i++) { + for (size_t j = 0; j < number_of_states; j++) { + yp_return[count] = yp[i][j]; + count++; + } + } + + // Sensitivity states, ypS + // Note: Ordering of vector is different if computing outputs vs returning + // the complete state vector + count = 0; + for (size_t idx0 = 0; idx0 < arg_sens0; idx0++) { + for (size_t idx1 = 0; idx1 < arg_sens1; idx1++) { + for (size_t idx2 = 0; idx2 < arg_sens2; idx2++) { + auto i = (save_outputs_only ? idx0 : idx1); + auto j = (save_outputs_only ? idx1 : idx2); + auto k = (save_outputs_only ? idx2 : idx0); + + ypS_return[count] = ypS[i][k][j]; + count++; + } + } + } + } + + return SolutionData( + retval, + number_of_timesteps, + length_of_return_vector, + arg_sens0, + arg_sens1, + arg_sens2, + length_of_final_sv_slice, + save_hermite, + t_return, + y_return, + yp_return, + yS_return, + ypS_return, + yterm_return); } template @@ -563,14 +648,30 @@ void IDAKLUSolverOpenMP::ExtendAdaptiveArrays() { if (sensitivity) { yS.emplace_back(number_of_parameters, vector(length_of_return_vector, 0.0)); } + + if (save_hermite) { + ExtendHermiteArrays(); + } +} + +template +void IDAKLUSolverOpenMP::ExtendHermiteArrays() { + DEBUG("IDAKLUSolver::ExtendHermiteArrays"); + // States + yp.emplace_back(number_of_states, 0.0); + + // Sensitivity + if (sensitivity) { + ypS.emplace_back(number_of_parameters, vector(number_of_states, 0.0)); + } } template void IDAKLUSolverOpenMP::ReinitializeIntegrator(const realtype& t_val) { DEBUG("IDAKLUSolver::ReinitializeIntegrator"); - CheckErrors(IDAReInit(ida_mem, t_val, yy, yp)); + CheckErrors(IDAReInit(ida_mem, t_val, yy, yyp)); if (sensitivity) { - CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, ypS)); + CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, yypS)); } } @@ -609,14 +710,16 @@ void IDAKLUSolverOpenMP::ConsistentInitializationODE( realtype *y_cache_val = N_VGetArrayPointer(y_cache); std::memset(y_cache_val, 0, number_of_states * sizeof(realtype)); // Overwrite yp - residual_eval(t_val, yy, y_cache, yp, functions.get()); + residual_eval(t_val, yy, y_cache, yyp, functions.get()); } template void IDAKLUSolverOpenMP::SetStep( realtype &tval, realtype *y_val, + realtype *yp_val, vector const &yS_val, + vector const &ypS_val, int &i_save ) { // Set adaptive step results for y and yS @@ -629,6 +732,10 @@ void IDAKLUSolverOpenMP::SetStep( SetStepOutput(tval, y_val, yS_val, i_save); } else { SetStepFull(tval, y_val, yS_val, i_save); + + if (save_hermite) { + SetStepHermite(tval, yp_val, ypS_val, i_save); + } } i_save++; @@ -644,7 +751,9 @@ void IDAKLUSolverOpenMP::SetStepInterp( realtype &t_prev, realtype const &t_eval_next, realtype *y_val, + realtype *yp_val, vector const &yS_val, + vector const &ypS_val, int &i_save ) { // Save the state at the requested time @@ -657,7 +766,7 @@ void IDAKLUSolverOpenMP::SetStepInterp( } // Memory is already allocated for the interpolated values - SetStep(t_interp_next, y_val, yS_val, i_save); + SetStep(t_interp_next, y_val, yp_val, yS_val, ypS_val, i_save); i_interp++; if (i_interp == (t_interp.size())) { @@ -769,6 +878,50 @@ void IDAKLUSolverOpenMP::SetStepOutputSensitivities( } } +template +void IDAKLUSolverOpenMP::SetStepHermite( + realtype &tval, + realtype *yp_val, + vector const &ypS_val, + int &i_save +) { + // Set adaptive step results for yp and ypS + DEBUG("IDAKLUSolver::SetStepHermite"); + + // States + CheckErrors(IDAGetDky(ida_mem, tval, 1, yyp)); + auto &yp_back = yp[i_save]; + for (size_t j = 0; j < length_of_return_vector; ++j) { + yp_back[j] = yp_val[j]; + + } + + // Sensitivity + if (sensitivity) { + SetStepHermiteSensitivities(tval, yp_val, ypS_val, i_save); + } +} + +template +void IDAKLUSolverOpenMP::SetStepHermiteSensitivities( + realtype &tval, + realtype *yp_val, + vector const &ypS_val, + int &i_save +) { + DEBUG("IDAKLUSolver::SetStepHermiteSensitivities"); + + // Calculate sensitivities for the full ypS array + CheckErrors(IDAGetSensDky(ida_mem, tval, 1, yypS)); + for (size_t j = 0; j < number_of_parameters; ++j) { + auto &ypS_back_j = ypS[i_save][j]; + auto &ypSval_j = ypS_val[j]; + for (size_t k = 0; k < number_of_states; ++k) { + ypS_back_j[k] = ypSval_j[k]; + } + } +} + template void IDAKLUSolverOpenMP::CheckErrors(int const & flag) { if (flag < 0) { diff --git a/src/pybamm/solvers/c_solvers/idaklu/Options.cpp b/src/pybamm/solvers/c_solvers/idaklu/Options.cpp index 51544040ee..8eb605fe77 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Options.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Options.cpp @@ -149,6 +149,7 @@ SolverOptions::SolverOptions(py::dict &py_opts) nonlinear_convergence_coefficient(RCONST(py_opts["nonlinear_convergence_coefficient"].cast())), nonlinear_convergence_coefficient_ic(RCONST(py_opts["nonlinear_convergence_coefficient_ic"].cast())), suppress_algebraic_error(py_opts["suppress_algebraic_error"].cast()), + hermite_interpolation(py_opts["hermite_interpolation"].cast()), // IDA initial conditions calculation calc_ic(py_opts["calc_ic"].cast()), init_all_y_ic(py_opts["init_all_y_ic"].cast()), diff --git a/src/pybamm/solvers/c_solvers/idaklu/Options.hpp b/src/pybamm/solvers/c_solvers/idaklu/Options.hpp index d0c0c1d766..7418c68ec3 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Options.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Options.hpp @@ -38,6 +38,7 @@ struct SolverOptions { double nonlinear_convergence_coefficient; double nonlinear_convergence_coefficient_ic; sunbooleantype suppress_algebraic_error; + bool hermite_interpolation; // IDA initial conditions calculation bool calc_ic; bool init_all_y_ic; diff --git a/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp index a43e6a7174..8227bb9da8 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp @@ -17,8 +17,8 @@ class Solution /** * @brief Constructor */ - Solution(int &retval, np_array &t_np, np_array &y_np, np_array &yS_np, np_array &y_term_np) - : flag(retval), t(t_np), y(y_np), yS(yS_np), y_term(y_term_np) + Solution(int &retval, np_array &t_np, np_array &y_np, np_array &yp_np, np_array &yS_np, np_array &ypS_np, np_array &y_term_np) + : flag(retval), t(t_np), y(y_np), yp(yp_np), yS(yS_np), ypS(ypS_np), y_term(y_term_np) { } @@ -30,7 +30,9 @@ class Solution int flag; np_array t; np_array y; + np_array yp; np_array yS; + np_array ypS; np_array y_term; }; diff --git a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp index 00c2ddbccc..bc48c646d3 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp @@ -29,6 +29,20 @@ Solution SolutionData::generate_solution() { free_y_when_done ); + py::capsule free_yp_when_done( + yp_return, + [](void *f) { + realtype *vect = reinterpret_cast(f); + delete[] vect; + } + ); + + np_array yp_ret = np_array( + (save_hermite ? 1 : 0) * number_of_timesteps * length_of_return_vector, + &yp_return[0], + free_yp_when_done + ); + py::capsule free_yS_when_done( yS_return, [](void *f) { @@ -47,6 +61,24 @@ Solution SolutionData::generate_solution() { free_yS_when_done ); + py::capsule free_ypS_when_done( + ypS_return, + [](void *f) { + realtype *vect = reinterpret_cast(f); + delete[] vect; + } + ); + + np_array ypS_ret = np_array( + std::vector { + (save_hermite ? 1 : 0) * arg_sens0, + arg_sens1, + arg_sens2 + }, + &ypS_return[0], + free_ypS_when_done + ); + // Final state slice, yterm py::capsule free_yterm_when_done( yterm_return, @@ -63,5 +95,5 @@ Solution SolutionData::generate_solution() { ); // Store the solution - return Solution(flag, t_ret, y_ret, yS_ret, y_term); + return Solution(flag, t_ret, y_ret, yp_ret, yS_ret, ypS_ret, y_term); } diff --git a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp index 815e41daca..81ca7f5221 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp @@ -27,9 +27,12 @@ class SolutionData int arg_sens1, int arg_sens2, int length_of_final_sv_slice, + bool save_hermite, realtype *t_return, realtype *y_return, + realtype *yp_return, realtype *yS_return, + realtype *ypS_return, realtype *yterm_return): flag(flag), number_of_timesteps(number_of_timesteps), @@ -38,9 +41,12 @@ class SolutionData arg_sens1(arg_sens1), arg_sens2(arg_sens2), length_of_final_sv_slice(length_of_final_sv_slice), + save_hermite(save_hermite), t_return(t_return), y_return(y_return), + yp_return(yp_return), yS_return(yS_return), + ypS_return(ypS_return), yterm_return(yterm_return) {} @@ -64,9 +70,12 @@ class SolutionData int arg_sens1; int arg_sens2; int length_of_final_sv_slice; + bool save_hermite; realtype *t_return; realtype *y_return; + realtype *yp_return; realtype *yS_return; + realtype *ypS_return; realtype *yterm_return; }; diff --git a/src/pybamm/solvers/c_solvers/idaklu/common.hpp b/src/pybamm/solvers/c_solvers/idaklu/common.hpp index 58be90932e..8318d9596f 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/common.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/common.hpp @@ -27,12 +27,17 @@ #include /* access to sparse SUNMatrix */ #include /* access to dense SUNMatrix */ +#include +#include +#include + #include #include namespace py = pybind11; // note: we rely on c_style ordering for numpy arrays so don't change this! using np_array = py::array_t; +using np_array_realtype = py::array_t; using np_array_int = py::array_t; /** diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp new file mode 100644 index 0000000000..0fec065ece --- /dev/null +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -0,0 +1,194 @@ +#include "observe.hpp" + +void hermite_interp( + std::vector& out, + const double t_interp, + const py::detail::unchecked_reference& t, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp, + const size_t j +) { + const double h = t_interp - t(j); + const double h2 = h * h; + const double h3 = h2 * h; + + const double h_full = t(j + 1) - t(j); + const double inv_h = 1 / h_full; + const double inv_h2 = inv_h * inv_h; + const double inv_h3 = inv_h2 * inv_h; + + double c, d, y_ij, yp_ij, y_ijp1, yp_ijp1; + + for (size_t i = 0; i < out.size(); ++i) { + y_ij = y(i, j); + yp_ij = yp(i, j); + y_ijp1 = y(i, j + 1); + yp_ijp1 = yp(i, j + 1); + + // c[i] = (3 * (y_ptr[i + 1] - y_ptr[i]) * inv_h_sq) - (2 * yp_ptr[i] + yp_ptr[i + 1]) * inv_h; + // d[i] = (2 * (y_ptr[i] - y_ptr[i + 1]) * inv_h_sq * inv_h) + (yp_ptr[i] + yp_ptr[i + 1]) * inv_h_sq; + + c = 3 * (y_ijp1 - y_ij) * inv_h2 - (2 * yp_ij + yp_ijp1) * inv_h; + d = 2 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; + + out[i] = y_ij + yp_ij * h + c * h2 + d * h3; + } +} + +void apply_copy( + std::vector& out, + const py::detail::unchecked_reference& y, + const size_t j +) { + for (size_t i = 0; i < out.size(); ++i) { + out[i] = y(i, j); + } +} + +void hermite_interp_no_y( + std::vector& out, + const double t_interp, + const py::detail::unchecked_reference& t, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp, + const size_t j +) { + // This begins from a copy of y, so we don't need to copy y + // once again + const double h = t_interp - t(j); + const double h2 = h * h; + const double h3 = h2 * h; + + const double h_full = t(j + 1) - t(j); + const double inv_h = 1 / h_full; + const double inv_h2 = inv_h * inv_h; + const double inv_h3 = inv_h2 * inv_h; + + double c, d, y_ij, yp_ij, y_ijp1, yp_ijp1; + + for (size_t i = 0; i < out.size(); ++i) { + y_ij = y(i, j); + yp_ij = yp(i, j); + y_ijp1 = y(i, j + 1); + yp_ijp1 = yp(i, j + 1); + + c = 3 * (y_ijp1 - y_ij) * inv_h2 - (2 * yp_ij + yp_ijp1) * inv_h; + d = 2 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; + + out[i] += yp_ij * h + c * h2 + d * h3; + } +} + +void compute_c_d( + std::vector& c_out, + std::vector& d_out, + const py::detail::unchecked_reference& t, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp, + const size_t j +) { + const double h_full = t(j + 1) - t(j); + const double inv_h = 1.0 / h_full; + const double inv_h2 = inv_h * inv_h; + const double inv_h3 = inv_h2 * inv_h; + + for (size_t i = 0; i < y.shape(0); ++i) { + double y_ij = y(i, j); + double yp_ij = yp(i, j); + double y_ijp1 = y(i, j + 1); + double yp_ijp1 = yp(i, j + 1); + + c_out[i] = 3.0 * (y_ijp1 - y_ij) * inv_h2 - (2.0 * yp_ij + yp_ijp1) * inv_h; + d_out[i] = 2.0 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; + } +} + +void apply_hermite_interp( + std::vector& out, + const double t_interp, + const double t_j, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp, + const std::vector& c, + const std::vector& d, + const size_t j +) { + const double h = t_interp - t_j; // h = t_interp - t(j) + const double h2 = h * h; + const double h3 = h2 * h; + + for (size_t i = 0; i < out.size(); ++i) { + double y_ij = y(i, j); + double yp_ij = yp(i, j); + + out[i] = y_ij + yp_ij * h + c[i] * h2 + d[i] * h3; + } +} + +const double hermite_interp_scalar( + const double t_interp, + const double t_j, + const double t_jp1, + const double y_j, + const double y_jp1, + const double yp_j, + const double yp_jp1 +) { + // const double h = t_interp - t(j); + // const double h2 = h * h; + // const double h3 = h2 * h; + + // const double h_full = t(j + 1) - t(j); + // const double inv_h = 1 / h_full; + // const double inv_h2 = inv_h * inv_h; + // const double inv_h3 = inv_h2 * inv_h; + + // double c, d, y_ij, yp_ij, y_ijp1, yp_ijp1; + + // for (size_t i = 0; i < out.size(); ++i) { + // y_ij = y(i, j); + // yp_ij = yp(i, j); + // y_ijp1 = y(i, j + 1); + // yp_ijp1 = yp(i, j + 1); + + // // c[i] = (3 * (y_ptr[i + 1] - y_ptr[i]) * inv_h_sq) - (2 * yp_ptr[i] + yp_ptr[i + 1]) * inv_h; + // // d[i] = (2 * (y_ptr[i] - y_ptr[i + 1]) * inv_h_sq * inv_h) + (yp_ptr[i] + yp_ptr[i + 1]) * inv_h_sq; + + // c = 3 * (y_ijp1 - y_ij) * inv_h2 - (2 * yp_ij + yp_ijp1) * inv_h; + // d = 2 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; + + // out[i] = y_ij + yp_ij * h + c * h2 + d * h3; + // } + + const double h = t_interp - t_j; + const double h2 = h * h; + const double h3 = h2 * h; + + const double hinv = 1 / (t_jp1 - t_j); + const double h2inv = hinv * hinv; + const double h3inv = h2inv * hinv; + + const double c = 3 * (y_jp1 - y_j) * h2inv - (2 * yp_j + yp_jp1) * hinv; + const double d = 2 * (y_j - y_jp1) * h3inv + (yp_j + yp_jp1) * h2inv; + + return y_j + yp_j * h + c * h2 + d * h3; +} + +const int _setup_observables(const vector& sizes) { + // Create a numpy array to manage the output + if (sizes.size() == 0) { + throw std::invalid_argument("sizes must have at least one element"); + } + + // Create a numpy array to manage the output + int size_tot = 1; + for (const auto& size : sizes) { + size_tot *= size; + } + + if (size_tot == 0) { + throw std::invalid_argument("sizes must have at least one element"); + } + + return size_tot; +} diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp new file mode 100644 index 0000000000..d1d8e60a67 --- /dev/null +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -0,0 +1,340 @@ +#ifndef PYBAMM_CREATE_OBSERVE_HPP +#define PYBAMM_CREATE_OBSERVE_HPP + +#include "IDAKLUSolverOpenMP_solvers.hpp" +#include +#include +#include +#include "common.hpp" + +#include +#include // For numpy support in pybind11 + +namespace py = pybind11; + +void apply_copy( + std::vector& out, + const py::detail::unchecked_reference& y, + const size_t j +); + +/** + * @brief Loops over the solution and generates the observable output + */ +template +void process_time_series( + const vector& ts_np, + const vector& ys_np, + const vector& inputs_np, + const vector& funcs, + double* out, + const bool is_f_contiguous, + const int len +) { + // Buffer for non-f-contiguous arrays + vector y_buffer; + + int count = 0; + for (size_t i = 0; i < ts_np.size(); i++) { + const auto& t_i = ts_np[i].unchecked<1>(); + const auto& y_i = ys_np[i].unchecked<2>(); // y_i is 2D + const auto inputs_data_i = inputs_np[i].data(); + const auto func_i = *funcs[i]; + + int M = y_i.shape(0); + if (!is_f_contiguous && y_buffer.size() < M) { + y_buffer.resize(M); // Resize the buffer + } + + for (size_t j = 0; j < t_i.size(); j++) { + const double t_ij = t_i(j); + + // Use a view of y_i + if (!is_f_contiguous) { + apply_copy(y_buffer, y_i, j); + } + const double* y_ij = is_f_contiguous ? &y_i(0, j) : y_buffer.data(); + + // Prepare CasADi function arguments + vector args = { &t_ij, y_ij, inputs_data_i }; + vector results = { &out[count] }; + // Call the CasADi function with proper arguments + (func_i)(args, results); + + count += len; + } + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +void hermite_interp( + std::vector& out, + const double t_interp, + const py::detail::unchecked_reference& t, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp, + const size_t j +); + +const double hermite_interp_scalar( + const double t_interp, + const double t_j, + const double t_jp1, + const double y_j, + const double y_jp1, + const double yp_j, + const double yp_jp1 +); + +void compute_c_d( + std::vector& c_out, + std::vector& d_out, + const py::detail::unchecked_reference& t, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp, + const size_t j +); + +void apply_hermite_interp( + std::vector& out, + const double t_interp, + const double t_j, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp, + const std::vector& c, + const std::vector& d, + const size_t j +); + +/** + * @brief Loops over the solution and generates the observable output + */ +template +void process_and_interp_sorted_time_series( + const np_array_realtype& t_interp_np, + const vector& ts_data_np, + const vector& ys_data_np, + const vector& yps_data_np, + const vector& inputs_np, + const vector& funcs, + double* out, + const int len +) { + // y cache + vector y_interp; + + auto t_interp = t_interp_np.unchecked<1>(); + ssize_t i_interp = 0; + int count = 0; + + auto t_interp_next = t_interp(0); + vector args; + vector results; + + ssize_t N_data = 0; + const ssize_t N_interp = t_interp.size(); + + for (size_t i = 0; i < ts_data_np.size(); i++) { + N_data += ts_data_np[i].size(); + } + + const bool cache_hermite_interp = N_interp > N_data; + vector hermite_c; + vector hermite_d; + + for (size_t i = 0; i < ts_data_np.size(); i++) { + const auto& t_data_i = ts_data_np[i].unchecked<1>(); + const auto& y_data_i = ys_data_np[i].unchecked<2>(); // y_data_i is 2D + const auto& yp_data_i = yps_data_np[i].unchecked<2>(); // yp_data_i is 2D + const auto inputs_i = inputs_np[i].data(); + const auto func_i = *funcs[i]; + const double t_data_final = t_data_i(t_data_i.size() - 1); // Access last element + + // Resize y_interp buffer to match the number of rows in y_data_i + int M = y_data_i.shape(0); + if (y_interp.size() < M) { + y_interp.resize(M); + if (cache_hermite_interp) { + hermite_c.resize(M); + hermite_d.resize(M); + } + } + + ssize_t j = 0; + t_interp_next = t_interp(i_interp); + while (i_interp < N_interp && t_interp(i_interp) <= t_data_final) { + // Find the correct index j + for (; j < t_data_i.size() - 2; ++j) { + if (t_data_i(j) <= t_interp_next && t_interp_next <= t_data_i(j + 1)) { + break; + } + } + const double t_data_start = t_data_i(j); + const double t_data_next = t_data_i(j + 1); + + if (cache_hermite_interp) { + compute_c_d(hermite_c, hermite_d, t_data_i, y_data_i, yp_data_i, j); + } + + args = { &t_interp_next, y_interp.data(), inputs_i }; + + // Perform Hermite interpolation for all valid t_interp values + for (ssize_t k = 0; t_interp_next <= t_data_next; ++k) { + if (k == 0 && t_interp_next == t_data_start) { + apply_copy(y_interp, y_data_i, j); + } else if (cache_hermite_interp) { + apply_hermite_interp(y_interp, t_interp_next, t_data_start, y_data_i, yp_data_i, hermite_c, hermite_d, j); + } else { + hermite_interp(y_interp, t_interp_next, t_data_i, y_data_i, yp_data_i, j); + } + + // Prepare CasADi function arguments + results = { &out[count] }; + + // Call the CasADi function with the proper arguments + func_i(args, results); + + count += len; + ++i_interp; // Move to the next time step for interpolation + if (i_interp >= N_interp) { + return; + } + t_interp_next = t_interp(i_interp); + } + } + } + + if (i_interp == N_interp) { + return; + } + + // Extrapolate right if needed + const auto& t_data_i = ts_data_np[ts_data_np.size() - 1].unchecked<1>(); + const auto& y_data_i = ys_data_np[ys_data_np.size() - 1].unchecked<2>(); // y_data_i is 2D + const auto& yp_data_i = yps_data_np[yps_data_np.size() - 1].unchecked<2>(); // yp_data_i is 2D + const auto inputs_i = inputs_np[inputs_np.size() - 1].data(); + const auto func_i = *funcs[funcs.size() - 1]; + + const ssize_t j = t_data_i.size() - 2; + const double t_data_start = t_data_i(j); + const double t_data_final = t_data_i(j + 1); + + // Resize y_interp buffer to match the number of rows in y_data_i + int M = y_data_i.shape(0); + if (y_interp.size() < M) { + y_interp.resize(M); + if (cache_hermite_interp) { + hermite_c.resize(M); + hermite_d.resize(M); + } + } + + // Find the number of steps within this interval + args = { &t_interp_next, y_interp.data(), inputs_i }; + + // Perform Hermite interpolation for all valid t_interp values + for (; i_interp < N_interp; ++i_interp) { + t_interp_next = t_interp(i_interp); + + if (cache_hermite_interp) { + compute_c_d(hermite_c, hermite_d, t_data_i, y_data_i, yp_data_i, j); + apply_hermite_interp(y_interp, t_interp_next, t_data_start, y_data_i, yp_data_i, hermite_c, hermite_d, j); + } else { + hermite_interp(y_interp, t_interp_next, t_data_i, y_data_i, yp_data_i, j); + } + + // Prepare CasADi function arguments + results = { &out[count] }; + + // Call the CasADi function with the proper arguments + func_i(args, results); + + count += len; + } +} + +const int _setup_observables(const vector& sizes); + + +/** + * @brief Observe and Hermite interpolate ND variables + */ +template +const py::array_t observe_hermite_interp_ND( + const np_array_realtype& t_interp_np, + const vector& ts_np, + const vector& ys_np, + const vector& yps_np, + const vector& inputs_np, + const vector& funcs, + const vector sizes +) { + const int size_tot = _setup_observables(sizes); + + py::array_t out_array(sizes); + auto out = out_array.mutable_data(); + + process_and_interp_sorted_time_series( + t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, out, size_tot / sizes.back() + ); + + return out_array; +} + + +/** + * @brief Observe ND variables + */ +template +const py::array_t observe_ND( + const vector& ts_np, + const vector& ys_np, + const vector& inputs_np, + const vector& funcs, + const bool is_f_contiguous, + const vector sizes +) { + const int size_tot = _setup_observables(sizes); + + py::array_t out_array(sizes); + auto out = out_array.mutable_data(); + + process_time_series( + ts_np, ys_np, inputs_np, funcs, out, is_f_contiguous, size_tot / sizes.back() + ); + + return out_array; +} + +#endif // PYBAMM_CREATE_OBSERVE_HPP diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 32048d89c0..058bfc0a1a 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -30,6 +30,7 @@ if idaklu_spec.loader: idaklu_spec.loader.exec_module(idaklu) except ImportError as e: # pragma: no cover + idaklu = None print(f"Error loading idaklu: {e}") idaklu_spec = None @@ -133,6 +134,10 @@ class IDAKLUSolver(pybamm.BaseSolver): "nonlinear_convergence_coefficient": 0.33, # Suppress algebraic variables from error test "suppress_algebraic_error": False, + # Store Hermite interpolation data for the solution. + # Note: this option is always disabled if output_variables are given + # or if t_interp values are specified + "hermite_interpolation": True, ## Initial conditions calculation # Positive constant in the Newton iteration convergence test within the # initial condition calculation @@ -201,6 +206,7 @@ def __init__( "max_convergence_failures": 100, "nonlinear_convergence_coefficient": 0.33, "suppress_algebraic_error": False, + "hermite_interpolation": True, "nonlinear_convergence_coefficient_ic": 0.0033, "max_num_steps_ic": 50, "max_num_jacobians_ic": 40, @@ -765,6 +771,8 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None): else: inputs = np.array([[]]) + save_adaptive_steps = t_interp is None or len(t_interp) == 0 + # stack y0full and ydot0full so they are a 2D array of shape (number_of_inputs, number_of_states + number_of_parameters * number_of_states) # note that y0full and ydot0full are currently 1D arrays (i.e. independent of inputs), but in the future we will support # different initial conditions for different inputs (see https://github.com/pybamm-team/PyBaMM/pull/4260). For now we just repeat the same initial conditions for each input @@ -792,18 +800,23 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None): integration_time = timer.time() return [ - self._post_process_solution(soln, model, integration_time, inputs_dict) + self._post_process_solution( + soln, model, integration_time, inputs_dict, save_adaptive_steps + ) for soln, inputs_dict in zip(solns, inputs_list) ] - def _post_process_solution(self, sol, model, integration_time, inputs_dict): + def _post_process_solution( + self, sol, model, integration_time, inputs_dict, save_adaptive_steps + ): number_of_sensitivity_parameters = self._setup[ "number_of_sensitivity_parameters" ] sensitivity_names = self._setup["sensitivity_names"] number_of_timesteps = sol.t.size number_of_states = model.len_rhs_and_alg - if self.output_variables: + save_outputs_only = self.output_variables + if save_outputs_only: # Substitute empty vectors for state vector 'y' y_out = np.zeros((number_of_timesteps * number_of_states, 0)) y_event = sol.y_term @@ -834,6 +847,15 @@ def _post_process_solution(self, sol, model, integration_time, inputs_dict): else: raise pybamm.SolverError(f"FAILURE {self._solver_flag(sol.flag)}") + if ( + self._options["hermite_interpolation"] + and save_adaptive_steps + and (not save_outputs_only) + ): + yp = sol.yp.reshape((number_of_timesteps, number_of_states)).T + else: + yp = None + newsol = pybamm.Solution( sol.t, np.transpose(y_out), @@ -843,10 +865,11 @@ def _post_process_solution(self, sol, model, integration_time, inputs_dict): np.transpose(y_event)[:, np.newaxis], termination, all_sensitivities=yS_out, + all_yps=yp, ) + newsol.integration_time = integration_time - if not self.output_variables: - # print((newsol.y).shape) + if not save_outputs_only: return newsol # Populate variables and sensititivies dictionaries directly diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 2464466348..6e39113347 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -46,9 +46,12 @@ def __init__( self.all_ts = solution.all_ts self.all_ys = solution.all_ys + self.all_yps = solution.all_yps self.all_inputs = solution.all_inputs self.all_inputs_casadi = solution.all_inputs_casadi + self.hermite_interpolation = solution.hermite_interpolation + self.mesh = base_variables[0].mesh self.domain = base_variables[0].domain self.domains = base_variables[0].domains @@ -76,45 +79,393 @@ def __init__( self.base_eval_size = self.base_variables[0].size # xr_data_array is initialized - self._xr_data_array = None + self._raw_data_initialized = False + self._xr_data_array_raw = None + self._entries_raw = None + self._entries_for_interp_raw = None + self._coords_raw = None + + def initialise(self, t=None): + t_observe, observe_raw = self._observe_raw_data(t) + if observe_raw and self._raw_data_initialized: + entries = self._entries_raw + is_interpolated = False + entries_for_interp = self._entries_for_interp_raw + coords = self._coords_raw + else: + entries, is_interpolated = self.observe(t_observe, observe_raw) + entries_for_interp, coords = self._interp_setup(entries, t_observe) + + if (not self._raw_data_initialized) and observe_raw: + self._entries_raw = entries + self._entries_for_interp_raw = entries_for_interp + self._coords_raw = coords + self._raw_data_initialized = True + + return t_observe, entries, is_interpolated, entries_for_interp, coords + + def observe(self, t, observe_raw): + """ + Evaluate the base variable at the given time points and y values. + """ + + is_sorted = observe_raw or _is_sorted(t) + if not is_sorted: + idxs_sort = np.argsort(t) + t = t[idxs_sort] + + if self.hermite_interpolation and not observe_raw: + pybamm.logger.debug( + "Observing and Hermite interpolating the variable in C++" + ) + entries = self._observe_hermite_cpp(t) + is_interpolated = True + elif self._linear_observable_cpp(t): + pybamm.logger.debug("Observing the variable raw data in C++") + entries = self._observe_raw_cpp() + is_interpolated = False + else: + pybamm.logger.debug("Observing the variable raw data in Python") + entries = self._observe_raw_python() + is_interpolated = False + + if not is_sorted: + idxs_unsort = np.arange(len(t))[idxs_sort] + + t = t[idxs_unsort] + entries = entries[..., idxs_unsort] + + entries = self._observe_postfix(entries, t) + return entries, is_interpolated + + def _setup_cpp_inputs(self): + pybamm.logger.debug("Setting up C++ interpolation inputs") + + ts = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(self.all_ts) + ys = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(self.all_ys) + if self.hermite_interpolation: + yps = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray( + self.all_yps + ) + else: + yps = None + + # Generate the serialized C++ functions only once + funcs_unique = {} + funcs = [None] * len(self.base_variables_casadi) + + for i, vars in enumerate(self.base_variables_casadi): + if vars not in funcs_unique: + funcs_unique[vars] = ( + pybamm.solvers.idaklu_solver.idaklu.generate_function( + vars.serialize() + ) + ) + funcs[i] = funcs_unique[vars] + + inputs = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray( + self.all_inputs_casadi + ) + + is_f_contiguous = _is_f_contiguous(self.all_ys) + + return ts, ys, yps, funcs, inputs, is_f_contiguous + + def _observe_hermite_cpp(self, t): + self._check_interp(t) + + ts, ys, yps, funcs, inputs, _ = self._setup_cpp_inputs() + sizes = self._size(t) + return pybamm.solvers.idaklu_solver.idaklu.observe_hermite_interp_ND( + t, ts, ys, yps, inputs, funcs, sizes + ) + + def _observe_raw_cpp(self): + ts, ys, _, funcs, inputs, is_f_contiguous = self._setup_cpp_inputs() + sizes = self._size(self.t_pts) + + return pybamm.solvers.idaklu_solver.idaklu.observe_ND( + ts, ys, inputs, funcs, is_f_contiguous, sizes + ) + + def _observe_raw_python(self): + pass # pragma: no cover + + def _observe_postfix(self, entries, t): + return entries + + def _interp_setup(self, entries, t): + pass # pragma: no cover + + def _size(self, t): + pass # pragma: no cover + + def _process_spatial_variable_names(self, spatial_variable): + if len(spatial_variable) == 0: + return None + + # Extract names + raw_names = [] + for var in spatial_variable: + # Ignore tabs in domain names + if var == "tabs": + continue + if isinstance(var, str): + raw_names.append(var) + else: + raw_names.append(var.name) + + # Rename battery variables to match PyBaMM convention + if all([var.startswith("r") for var in raw_names]): + return "r" + elif all([var.startswith("x") for var in raw_names]): + return "x" + elif all([var.startswith("R") for var in raw_names]): + return "R" + elif len(raw_names) == 1: + return raw_names[0] + else: + raise NotImplementedError( + f"Spatial variable name not recognized for {spatial_variable}" + ) - # handle 2D (in space) finite element variables differently - if ( - self.mesh - and "current collector" in self.domain - and isinstance(self.mesh, pybamm.ScikitSubMesh2D) + def _initialize_xr_data_array(self, entries, coords): + """ + Initialize the xarray DataArray for interpolation. We don't do this by + default as it has some overhead (~75 us) and sometimes we only need the entries + of the processed variable, not the xarray object for interpolation. + """ + return xr.DataArray(entries, coords=coords) + + def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): + t_observe, entries, is_interpolated, entries_for_interp, coords = ( + self.initialise(t) + ) + + post_interpolate = ( + (x is not None) + or (r is not None) + or (y is not None) + or (z is not None) + or (R is not None) + ) + + if is_interpolated and not post_interpolate: + return entries + return self._xr_interpolate( + t_observe, + entries_for_interp, + coords, + is_interpolated, + t, + x, + r, + y, + z, + R, + warn, + ) + + def _xr_interpolate( + self, + t_observe, + entries_for_interp, + coords, + is_interpolated, + t=None, + x=None, + r=None, + y=None, + z=None, + R=None, + warn=True, + ): + """ + Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), + using interpolation + """ + if is_interpolated: + xr_data_array = self._initialize_xr_data_array(entries_for_interp, coords) + else: + if self._xr_data_array_raw is None: + if not self._raw_data_initialized: + self.initialise(t=None) + self._xr_data_array_raw = self._initialize_xr_data_array( + self._entries_for_interp_raw, self._coords_raw + ) + xr_data_array = self._xr_data_array_raw + kwargs = {"t": t, "x": x, "r": r, "y": y, "z": z, "R": R} + if is_interpolated: + kwargs["t"] = None + # Remove any None arguments + kwargs = {key: value for key, value in kwargs.items() if value is not None} + # Use xarray interpolation, return numpy array + return xr_data_array.interp(**kwargs).values + + def _linear_observable_cpp(self, t): + """ + For a small number of time points, it is faster to evaluate the base variable in + Python. For large number of time points, it is faster to evaluate the base + variable in C++. + """ + return pybamm.has_idaklu() and (t is not None) and (np.asarray(t).size > 1) + + def _observe_raw_data(self, t): + observe_raw = (t is None) or ( + np.asarray(t).size == len(self.t_pts) and np.all(t == self.t_pts) + ) + + if observe_raw: + t_observe = self.t_pts + elif not isinstance(t, np.ndarray): + if not isinstance(t, list): + t = [t] + t_observe = np.array(t) + else: + t_observe = t + + return t_observe, observe_raw + + def _check_interp(self, t): + """ + Check if the time points are sorted and unique + + Args: + t (np.ndarray): array to check + + Returns: + bool: True if array is sorted and unique + """ + if t[0] < self.t_pts[0]: + raise ValueError( + "The interpolation points must be greater than or equal to the initial solution time" + ) + + @property + def entries(self): + """ + Returns the raw data entries of the processed variable. This is the data that + is used for interpolation. If the processed variable has not been initialized + (i.e. the entries have not been calculated), then the processed variable is + initialized first. + """ + if not self._raw_data_initialized: + self.initialise() + return self._entries_raw + + @property + def data(self): + """Same as entries, but different name""" + return self.entries + + @property + def sensitivities(self): + """ + Returns a dictionary of sensitivities for each input parameter. + The keys are the input parameters, and the value is a matrix of size + (n_x * n_t, n_p), where n_x is the number of states, n_t is the number of time + points, and n_p is the size of the input parameter + """ + # No sensitivities if there are no inputs + if len(self.all_inputs[0]) == 0: + return {} + # Otherwise initialise and return sensitivities + if self._sensitivities is None: + if self.all_solution_sensitivities: + self.initialise_sensitivity_explicit_forward() + else: + raise ValueError( + "Cannot compute sensitivities. The 'calculate_sensitivities' " + "argument of the solver.solve should be changed from 'None' to " + "allow sensitivities calculations. Check solver documentation for " + "details." + ) + return self._sensitivities + + def initialise_sensitivity_explicit_forward(self): + "Set up the sensitivity dictionary" + + all_S_var = [] + for ts, ys, inputs_stacked, inputs, base_variable, dy_dp in zip( + self.all_ts, + self.all_ys, + self.all_inputs_casadi, + self.all_inputs, + self.base_variables, + self.all_solution_sensitivities["all"], ): - return self.initialise_2D_scikit_fem() + # Set up symbolic variables + t_casadi = casadi.MX.sym("t") + y_casadi = casadi.MX.sym("y", ys.shape[0]) + p_casadi = { + name: casadi.MX.sym(name, value.shape[0]) + for name, value in inputs.items() + } - # check variable shape - if len(self.base_eval_shape) == 0 or self.base_eval_shape[0] == 1: - return self.initialise_0D() + p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) - n = self.mesh.npts - base_shape = self.base_eval_shape[0] - # Try some shapes that could make the variable a 1D variable - if base_shape in [n, n + 1]: - return self.initialise_1D() + # Convert variable to casadi format for differentiating + var_casadi = base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) + dvar_dy = casadi.jacobian(var_casadi, y_casadi) + dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) - # Try some shapes that could make the variable a 2D variable - first_dim_nodes = self.mesh.nodes - first_dim_edges = self.mesh.edges - second_dim_pts = self.base_variables[0].secondary_mesh.nodes - if self.base_eval_size // len(second_dim_pts) in [ - len(first_dim_nodes), - len(first_dim_edges), - ]: - return self.initialise_2D() - - # Raise error for 3D variable - raise NotImplementedError( - f"Shape not recognized for {base_variables[0]}" - + "(note processing of 3D variables is not yet implemented)" + # Convert to functions and evaluate index-by-index + dvar_dy_func = casadi.Function( + "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] + ) + dvar_dp_func = casadi.Function( + "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] + ) + for idx, t in enumerate(ts): + u = ys[:, idx] + next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) + next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) + if idx == 0: + dvar_dy_eval = next_dvar_dy_eval + dvar_dp_eval = next_dvar_dp_eval + else: + dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) + dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) + + # Compute sensitivity + S_var = dvar_dy_eval @ dy_dp + dvar_dp_eval + all_S_var.append(S_var) + + S_var = casadi.vertcat(*all_S_var) + sensitivities = {"all": S_var} + + # Add the individual sensitivity + start = 0 + for name, inp in self.all_inputs[0].items(): + end = start + inp.shape[0] + sensitivities[name] = S_var[:, start:end] + start = end + + # Save attribute + self._sensitivities = sensitivities + + +class ProcessedVariable0D(ProcessedVariable): + def __init__( + self, + base_variables, + base_variables_casadi, + solution, + warn=True, + cumtrapz_ic=None, + ): + self.dimensions = 0 + super().__init__( + base_variables, + base_variables_casadi, + solution, + warn=warn, + cumtrapz_ic=cumtrapz_ic, ) - def initialise_0D(self): + def _observe_raw_python(self): # initialise empty array of the correct size - entries = np.empty(len(self.t_pts)) + entries = np.empty(self._size(self.t_pts)) idx = 0 # Evaluate the base_variable index-by-index for ts, ys, inputs, base_var_casadi in zip( @@ -126,22 +477,71 @@ def initialise_0D(self): entries[idx] = float(base_var_casadi(t, y, inputs)) idx += 1 + return entries - if self.cumtrapz_ic is not None: - entries = cumulative_trapezoid( - entries, self.t_pts, initial=float(self.cumtrapz_ic) - ) + def _observe_postfix(self, entries, _): + if self.cumtrapz_ic is None: + return entries + return cumulative_trapezoid( + entries, self.t_pts, initial=float(self.cumtrapz_ic) + ) + + def _interp_setup(self, entries, t): # save attributes for interpolation - self.entries_for_interp = entries - self.coords_for_interp = {"t": self.t_pts} + entries_for_interp = entries + coords_for_interp = {"t": t} - self.entries = entries - self.dimensions = 0 + return entries_for_interp, coords_for_interp + + def _size(self, t): + return [len(t)] + + +class ProcessedVariable1D(ProcessedVariable): + """ + An object that can be evaluated at arbitrary (scalars or vectors) t and x, and + returns the (interpolated) value of the base variable at that t and x. + + Parameters + ---------- + base_variables : list of :class:`pybamm.Symbol` + A list of base variables with a method `evaluate(t,y)`, each entry of which + returns the value of that variable for that particular sub-solution. + A Solution can be comprised of sub-solutions which are the solutions of + different models. + Note that this can be any kind of node in the expression tree, not + just a :class:`pybamm.Variable`. + When evaluated, returns an array of size (m,n) + base_variable_casadis : list of :class:`casadi.Function` + A list of casadi functions. When evaluated, returns the same thing as + `base_Variable.evaluate` (but more efficiently). + solution : :class:`pybamm.Solution` + The solution object to be used to create the processed variables + warn : bool, optional + Whether to raise warnings when trying to evaluate time and length scales. + Default is True. + """ + + def __init__( + self, + base_variables, + base_variables_casadi, + solution, + warn=True, + cumtrapz_ic=None, + ): + self.dimensions = 1 + super().__init__( + base_variables, + base_variables_casadi, + solution, + warn=warn, + cumtrapz_ic=cumtrapz_ic, + ) - def initialise_1D(self, fixed_t=False): - len_space = self.base_eval_shape[0] - entries = np.empty((len_space, len(self.t_pts))) + def _observe_raw_python(self): + entries = np.empty(self._size(self.t_pts)) # Evaluate the base_variable index-by-index idx = 0 @@ -153,7 +553,9 @@ def initialise_1D(self, fixed_t=False): y = ys[:, inner_idx] entries[:, idx] = base_var_casadi(t, y, inputs).full()[:, 0] idx += 1 + return entries + def _interp_setup(self, entries, t): # Get node and edge values nodes = self.mesh.nodes edges = self.mesh.edges @@ -173,8 +575,6 @@ def initialise_1D(self, fixed_t=False): ) # assign attributes for reference (either x_sol or r_sol) - self.entries = entries - self.dimensions = 1 self.spatial_variable_names = { k: self._process_spatial_variable_names(v) for k, v in self.spatial_variables.items() @@ -189,26 +589,75 @@ def initialise_1D(self, fixed_t=False): self.first_dim_pts = edges # save attributes for interpolation - self.entries_for_interp = entries_for_interp - self.coords_for_interp = {self.first_dimension: pts_for_interp, "t": self.t_pts} + coords_for_interp = {self.first_dimension: pts_for_interp, "t": t} - def initialise_2D(self): - """ - Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. - """ + return entries_for_interp, coords_for_interp + + def _size(self, t): + t_size = len(t) + space_size = self.base_eval_shape[0] + return [space_size, t_size] + + +class ProcessedVariable2D(ProcessedVariable): + """ + An object that can be evaluated at arbitrary (scalars or vectors) t and x, and + returns the (interpolated) value of the base variable at that t and x. + + Parameters + ---------- + base_variables : list of :class:`pybamm.Symbol` + A list of base variables with a method `evaluate(t,y)`, each entry of which + returns the value of that variable for that particular sub-solution. + A Solution can be comprised of sub-solutions which are the solutions of + different models. + Note that this can be any kind of node in the expression tree, not + just a :class:`pybamm.Variable`. + When evaluated, returns an array of size (m,n) + base_variable_casadis : list of :class:`casadi.Function` + A list of casadi functions. When evaluated, returns the same thing as + `base_Variable.evaluate` (but more efficiently). + solution : :class:`pybamm.Solution` + The solution object to be used to create the processed variables + warn : bool, optional + Whether to raise warnings when trying to evaluate time and length scales. + Default is True. + """ + + def __init__( + self, + base_variables, + base_variables_casadi, + solution, + warn=True, + cumtrapz_ic=None, + ): + self.dimensions = 2 + super().__init__( + base_variables, + base_variables_casadi, + solution, + warn=warn, + cumtrapz_ic=cumtrapz_ic, + ) first_dim_nodes = self.mesh.nodes first_dim_edges = self.mesh.edges second_dim_nodes = self.base_variables[0].secondary_mesh.nodes - second_dim_edges = self.base_variables[0].secondary_mesh.edges if self.base_eval_size // len(second_dim_nodes) == len(first_dim_nodes): first_dim_pts = first_dim_nodes elif self.base_eval_size // len(second_dim_nodes) == len(first_dim_edges): first_dim_pts = first_dim_edges second_dim_pts = second_dim_nodes - first_dim_size = len(first_dim_pts) - second_dim_size = len(second_dim_pts) - entries = np.empty((first_dim_size, second_dim_size, len(self.t_pts))) + self.first_dim_size = len(first_dim_pts) + self.second_dim_size = len(second_dim_pts) + + def _observe_raw_python(self): + """ + Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. + """ + first_dim_size, second_dim_size, t_size = self._size(self.t_pts) + entries = np.empty((first_dim_size, second_dim_size, t_size)) # Evaluate the base_variable index-by-index idx = 0 @@ -224,6 +673,22 @@ def initialise_2D(self): order="F", ) idx += 1 + return entries + + def _interp_setup(self, entries, t): + """ + Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. + """ + first_dim_nodes = self.mesh.nodes + first_dim_edges = self.mesh.edges + second_dim_nodes = self.base_variables[0].secondary_mesh.nodes + second_dim_edges = self.base_variables[0].secondary_mesh.edges + if self.base_eval_size // len(second_dim_nodes) == len(first_dim_nodes): + first_dim_pts = first_dim_nodes + elif self.base_eval_size // len(second_dim_nodes) == len(first_dim_edges): + first_dim_pts = first_dim_edges + + second_dim_pts = second_dim_nodes # add points outside first dimension domain for extrapolation to # boundaries @@ -281,8 +746,6 @@ def initialise_2D(self): self.second_dimension = self.spatial_variable_names["secondary"] # assign attributes for reference - self.entries = entries - self.dimensions = 2 first_dim_pts_for_interp = first_dim_pts second_dim_pts_for_interp = second_dim_pts @@ -291,38 +754,73 @@ def initialise_2D(self): self.second_dim_pts = second_dim_edges # save attributes for interpolation - self.entries_for_interp = entries_for_interp - self.coords_for_interp = { + coords_for_interp = { self.first_dimension: first_dim_pts_for_interp, self.second_dimension: second_dim_pts_for_interp, - "t": self.t_pts, + "t": t, } - def initialise_2D_scikit_fem(self): + return entries_for_interp, coords_for_interp + + def _size(self, t): + first_dim_size = self.first_dim_size + second_dim_size = self.second_dim_size + t_size = len(t) + return [first_dim_size, second_dim_size, t_size] + + +class ProcessedVariable2DSciKitFEM(ProcessedVariable2D): + """ + An object that can be evaluated at arbitrary (scalars or vectors) t and x, and + returns the (interpolated) value of the base variable at that t and x. + + Parameters + ---------- + base_variables : list of :class:`pybamm.Symbol` + A list of base variables with a method `evaluate(t,y)`, each entry of which + returns the value of that variable for that particular sub-solution. + A Solution can be comprised of sub-solutions which are the solutions of + different models. + Note that this can be any kind of node in the expression tree, not + just a :class:`pybamm.Variable`. + When evaluated, returns an array of size (m,n) + base_variable_casadis : list of :class:`casadi.Function` + A list of casadi functions. When evaluated, returns the same thing as + `base_Variable.evaluate` (but more efficiently). + solution : :class:`pybamm.Solution` + The solution object to be used to create the processed variables + warn : bool, optional + Whether to raise warnings when trying to evaluate time and length scales. + Default is True. + """ + + def __init__( + self, + base_variables, + base_variables_casadi, + solution, + warn=True, + cumtrapz_ic=None, + ): + self.dimensions = 2 + super(ProcessedVariable2D, self).__init__( + base_variables, + base_variables_casadi, + solution, + warn=warn, + cumtrapz_ic=cumtrapz_ic, + ) y_sol = self.mesh.edges["y"] - len_y = len(y_sol) z_sol = self.mesh.edges["z"] - len_z = len(z_sol) - entries = np.empty((len_y, len_z, len(self.t_pts))) - # Evaluate the base_variable index-by-index - idx = 0 - for ts, ys, inputs, base_var_casadi in zip( - self.all_ts, self.all_ys, self.all_inputs_casadi, self.base_variables_casadi - ): - for inner_idx, t in enumerate(ts): - t = ts[inner_idx] - y = ys[:, inner_idx] - entries[:, :, idx] = np.reshape( - base_var_casadi(t, y, inputs).full(), - [len_y, len_z], - order="C", - ) - idx += 1 + self.first_dim_size = len(y_sol) + self.second_dim_size = len(z_sol) + + def _interp_setup(self, entries, t): + y_sol = self.mesh.edges["y"] + z_sol = self.mesh.edges["z"] # assign attributes for reference - self.entries = entries - self.dimensions = 2 self.y_sol = y_sol self.z_sol = z_sol self.first_dimension = "y" @@ -331,148 +829,76 @@ def initialise_2D_scikit_fem(self): self.second_dim_pts = z_sol # save attributes for interpolation - self.entries_for_interp = entries - self.coords_for_interp = {"y": y_sol, "z": z_sol, "t": self.t_pts} + coords_for_interp = {"y": y_sol, "z": z_sol, "t": t} - def _process_spatial_variable_names(self, spatial_variable): - if len(spatial_variable) == 0: - return None + return entries, coords_for_interp - # Extract names - raw_names = [] - for var in spatial_variable: - # Ignore tabs in domain names - if var == "tabs": - continue - if isinstance(var, str): - raw_names.append(var) - else: - raw_names.append(var.name) - # Rename battery variables to match PyBaMM convention - if all([var.startswith("r") for var in raw_names]): - return "r" - elif all([var.startswith("x") for var in raw_names]): - return "x" - elif all([var.startswith("R") for var in raw_names]): - return "R" - elif len(raw_names) == 1: - return raw_names[0] - else: - raise NotImplementedError( - f"Spatial variable name not recognized for {spatial_variable}" - ) - - def _initialize_xr_data_array(self): - """ - Initialize the xarray DataArray for interpolation. We don't do this by - default as it has some overhead (~75 us) and sometimes we only need the entries - of the processed variable, not the xarray object for interpolation. - """ - entries = self.entries_for_interp - coords = self.coords_for_interp - self._xr_data_array = xr.DataArray(entries, coords=coords) - - def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): - """ - Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), - using interpolation - """ - if self._xr_data_array is None: - self._initialize_xr_data_array() - kwargs = {"t": t, "x": x, "r": r, "y": y, "z": z, "R": R} - # Remove any None arguments - kwargs = {key: value for key, value in kwargs.items() if value is not None} - # Use xarray interpolation, return numpy array - return self._xr_data_array.interp(**kwargs).values - - @property - def data(self): - """Same as entries, but different name""" - return self.entries - - @property - def sensitivities(self): - """ - Returns a dictionary of sensitivities for each input parameter. - The keys are the input parameters, and the value is a matrix of size - (n_x * n_t, n_p), where n_x is the number of states, n_t is the number of time - points, and n_p is the size of the input parameter - """ - # No sensitivities if there are no inputs - if len(self.all_inputs[0]) == 0: - return {} - # Otherwise initialise and return sensitivities - if self._sensitivities is None: - if self.all_solution_sensitivities: - self.initialise_sensitivity_explicit_forward() - else: - raise ValueError( - "Cannot compute sensitivities. The 'calculate_sensitivities' " - "argument of the solver.solve should be changed from 'None' to " - "allow sensitivities calculations. Check solver documentation for " - "details." - ) - return self._sensitivities +def process_variable(base_variables, *args, **kwargs): + mesh = base_variables[0].mesh + domain = base_variables[0].domain - def initialise_sensitivity_explicit_forward(self): - "Set up the sensitivity dictionary" + # Evaluate base variable at initial time + base_eval_shape = base_variables[0].shape + base_eval_size = base_variables[0].size - all_S_var = [] - for ts, ys, inputs_stacked, inputs, base_variable, dy_dp in zip( - self.all_ts, - self.all_ys, - self.all_inputs_casadi, - self.all_inputs, - self.base_variables, - self.all_solution_sensitivities["all"], - ): - # Set up symbolic variables - t_casadi = casadi.MX.sym("t") - y_casadi = casadi.MX.sym("y", ys.shape[0]) - p_casadi = { - name: casadi.MX.sym(name, value.shape[0]) - for name, value in inputs.items() - } + # handle 2D (in space) finite element variables differently + if ( + mesh + and "current collector" in domain + and isinstance(mesh, pybamm.ScikitSubMesh2D) + ): + return ProcessedVariable2DSciKitFEM(base_variables, *args, **kwargs) + + # check variable shape + if len(base_eval_shape) == 0 or base_eval_shape[0] == 1: + return ProcessedVariable0D(base_variables, *args, **kwargs) + + n = mesh.npts + base_shape = base_eval_shape[0] + # Try some shapes that could make the variable a 1D variable + if base_shape in [n, n + 1]: + return ProcessedVariable1D(base_variables, *args, **kwargs) + + # Try some shapes that could make the variable a 2D variable + first_dim_nodes = mesh.nodes + first_dim_edges = mesh.edges + second_dim_pts = base_variables[0].secondary_mesh.nodes + if base_eval_size // len(second_dim_pts) in [ + len(first_dim_nodes), + len(first_dim_edges), + ]: + return ProcessedVariable2D(base_variables, *args, **kwargs) + + # Raise error for 3D variable + raise NotImplementedError( + f"Shape not recognized for {base_variables[0]}" + + "(note processing of 3D variables is not yet implemented)" + ) + + +def _is_f_contiguous(all_ys): + """ + Check if all the ys are f-contiguous in memory - p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) + Args: + all_ys (list of np.ndarray): list of all ys - # Convert variable to casadi format for differentiating - var_casadi = base_variable.to_casadi(t_casadi, y_casadi, inputs=p_casadi) - dvar_dy = casadi.jacobian(var_casadi, y_casadi) - dvar_dp = casadi.jacobian(var_casadi, p_casadi_stacked) + Returns: + bool: True if all ys are f-contiguous + """ - # Convert to functions and evaluate index-by-index - dvar_dy_func = casadi.Function( - "dvar_dy", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dy] - ) - dvar_dp_func = casadi.Function( - "dvar_dp", [t_casadi, y_casadi, p_casadi_stacked], [dvar_dp] - ) - for idx, t in enumerate(ts): - u = ys[:, idx] - next_dvar_dy_eval = dvar_dy_func(t, u, inputs_stacked) - next_dvar_dp_eval = dvar_dp_func(t, u, inputs_stacked) - if idx == 0: - dvar_dy_eval = next_dvar_dy_eval - dvar_dp_eval = next_dvar_dp_eval - else: - dvar_dy_eval = casadi.diagcat(dvar_dy_eval, next_dvar_dy_eval) - dvar_dp_eval = casadi.vertcat(dvar_dp_eval, next_dvar_dp_eval) + return all(isinstance(y, np.ndarray) and y.data.f_contiguous for y in all_ys) - # Compute sensitivity - S_var = dvar_dy_eval @ dy_dp + dvar_dp_eval - all_S_var.append(S_var) - S_var = casadi.vertcat(*all_S_var) - sensitivities = {"all": S_var} +def _is_sorted(t): + """ + Check if an array is sorted - # Add the individual sensitivity - start = 0 - for name, inp in self.all_inputs[0].items(): - end = start + inp.shape[0] - sensitivities[name] = S_var[:, start:end] - start = end + Args: + t (np.ndarray): array to check - # Save attribute - self._sensitivities = sensitivities + Returns: + bool: True if array is sorted + """ + return np.all(t[:-1] <= t[1:]) diff --git a/src/pybamm/solvers/processed_variable_computed.py b/src/pybamm/solvers/processed_variable_computed.py index a717c8b0cb..d025a90908 100644 --- a/src/pybamm/solvers/processed_variable_computed.py +++ b/src/pybamm/solvers/processed_variable_computed.py @@ -82,33 +82,35 @@ def __init__( and isinstance(self.mesh, pybamm.ScikitSubMesh2D) ): self.initialise_2D_scikit_fem() + return # check variable shape - else: - if len(self.base_eval_shape) == 0 or self.base_eval_shape[0] == 1: - self.initialise_0D() - else: - n = self.mesh.npts - base_shape = self.base_eval_shape[0] - # Try some shapes that could make the variable a 1D variable - if base_shape in [n, n + 1]: - self.initialise_1D() - else: - # Try some shapes that could make the variable a 2D variable - first_dim_nodes = self.mesh.nodes - first_dim_edges = self.mesh.edges - second_dim_pts = self.base_variables[0].secondary_mesh.nodes - if self.base_eval_size // len(second_dim_pts) in [ - len(first_dim_nodes), - len(first_dim_edges), - ]: - self.initialise_2D() - else: - # Raise error for 3D variable - raise NotImplementedError( - f"Shape not recognized for {base_variables[0]} " - + "(note processing of 3D variables is not yet implemented)" - ) + if len(self.base_eval_shape) == 0 or self.base_eval_shape[0] == 1: + self.initialise_0D() + return + + n = self.mesh.npts + base_shape = self.base_eval_shape[0] + # Try some shapes that could make the variable a 1D variable + if base_shape in [n, n + 1]: + self.initialise_1D() + return + + # Try some shapes that could make the variable a 2D variable + first_dim_nodes = self.mesh.nodes + first_dim_edges = self.mesh.edges + second_dim_pts = self.base_variables[0].secondary_mesh.nodes + if self.base_eval_size // len(second_dim_pts) not in [ + len(first_dim_nodes), + len(first_dim_edges), + ]: + # Raise error for 3D variable + raise NotImplementedError( + f"Shape not recognized for {base_variables[0]} " + + "(note processing of 3D variables is not yet implemented)" + ) + + self.initialise_2D() def add_sensitivity(self, param, data): # unroll from sparse representation into n-d matrix @@ -203,7 +205,7 @@ def initialise_0D(self): self.entries = entries self.dimensions = 0 - def initialise_1D(self, fixed_t=False): + def initialise_1D(self): entries = self.unroll_1D() # Get node and edge values diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index 74d9ce7baf..0512a94409 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -75,6 +75,7 @@ def __init__( y_event=None, termination="final time", all_sensitivities=False, + all_yps=None, check_solution=True, ): if not isinstance(all_ts, list): @@ -88,6 +89,11 @@ def __init__( self._all_ys_and_sens = all_ys self._all_models = all_models + self._hermite_interpolation = all_yps is not None + if self.hermite_interpolation and not isinstance(all_yps, list): + all_yps = [all_yps] + self._all_yps = all_yps + # Set up inputs if not isinstance(all_inputs, list): all_inputs_copy = dict(all_inputs) @@ -128,7 +134,7 @@ def __init__( # initialize empty variables and data self._variables = pybamm.FuzzyDict() - self.data = pybamm.FuzzyDict() + self._data = pybamm.FuzzyDict() # Add self as sub-solution for compatibility with ProcessedVariable self._sub_solutions = [self] @@ -298,6 +304,13 @@ def y(self): return self._y + @property + def data(self): + for k, v in self._variables.items(): + if k not in self._data: + self._data[k] = v.data + return self._data + @property def sensitivities(self): """Values of the sensitivities. Returns a dict of param_name: np_array""" @@ -400,6 +413,14 @@ def all_models(self): def all_inputs_casadi(self): return [casadi.vertcat(*inp.values()) for inp in self.all_inputs] + @property + def all_yps(self): + return self._all_yps + + @property + def hermite_interpolation(self): + return self._hermite_interpolation + @property def t_event(self): """Time at which the event happens""" @@ -434,6 +455,12 @@ def first_state(self): n_states = self.all_models[0].len_rhs_and_alg for key in self._all_sensitivities: sensitivities[key] = self._all_sensitivities[key][0][-n_states:, :] + + if self.all_yps is None: + all_yps = None + else: + all_yps = self.all_yps[0][:, :1] + new_sol = Solution( self.all_ts[0][:1], self.all_ys[0][:, :1], @@ -443,6 +470,7 @@ def first_state(self): None, "final time", all_sensitivities=sensitivities, + all_yps=all_yps, ) new_sol._all_inputs_casadi = self.all_inputs_casadi[:1] new_sol._sub_solutions = self.sub_solutions[:1] @@ -467,6 +495,12 @@ def last_state(self): n_states = self.all_models[-1].len_rhs_and_alg for key in self._all_sensitivities: sensitivities[key] = self._all_sensitivities[key][-1][-n_states:, :] + + if self.all_yps is None: + all_yps = None + else: + all_yps = self.all_yps[-1][:, -1:] + new_sol = Solution( self.all_ts[-1][-1:], self.all_ys[-1][:, -1:], @@ -476,6 +510,7 @@ def last_state(self): self.y_event, self.termination, all_sensitivities=sensitivities, + all_yps=all_yps, ) new_sol._all_inputs_casadi = self.all_inputs_casadi[-1:] new_sol._sub_solutions = self.sub_solutions[-1:] @@ -528,57 +563,63 @@ def update(self, variables): if isinstance(self._all_sensitivities, bool) and self._all_sensitivities: self.extract_explicit_sensitivities() - # Convert single entry to list + # Single variable if isinstance(variables, str): - variables = [variables] + self._update_variable(variables) + return + # Process - for key in variables: - cumtrapz_ic = None - pybamm.logger.debug(f"Post-processing {key}") - vars_pybamm = [model.variables_and_events[key] for model in self.all_models] - - # Iterate through all models, some may be in the list several times and - # therefore only get set up once - vars_casadi = [] - for i, (model, ys, inputs, var_pybamm) in enumerate( - zip(self.all_models, self.all_ys, self.all_inputs, vars_pybamm) + for variable in variables: + self._update_variable(variable) + + def _update_variable(self, variable): + cumtrapz_ic = None + pybamm.logger.debug(f"Post-processing {variable}") + vars_pybamm = [ + model.variables_and_events[variable] for model in self.all_models + ] + + # Iterate through all models, some may be in the list several times and + # therefore only get set up once + vars_casadi = [] + for i, (model, ys, inputs, var_pybamm) in enumerate( + zip(self.all_models, self.all_ys, self.all_inputs, vars_pybamm) + ): + if ys.size == 0 and var_pybamm.has_symbol_of_classes( + pybamm.expression_tree.state_vector.StateVector ): - if ys.size == 0 and var_pybamm.has_symbol_of_classes( - pybamm.expression_tree.state_vector.StateVector - ): - raise KeyError( - f"Cannot process variable '{key}' as it was not part of the " - "solve. Please re-run the solve with `output_variables` set to " - "include this variable." - ) - elif isinstance(var_pybamm, pybamm.ExplicitTimeIntegral): - cumtrapz_ic = var_pybamm.initial_condition - cumtrapz_ic = cumtrapz_ic.evaluate() - var_pybamm = var_pybamm.child - var_casadi = self.process_casadi_var( - var_pybamm, - inputs, - ys.shape, - ) - model._variables_casadi[key] = var_casadi - vars_pybamm[i] = var_pybamm - elif key in model._variables_casadi: - var_casadi = model._variables_casadi[key] - else: - var_casadi = self.process_casadi_var( - var_pybamm, - inputs, - ys.shape, - ) - model._variables_casadi[key] = var_casadi - vars_casadi.append(var_casadi) - var = pybamm.ProcessedVariable( - vars_pybamm, vars_casadi, self, cumtrapz_ic=cumtrapz_ic - ) + raise KeyError( + f"Cannot process variable '{variable}' as it was not part of the " + "solve. Please re-run the solve with `output_variables` set to " + "include this variable." + ) + elif isinstance(var_pybamm, pybamm.ExplicitTimeIntegral): + cumtrapz_ic = var_pybamm.initial_condition + cumtrapz_ic = cumtrapz_ic.evaluate() + var_pybamm = var_pybamm.child + var_casadi = self.process_casadi_var( + var_pybamm, + inputs, + ys.shape, + ) + model._variables_casadi[variable] = var_casadi + vars_pybamm[i] = var_pybamm + elif variable in model._variables_casadi: + var_casadi = model._variables_casadi[variable] + else: + var_casadi = self.process_casadi_var( + var_pybamm, + inputs, + ys.shape, + ) + model._variables_casadi[variable] = var_casadi + vars_casadi.append(var_casadi) + var = pybamm.process_variable( + vars_pybamm, vars_casadi, self, cumtrapz_ic=cumtrapz_ic + ) - # Save variable and data - self._variables[key] = var - self.data[key] = var.data + # Save variable + self._variables[variable] = var def process_casadi_var(self, var_pybamm, inputs, ys_shape): t_MX = casadi.MX.sym("t") @@ -588,7 +629,27 @@ def process_casadi_var(self, var_pybamm, inputs, ys_shape): } inputs_MX = casadi.vertcat(*[p for p in inputs_MX_dict.values()]) var_sym = var_pybamm.to_casadi(t_MX, y_MX, inputs=inputs_MX_dict) - var_casadi = casadi.Function("variable", [t_MX, y_MX, inputs_MX], [var_sym]) + + opts = { + "inputs_check": False, + "is_diff_in": [False, False, False], + "is_diff_out": [False], + "regularity_check": False, + "error_on_fail": False, + "enable_jacobian": False, + } + + epsilon = 5e-324 + var_sym = var_sym - epsilon + var_sym = var_sym + epsilon + + var_casadi = casadi.Function( + "variable", + [t_MX, y_MX, inputs_MX], + [var_sym], + opts, + ) + return var_casadi def __getitem__(self, key): @@ -822,9 +883,16 @@ def __add__(self, other): # Skip first time step if it is repeated all_ts = self.all_ts + [other.all_ts[0][1:]] + other.all_ts[1:] all_ys = self.all_ys + [other.all_ys[0][:, 1:]] + other.all_ys[1:] + if self.hermite_interpolation: + all_yps = self.all_yps + [other.all_yps[0][:, 1:]] + other.all_yps[1:] else: all_ts = self.all_ts + other.all_ts all_ys = self.all_ys + other.all_ys + if self.hermite_interpolation: + all_yps = self.all_yps + other.all_yps + + if not self.hermite_interpolation: + all_yps = None # sensitivities can be: # - bool if not using sensitivities or using explicit sensitivities which still @@ -859,6 +927,7 @@ def __add__(self, other): other.y_event, other.termination, all_sensitivities=all_sensitivities, + all_yps=all_yps, ) new_sol.closest_event_idx = other.closest_event_idx @@ -891,6 +960,7 @@ def copy(self): self.y_event, self.termination, self._all_sensitivities, + self.all_yps, ) new_sol._all_inputs_casadi = self.all_inputs_casadi new_sol._sub_solutions = self.sub_solutions @@ -1001,6 +1071,7 @@ def make_cycle_solution( sum_sols.y_event, sum_sols.termination, sum_sols._all_sensitivities, + sum_sols.all_yps, ) cycle_solution._all_inputs_casadi = sum_sols.all_inputs_casadi cycle_solution._sub_solutions = sum_sols.sub_solutions diff --git a/tests/integration/test_models/standard_output_comparison.py b/tests/integration/test_models/standard_output_comparison.py index 4d4d16e5ca..6c56894314 100644 --- a/tests/integration/test_models/standard_output_comparison.py +++ b/tests/integration/test_models/standard_output_comparison.py @@ -66,6 +66,7 @@ def compare(self, var, atol=0, rtol=0.02): # Get variable for each model model_variables = [solution[var] for solution in self.solutions] var0 = model_variables[0] + var0.initialise() spatial_pts = {} if var0.dimensions >= 1: diff --git a/tests/unit/test_plotting/test_quick_plot.py b/tests/unit/test_plotting/test_quick_plot.py index d5d994117d..6f99266862 100644 --- a/tests/unit/test_plotting/test_quick_plot.py +++ b/tests/unit/test_plotting/test_quick_plot.py @@ -60,8 +60,20 @@ def test_simple_ode_model(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) solver = model.default_solver - t_eval = np.linspace(0, 2, 100) - solution = solver.solve(model, t_eval) + t_eval = np.linspace(0, 2, 201) + if solver.supports_interp: + t_interp = t_eval + else: + t_interp = None + + solution = solver.solve(model, t_eval, t_interp=t_interp) + + if solution.hermite_interpolation: + t_linspace = np.linspace(t_eval[0], t_eval[-1], 100) + t_plot = np.union1d(t_eval, t_linspace) + else: + t_plot = t_eval + quick_plot = pybamm.QuickPlot( solution, [ @@ -149,28 +161,28 @@ def test_simple_ode_model(self): quick_plot.plot(0) assert quick_plot.time_scaling_factor == 1 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="minutes") quick_plot.plot(0) assert quick_plot.time_scaling_factor == 60 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 60 + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot / 60 ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="hours") quick_plot.plot(0) assert quick_plot.time_scaling_factor == 3600 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 3600 + quick_plot.plots[("a",)][0][0].get_xdata(), t_plot / 3600 ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot ) with pytest.raises(ValueError, match="time unit"): pybamm.QuickPlot(solution, ["a"], time_unit="bad unit") diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index b049729ae3..ac7efc89e6 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -43,9 +43,8 @@ def test_ida_roberts_klu(self): ) # Test - t_eval = np.linspace(0, 3, 100) - t_interp = t_eval - solution = solver.solve(model, t_eval, t_interp=t_interp) + t_eval = [0, 3] + solution = solver.solve(model, t_eval) # test that final time is time of event # y = 0.1 t + y0 so y=0.2 when t=2 @@ -311,8 +310,7 @@ def test_sensitivities_initial_condition(self): options={"jax_evaluator": "iree"} if form == "iree" else {}, ) - t_interp = np.linspace(0, 3, 100) - t_eval = [t_interp[0], t_interp[-1]] + t_eval = [0, 3] a_value = 0.1 @@ -321,7 +319,6 @@ def test_sensitivities_initial_condition(self): t_eval, inputs={"a": a_value}, calculate_sensitivities=True, - t_interp=t_interp, ) np.testing.assert_array_almost_equal( @@ -638,7 +635,7 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() t_eval = [0, 3] - with pytest.raises(pybamm.SolverError, match="FAILURE IDA"): + with pytest.raises(ValueError, match="std::exception"): solver.solve(model, t_eval) def test_dae_solver_algebraic_model(self): @@ -944,7 +941,7 @@ def construct_model(): # Mock a 1D current collector and initialise (none in the model) sol["x_s [m]"].domain = ["current collector"] - sol["x_s [m]"].initialise_1D() + sol["x_s [m]"].entries def test_with_output_variables_and_sensitivities(self): # Construct a model and solve for all variables, then test @@ -1040,7 +1037,7 @@ def test_with_output_variables_and_sensitivities(self): # Mock a 1D current collector and initialise (none in the model) sol["x_s [m]"].domain = ["current collector"] - sol["x_s [m]"].initialise_1D() + sol["x_s [m]"].entries def test_bad_jax_evaluator(self): model = pybamm.lithium_ion.DFN() diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 6cd456347d..33c561b134 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -48,7 +48,7 @@ def process_and_check_2D_variable( var_casadi = to_casadi(var_sol, y_sol) model = tests.get_base_model_with_battery_geometry(**geometry_options) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution(t_sol, y_sol, model, {}), @@ -71,7 +71,7 @@ def test_processed_variable_0D(self): t_sol = np.linspace(0, 1) y_sol = np.array([np.linspace(0, 5)]) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), @@ -85,7 +85,7 @@ def test_processed_variable_0D(self): t_sol = np.array([0]) y_sol = np.array([1])[:, np.newaxis] var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), @@ -104,7 +104,7 @@ def test_processed_variable_0D_no_sensitivity(self): t_sol = np.linspace(0, 1) y_sol = np.array([np.linspace(0, 5)]) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), @@ -124,7 +124,7 @@ def test_processed_variable_0D_no_sensitivity(self): y_sol = np.array([np.linspace(0, 5)]) inputs = {"a": np.array([1.0])} var_casadi = to_casadi(var, y_sol, inputs=inputs) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), inputs), @@ -151,7 +151,7 @@ def test_processed_variable_1D(self): y_sol = np.ones_like(x_sol)[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -162,7 +162,7 @@ def test_processed_variable_1D(self): np.testing.assert_array_equal(processed_var.entries, y_sol) np.testing.assert_array_almost_equal(processed_var(t_sol, x_sol), y_sol) eqn_casadi = to_casadi(eqn_sol, y_sol) - processed_eqn = pybamm.ProcessedVariable( + processed_eqn = pybamm.process_variable( [eqn_sol], [eqn_casadi], pybamm.Solution( @@ -184,7 +184,7 @@ def test_processed_variable_1D(self): x_s_edge = pybamm.Matrix(disc.mesh["separator"].edges, domain="separator") x_s_edge.mesh = disc.mesh["separator"] x_s_casadi = to_casadi(x_s_edge, y_sol) - processed_x_s_edge = pybamm.ProcessedVariable( + processed_x_s_edge = pybamm.process_variable( [x_s_edge], [x_s_casadi], pybamm.Solution( @@ -202,7 +202,7 @@ def test_processed_variable_1D(self): t_sol = np.array([0]) y_sol = np.ones_like(x_sol)[:, np.newaxis] eqn_casadi = to_casadi(eqn_sol, y_sol) - processed_eqn2 = pybamm.ProcessedVariable( + processed_eqn2 = pybamm.process_variable( [eqn_sol], [eqn_casadi], pybamm.Solution( @@ -242,7 +242,7 @@ def test_processed_variable_1D_unknown_domain(self): c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) c.mesh = mesh["SEI layer"] c_casadi = to_casadi(c, y_sol) - pybamm.ProcessedVariable([c], [c_casadi], solution, warn=False) + pybamm.process_variable([c], [c_casadi], solution, warn=False) def test_processed_variable_2D_x_r(self): var = pybamm.Variable( @@ -352,7 +352,7 @@ def test_processed_variable_2D_x_z(self): x_s_edge.mesh = disc.mesh["separator"] x_s_edge.secondary_mesh = disc.mesh["current collector"] x_s_casadi = to_casadi(x_s_edge, y_sol) - processed_x_s_edge = pybamm.ProcessedVariable( + processed_x_s_edge = pybamm.process_variable( [x_s_edge], [x_s_casadi], pybamm.Solution( @@ -388,7 +388,7 @@ def test_processed_variable_2D_space_only(self): y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -414,7 +414,7 @@ def test_processed_variable_2D_scikit(self): u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, u_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -442,7 +442,7 @@ def test_processed_variable_2D_fixed_t_scikit(self): model = tests.get_base_model_with_battery_geometry( options={"dimensionality": 2} ) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution(t_sol, u_sol, model, {}), @@ -464,7 +464,7 @@ def test_processed_var_0D_interpolation(self): t_sol = np.linspace(0, 1, 1000) y_sol = np.array([np.linspace(0, 5, 1000)]) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], pybamm.Solution( @@ -479,7 +479,7 @@ def test_processed_var_0D_interpolation(self): np.testing.assert_array_equal(processed_var(0.7), 3.5) eqn_casadi = to_casadi(eqn, y_sol) - processed_eqn = pybamm.ProcessedVariable( + processed_eqn = pybamm.process_variable( [eqn], [eqn_casadi], pybamm.Solution( @@ -505,7 +505,7 @@ def test_processed_var_0D_fixed_t_interpolation(self): t_sol = np.array([10]) y_sol = np.array([[100]]) eqn_casadi = to_casadi(eqn, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [eqn], [eqn_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), @@ -529,7 +529,7 @@ def test_processed_var_1D_interpolation(self): y_sol = x_sol[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -549,7 +549,7 @@ def test_processed_var_1D_interpolation(self): processed_var(0.5, x_sol[-1]), 2.5 * x_sol[-1] ) eqn_casadi = to_casadi(eqn_sol, y_sol) - processed_eqn = pybamm.ProcessedVariable( + processed_eqn = pybamm.process_variable( [eqn_sol], [eqn_casadi], pybamm.Solution( @@ -571,7 +571,7 @@ def test_processed_var_1D_interpolation(self): x_disc = disc.process_symbol(x) x_casadi = to_casadi(x_disc, y_sol) - processed_x = pybamm.ProcessedVariable( + processed_x = pybamm.process_variable( [x_disc], [x_casadi], pybamm.Solution( @@ -587,7 +587,7 @@ def test_processed_var_1D_interpolation(self): ) r_n.mesh = disc.mesh["negative particle"] r_n_casadi = to_casadi(r_n, y_sol) - processed_r_n = pybamm.ProcessedVariable( + processed_r_n = pybamm.process_variable( [r_n], [r_n_casadi], pybamm.Solution( @@ -608,7 +608,7 @@ def test_processed_var_1D_interpolation(self): model = tests.get_base_model_with_battery_geometry( options={"particle size": "distribution"} ) - processed_R_n = pybamm.ProcessedVariable( + processed_R_n = pybamm.process_variable( [R_n], [R_n_casadi], pybamm.Solution(t_sol, y_sol, model, {}), @@ -631,7 +631,7 @@ def test_processed_var_1D_fixed_t_interpolation(self): y_sol = x_sol[:, np.newaxis] eqn_casadi = to_casadi(eqn_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [eqn_sol], [eqn_casadi], pybamm.Solution( @@ -687,12 +687,12 @@ def test_processed_var_wrong_spatial_variable_names(self): } ) with pytest.raises(NotImplementedError, match="Spatial variable name"): - pybamm.ProcessedVariable( + pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution(t_sol, y_sol, model, {}), warn=False, - ) + ).initialise() def test_processed_var_2D_interpolation(self): var = pybamm.Variable( @@ -718,7 +718,7 @@ def test_processed_var_2D_interpolation(self): y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -768,7 +768,7 @@ def test_processed_var_2D_interpolation(self): y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -805,7 +805,7 @@ def test_processed_var_2D_fixed_t_interpolation(self): y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -838,7 +838,7 @@ def test_processed_var_2D_secondary_broadcast(self): y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -879,7 +879,7 @@ def test_processed_var_2D_secondary_broadcast(self): y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -905,7 +905,7 @@ def test_processed_var_2_d_scikit_interpolation(self): u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) var_casadi = to_casadi(var_sol, u_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -951,7 +951,7 @@ def test_processed_var_2D_fixed_t_scikit_interpolation(self): u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] var_casadi = to_casadi(var_sol, u_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -1016,7 +1016,7 @@ def test_processed_var_2D_unknown_domain(self): "domain B": {z: {"min": 0, "max": 1}}, } ) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution(t_sol, y_sol, model, {}), @@ -1062,7 +1062,7 @@ def test_3D_raises_error(self): var_casadi = to_casadi(var_sol, u_sol) with pytest.raises(NotImplementedError, match="Shape not recognized"): - pybamm.ProcessedVariable( + pybamm.process_variable( [var_sol], [var_casadi], pybamm.Solution( @@ -1080,7 +1080,7 @@ def test_process_spatial_variable_names(self): t_sol = np.linspace(0, 1) y_sol = np.array([np.linspace(0, 5)]) var_casadi = to_casadi(var, y_sol) - processed_var = pybamm.ProcessedVariable( + processed_var = pybamm.process_variable( [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), diff --git a/tests/unit/test_solvers/test_processed_variable_computed.py b/tests/unit/test_solvers/test_processed_variable_computed.py index 59a062b199..7eae2a2d3c 100644 --- a/tests/unit/test_solvers/test_processed_variable_computed.py +++ b/tests/unit/test_solvers/test_processed_variable_computed.py @@ -94,7 +94,7 @@ def test_processed_variable_0D(self): # Check cumtrapz workflow produces no errors processed_var.cumtrapz_ic = 1 - processed_var.initialise_0D() + processed_var.entries # check empty sensitivity works def test_processed_variable_0D_no_sensitivity(self): @@ -175,7 +175,7 @@ def test_processed_variable_1D(self): processed_var.mesh.edges, processed_var.mesh.nodes, ) - processed_var.initialise_1D() + processed_var.entries processed_var.mesh.nodes, processed_var.mesh.edges = ( processed_var.mesh.edges, processed_var.mesh.nodes, @@ -192,7 +192,7 @@ def test_processed_variable_1D(self): ] for domain in domain_list: processed_var.domain[0] = domain - processed_var.initialise_1D() + processed_var.entries def test_processed_variable_1D_unknown_domain(self): x = pybamm.SpatialVariable("x", domain="SEI layer", coord_sys="cartesian") @@ -330,7 +330,7 @@ def test_processed_variable_2D_x_z(self): x_s_edge.mesh = disc.mesh["separator"] x_s_edge.secondary_mesh = disc.mesh["current collector"] x_s_casadi = to_casadi(x_s_edge, y_sol) - processed_x_s_edge = pybamm.ProcessedVariable( + processed_x_s_edge = pybamm.process_variable( [x_s_edge], [x_s_casadi], pybamm.Solution( From 7125cfd67d7b4830383b47a6616152f3170df164 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:18:19 -0400 Subject: [PATCH 02/18] fix release and types --- CMakeLists.txt | 13 ------------- src/pybamm/solvers/c_solvers/idaklu/observe.hpp | 8 ++++---- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fce51a0d62..ec594e5ca5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,19 +2,6 @@ cmake_minimum_required(VERSION 3.13) cmake_policy(SET CMP0074 NEW) set(CMAKE_VERBOSE_MAKEFILE ON) -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE RelWithDebInfo) # Default to RelWithDebInfo build type if not set -endif() - -# Set optimization flags for Release builds -if(CMAKE_BUILD_TYPE STREQUAL "Release") - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") # Max optimization -endif() - -# Alternatively, you can also set optimization flags for different build types -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -g") # No optimization, with debugging info -set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -O2") # Optimization with debugging info - if(DEFINED ENV{VCPKG_ROOT_DIR} AND NOT DEFINED VCPKG_ROOT_DIR) set(VCPKG_ROOT_DIR "$ENV{VCPKG_ROOT_DIR}" CACHE STRING "Vcpkg root directory") diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index d1d8e60a67..9618cefee1 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -26,7 +26,7 @@ void process_time_series( const vector& ts_np, const vector& ys_np, const vector& inputs_np, - const vector& funcs, + const std::vector& funcs, double* out, const bool is_f_contiguous, const int len @@ -147,7 +147,7 @@ void process_and_interp_sorted_time_series( const vector& ys_data_np, const vector& yps_data_np, const vector& inputs_np, - const vector& funcs, + const std::vector& funcs, double* out, const int len ) { @@ -297,7 +297,7 @@ const py::array_t observe_hermite_interp_ND( const vector& ys_np, const vector& yps_np, const vector& inputs_np, - const vector& funcs, + const std::vector& funcs, const vector sizes ) { const int size_tot = _setup_observables(sizes); @@ -321,7 +321,7 @@ const py::array_t observe_ND( const vector& ts_np, const vector& ys_np, const vector& inputs_np, - const vector& funcs, + const std::vector& funcs, const bool is_f_contiguous, const vector sizes ) { From f587877168375efe3494df4e945629de76c2ff41 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Sun, 22 Sep 2024 17:49:25 -0400 Subject: [PATCH 03/18] good --- src/pybamm/solvers/c_solvers/idaklu.cpp | 25 +- .../solvers/c_solvers/idaklu/observe.cpp | 294 ++++++++++++++---- .../solvers/c_solvers/idaklu/observe.hpp | 241 +------------- 3 files changed, 253 insertions(+), 307 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu.cpp b/src/pybamm/solvers/c_solvers/idaklu.cpp index aafa266804..173fade8fd 100644 --- a/src/pybamm/solvers/c_solvers/idaklu.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu.cpp @@ -75,7 +75,7 @@ PYBIND11_MODULE(idaklu, m) py::arg("options"), py::return_value_policy::take_ownership); - m.def("observe_ND", &observe_ND, + m.def("observe_ND", &observe_ND, "Observe ND variables", py::arg("ts_np"), py::arg("ys_np"), @@ -85,7 +85,7 @@ PYBIND11_MODULE(idaklu, m) py::arg("sizes"), py::return_value_policy::take_ownership); - m.def("observe_hermite_interp_ND", &observe_hermite_interp_ND, + m.def("observe_hermite_interp_ND", &observe_hermite_interp_ND, "Observe ND variables", py::arg("t_interp_np"), py::arg("ts_np"), @@ -122,27 +122,6 @@ PYBIND11_MODULE(idaklu, m) py::arg("dvar_dp_fcns"), py::arg("options"), py::return_value_policy::take_ownership); - - m.def("observe_ND", &observe_ND, - "Observe ND variables", - py::arg("ts_np"), - py::arg("ys_np"), - py::arg("inputs_np"), - py::arg("funcs"), - py::arg("is_f_contiguous"), - py::arg("sizes"), - py::return_value_policy::take_ownership); - - m.def("observe_hermite_interp_ND", &observe_hermite_interp_ND, - "Observe ND variables", - py::arg("t_interp_np"), - py::arg("ts_np"), - py::arg("ys_np"), - py::arg("yps_np"), - py::arg("inputs_np"), - py::arg("funcs"), - py::arg("sizes"), - py::return_value_policy::take_ownership); #endif m.def("generate_function", &generate_casadi_function, diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp index 0fec065ece..e7e2e517a3 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -25,9 +25,6 @@ void hermite_interp( y_ijp1 = y(i, j + 1); yp_ijp1 = yp(i, j + 1); - // c[i] = (3 * (y_ptr[i + 1] - y_ptr[i]) * inv_h_sq) - (2 * yp_ptr[i] + yp_ptr[i + 1]) * inv_h; - // d[i] = (2 * (y_ptr[i] - y_ptr[i + 1]) * inv_h_sq * inv_h) + (yp_ptr[i] + yp_ptr[i + 1]) * inv_h_sq; - c = 3 * (y_ijp1 - y_ij) * inv_h2 - (2 * yp_ij + yp_ijp1) * inv_h; d = 2 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; @@ -125,55 +122,6 @@ void apply_hermite_interp( } } -const double hermite_interp_scalar( - const double t_interp, - const double t_j, - const double t_jp1, - const double y_j, - const double y_jp1, - const double yp_j, - const double yp_jp1 -) { - // const double h = t_interp - t(j); - // const double h2 = h * h; - // const double h3 = h2 * h; - - // const double h_full = t(j + 1) - t(j); - // const double inv_h = 1 / h_full; - // const double inv_h2 = inv_h * inv_h; - // const double inv_h3 = inv_h2 * inv_h; - - // double c, d, y_ij, yp_ij, y_ijp1, yp_ijp1; - - // for (size_t i = 0; i < out.size(); ++i) { - // y_ij = y(i, j); - // yp_ij = yp(i, j); - // y_ijp1 = y(i, j + 1); - // yp_ijp1 = yp(i, j + 1); - - // // c[i] = (3 * (y_ptr[i + 1] - y_ptr[i]) * inv_h_sq) - (2 * yp_ptr[i] + yp_ptr[i + 1]) * inv_h; - // // d[i] = (2 * (y_ptr[i] - y_ptr[i + 1]) * inv_h_sq * inv_h) + (yp_ptr[i] + yp_ptr[i + 1]) * inv_h_sq; - - // c = 3 * (y_ijp1 - y_ij) * inv_h2 - (2 * yp_ij + yp_ijp1) * inv_h; - // d = 2 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; - - // out[i] = y_ij + yp_ij * h + c * h2 + d * h3; - // } - - const double h = t_interp - t_j; - const double h2 = h * h; - const double h3 = h2 * h; - - const double hinv = 1 / (t_jp1 - t_j); - const double h2inv = hinv * hinv; - const double h3inv = h2inv * hinv; - - const double c = 3 * (y_jp1 - y_j) * h2inv - (2 * yp_j + yp_jp1) * hinv; - const double d = 2 * (y_j - y_jp1) * h3inv + (yp_j + yp_jp1) * h2inv; - - return y_j + yp_j * h + c * h2 + d * h3; -} - const int _setup_observables(const vector& sizes) { // Create a numpy array to manage the output if (sizes.size() == 0) { @@ -192,3 +140,245 @@ const int _setup_observables(const vector& sizes) { return size_tot; } + + +/** + * @brief Loops over the solution and generates the observable output + */ +void process_time_series( + const vector& ts_np, + const vector& ys_np, + const vector& inputs_np, + const vector& funcs, + double* out, + const bool is_f_contiguous, + const int len +) { + // Buffer for non-f-contiguous arrays + vector y_buffer; + + int count = 0; + for (size_t i = 0; i < ts_np.size(); i++) { + const auto& t_i = ts_np[i].unchecked<1>(); + const auto& y_i = ys_np[i].unchecked<2>(); // y_i is 2D + const auto inputs_data_i = inputs_np[i].data(); + const auto func_i = *funcs[i]; + + int M = y_i.shape(0); + if (!is_f_contiguous && y_buffer.size() < M) { + y_buffer.resize(M); // Resize the buffer + } + + for (size_t j = 0; j < t_i.size(); j++) { + const double t_ij = t_i(j); + + // Use a view of y_i + if (!is_f_contiguous) { + apply_copy(y_buffer, y_i, j); + } + const double* y_ij = is_f_contiguous ? &y_i(0, j) : y_buffer.data(); + + // Prepare CasADi function arguments + vector args = { &t_ij, y_ij, inputs_data_i }; + vector results = { &out[count] }; + // Call the CasADi function with proper arguments + (func_i)(args, results); + + count += len; + } + } +} + +/** + * @brief Loops over the solution and generates the observable output + */ +void process_and_interp_sorted_time_series( + const np_array_realtype& t_interp_np, + const vector& ts_data_np, + const vector& ys_data_np, + const vector& yps_data_np, + const vector& inputs_np, + const vector& funcs, + double* out, + const int len +) { + // y cache + vector y_interp; + + auto t_interp = t_interp_np.unchecked<1>(); + ssize_t i_interp = 0; + int count = 0; + + auto t_interp_next = t_interp(0); + vector args; + vector results; + + ssize_t N_data = 0; + const ssize_t N_interp = t_interp.size(); + + for (size_t i = 0; i < ts_data_np.size(); i++) { + N_data += ts_data_np[i].size(); + } + + const bool cache_hermite_interp = N_interp > N_data; + vector hermite_c; + vector hermite_d; + + for (size_t i = 0; i < ts_data_np.size(); i++) { + const auto& t_data_i = ts_data_np[i].unchecked<1>(); + const auto& y_data_i = ys_data_np[i].unchecked<2>(); // y_data_i is 2D + const auto& yp_data_i = yps_data_np[i].unchecked<2>(); // yp_data_i is 2D + const auto inputs_i = inputs_np[i].data(); + const auto func_i = *funcs[i]; + const double t_data_final = t_data_i(t_data_i.size() - 1); // Access last element + + // Resize y_interp buffer to match the number of rows in y_data_i + int M = y_data_i.shape(0); + if (y_interp.size() < M) { + y_interp.resize(M); + if (cache_hermite_interp) { + hermite_c.resize(M); + hermite_d.resize(M); + } + } + + ssize_t j = 0; + t_interp_next = t_interp(i_interp); + while (i_interp < N_interp && t_interp(i_interp) <= t_data_final) { + // Find the correct index j + for (; j < t_data_i.size() - 2; ++j) { + if (t_data_i(j) <= t_interp_next && t_interp_next <= t_data_i(j + 1)) { + break; + } + } + const double t_data_start = t_data_i(j); + const double t_data_next = t_data_i(j + 1); + + if (cache_hermite_interp) { + compute_c_d(hermite_c, hermite_d, t_data_i, y_data_i, yp_data_i, j); + } + + args = { &t_interp_next, y_interp.data(), inputs_i }; + + // Perform Hermite interpolation for all valid t_interp values + for (ssize_t k = 0; t_interp_next <= t_data_next; ++k) { + if (k == 0 && t_interp_next == t_data_start) { + apply_copy(y_interp, y_data_i, j); + } else if (cache_hermite_interp) { + apply_hermite_interp(y_interp, t_interp_next, t_data_start, y_data_i, yp_data_i, hermite_c, hermite_d, j); + } else { + hermite_interp(y_interp, t_interp_next, t_data_i, y_data_i, yp_data_i, j); + } + + // Prepare CasADi function arguments + results = { &out[count] }; + + // Call the CasADi function with the proper arguments + func_i(args, results); + + count += len; + ++i_interp; // Move to the next time step for interpolation + if (i_interp >= N_interp) { + return; + } + t_interp_next = t_interp(i_interp); + } + } + } + + // Return if there are no more data points to interpolate + if (i_interp == N_interp) { + return; + } + + // Extrapolate to the right + const auto& t_data_i = ts_data_np[ts_data_np.size() - 1].unchecked<1>(); + const auto& y_data_i = ys_data_np[ys_data_np.size() - 1].unchecked<2>(); // y_data_i is 2D + const auto& yp_data_i = yps_data_np[yps_data_np.size() - 1].unchecked<2>(); // yp_data_i is 2D + const auto inputs_i = inputs_np[inputs_np.size() - 1].data(); + const auto func_i = *funcs[funcs.size() - 1]; + + const ssize_t j = t_data_i.size() - 2; + const double t_data_start = t_data_i(j); + const double t_data_final = t_data_i(j + 1); + + // Resize y_interp buffer to match the number of rows in y_data_i + int M = y_data_i.shape(0); + if (y_interp.size() < M) { + y_interp.resize(M); + if (cache_hermite_interp) { + hermite_c.resize(M); + hermite_d.resize(M); + } + } + + // Find the number of steps within this interval + args = { &t_interp_next, y_interp.data(), inputs_i }; + + // Perform Hermite interpolation for all valid t_interp values + for (; i_interp < N_interp; ++i_interp) { + t_interp_next = t_interp(i_interp); + + if (cache_hermite_interp) { + compute_c_d(hermite_c, hermite_d, t_data_i, y_data_i, yp_data_i, j); + apply_hermite_interp(y_interp, t_interp_next, t_data_start, y_data_i, yp_data_i, hermite_c, hermite_d, j); + } else { + hermite_interp(y_interp, t_interp_next, t_data_i, y_data_i, yp_data_i, j); + } + + // Prepare CasADi function arguments + results = { &out[count] }; + + // Call the CasADi function with the proper arguments + func_i(args, results); + + count += len; + } +} + +/** + * @brief Observe and Hermite interpolate ND variables + */ +const py::array_t observe_hermite_interp_ND( + const np_array_realtype& t_interp_np, + const vector& ts_np, + const vector& ys_np, + const vector& yps_np, + const vector& inputs_np, + const vector& funcs, + const vector sizes +) { + const int size_tot = _setup_observables(sizes); + + py::array_t out_array(sizes); + auto out = out_array.mutable_data(); + + process_and_interp_sorted_time_series( + t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, out, size_tot / sizes.back() + ); + + return out_array; +} + +/** + * @brief Observe ND variables + */ +const py::array_t observe_ND( + const vector& ts_np, + const vector& ys_np, + const vector& inputs_np, + const vector& funcs, + const bool is_f_contiguous, + const vector sizes +) { + const int size_tot = _setup_observables(sizes); + + py::array_t out_array(sizes); + auto out = out_array.mutable_data(); + + process_time_series( + ts_np, ys_np, inputs_np, funcs, out, is_f_contiguous, size_tot / sizes.back() + ); + + return out_array; +} diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index 9618cefee1..d49593534e 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -9,6 +9,7 @@ #include #include // For numpy support in pybind11 +#include namespace py = pybind11; @@ -21,82 +22,15 @@ void apply_copy( /** * @brief Loops over the solution and generates the observable output */ -template void process_time_series( const vector& ts_np, const vector& ys_np, const vector& inputs_np, - const std::vector& funcs, + const vector& funcs, double* out, const bool is_f_contiguous, const int len -) { - // Buffer for non-f-contiguous arrays - vector y_buffer; - - int count = 0; - for (size_t i = 0; i < ts_np.size(); i++) { - const auto& t_i = ts_np[i].unchecked<1>(); - const auto& y_i = ys_np[i].unchecked<2>(); // y_i is 2D - const auto inputs_data_i = inputs_np[i].data(); - const auto func_i = *funcs[i]; - - int M = y_i.shape(0); - if (!is_f_contiguous && y_buffer.size() < M) { - y_buffer.resize(M); // Resize the buffer - } - - for (size_t j = 0; j < t_i.size(); j++) { - const double t_ij = t_i(j); - - // Use a view of y_i - if (!is_f_contiguous) { - apply_copy(y_buffer, y_i, j); - } - const double* y_ij = is_f_contiguous ? &y_i(0, j) : y_buffer.data(); - - // Prepare CasADi function arguments - vector args = { &t_ij, y_ij, inputs_data_i }; - vector results = { &out[count] }; - // Call the CasADi function with proper arguments - (func_i)(args, results); - - count += len; - } - } -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +); void hermite_interp( std::vector& out, @@ -140,149 +74,16 @@ void apply_hermite_interp( /** * @brief Loops over the solution and generates the observable output */ -template void process_and_interp_sorted_time_series( const np_array_realtype& t_interp_np, const vector& ts_data_np, const vector& ys_data_np, const vector& yps_data_np, const vector& inputs_np, - const std::vector& funcs, + const vector& funcs, double* out, const int len -) { - // y cache - vector y_interp; - - auto t_interp = t_interp_np.unchecked<1>(); - ssize_t i_interp = 0; - int count = 0; - - auto t_interp_next = t_interp(0); - vector args; - vector results; - - ssize_t N_data = 0; - const ssize_t N_interp = t_interp.size(); - - for (size_t i = 0; i < ts_data_np.size(); i++) { - N_data += ts_data_np[i].size(); - } - - const bool cache_hermite_interp = N_interp > N_data; - vector hermite_c; - vector hermite_d; - - for (size_t i = 0; i < ts_data_np.size(); i++) { - const auto& t_data_i = ts_data_np[i].unchecked<1>(); - const auto& y_data_i = ys_data_np[i].unchecked<2>(); // y_data_i is 2D - const auto& yp_data_i = yps_data_np[i].unchecked<2>(); // yp_data_i is 2D - const auto inputs_i = inputs_np[i].data(); - const auto func_i = *funcs[i]; - const double t_data_final = t_data_i(t_data_i.size() - 1); // Access last element - - // Resize y_interp buffer to match the number of rows in y_data_i - int M = y_data_i.shape(0); - if (y_interp.size() < M) { - y_interp.resize(M); - if (cache_hermite_interp) { - hermite_c.resize(M); - hermite_d.resize(M); - } - } - - ssize_t j = 0; - t_interp_next = t_interp(i_interp); - while (i_interp < N_interp && t_interp(i_interp) <= t_data_final) { - // Find the correct index j - for (; j < t_data_i.size() - 2; ++j) { - if (t_data_i(j) <= t_interp_next && t_interp_next <= t_data_i(j + 1)) { - break; - } - } - const double t_data_start = t_data_i(j); - const double t_data_next = t_data_i(j + 1); - - if (cache_hermite_interp) { - compute_c_d(hermite_c, hermite_d, t_data_i, y_data_i, yp_data_i, j); - } - - args = { &t_interp_next, y_interp.data(), inputs_i }; - - // Perform Hermite interpolation for all valid t_interp values - for (ssize_t k = 0; t_interp_next <= t_data_next; ++k) { - if (k == 0 && t_interp_next == t_data_start) { - apply_copy(y_interp, y_data_i, j); - } else if (cache_hermite_interp) { - apply_hermite_interp(y_interp, t_interp_next, t_data_start, y_data_i, yp_data_i, hermite_c, hermite_d, j); - } else { - hermite_interp(y_interp, t_interp_next, t_data_i, y_data_i, yp_data_i, j); - } - - // Prepare CasADi function arguments - results = { &out[count] }; - - // Call the CasADi function with the proper arguments - func_i(args, results); - - count += len; - ++i_interp; // Move to the next time step for interpolation - if (i_interp >= N_interp) { - return; - } - t_interp_next = t_interp(i_interp); - } - } - } - - if (i_interp == N_interp) { - return; - } - - // Extrapolate right if needed - const auto& t_data_i = ts_data_np[ts_data_np.size() - 1].unchecked<1>(); - const auto& y_data_i = ys_data_np[ys_data_np.size() - 1].unchecked<2>(); // y_data_i is 2D - const auto& yp_data_i = yps_data_np[yps_data_np.size() - 1].unchecked<2>(); // yp_data_i is 2D - const auto inputs_i = inputs_np[inputs_np.size() - 1].data(); - const auto func_i = *funcs[funcs.size() - 1]; - - const ssize_t j = t_data_i.size() - 2; - const double t_data_start = t_data_i(j); - const double t_data_final = t_data_i(j + 1); - - // Resize y_interp buffer to match the number of rows in y_data_i - int M = y_data_i.shape(0); - if (y_interp.size() < M) { - y_interp.resize(M); - if (cache_hermite_interp) { - hermite_c.resize(M); - hermite_d.resize(M); - } - } - - // Find the number of steps within this interval - args = { &t_interp_next, y_interp.data(), inputs_i }; - - // Perform Hermite interpolation for all valid t_interp values - for (; i_interp < N_interp; ++i_interp) { - t_interp_next = t_interp(i_interp); - - if (cache_hermite_interp) { - compute_c_d(hermite_c, hermite_d, t_data_i, y_data_i, yp_data_i, j); - apply_hermite_interp(y_interp, t_interp_next, t_data_start, y_data_i, yp_data_i, hermite_c, hermite_d, j); - } else { - hermite_interp(y_interp, t_interp_next, t_data_i, y_data_i, yp_data_i, j); - } - - // Prepare CasADi function arguments - results = { &out[count] }; - - // Call the CasADi function with the proper arguments - func_i(args, results); - - count += len; - } -} +); const int _setup_observables(const vector& sizes); @@ -290,51 +91,27 @@ const int _setup_observables(const vector& sizes); /** * @brief Observe and Hermite interpolate ND variables */ -template const py::array_t observe_hermite_interp_ND( const np_array_realtype& t_interp_np, const vector& ts_np, const vector& ys_np, const vector& yps_np, const vector& inputs_np, - const std::vector& funcs, + const vector& funcs, const vector sizes -) { - const int size_tot = _setup_observables(sizes); - - py::array_t out_array(sizes); - auto out = out_array.mutable_data(); - - process_and_interp_sorted_time_series( - t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, out, size_tot / sizes.back() - ); - - return out_array; -} +); /** * @brief Observe ND variables */ -template const py::array_t observe_ND( const vector& ts_np, const vector& ys_np, const vector& inputs_np, - const std::vector& funcs, + const vector& funcs, const bool is_f_contiguous, const vector sizes -) { - const int size_tot = _setup_observables(sizes); - - py::array_t out_array(sizes); - auto out = out_array.mutable_data(); - - process_time_series( - ts_np, ys_np, inputs_np, funcs, out, is_f_contiguous, size_tot / sizes.back() - ); - - return out_array; -} +); #endif // PYBAMM_CREATE_OBSERVE_HPP From a4ca2d1de7bfb6e552bcefcd340fbeee9299808b Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:22:03 -0400 Subject: [PATCH 04/18] faster interp --- .../solvers/c_solvers/idaklu/observe.cpp | 497 +++++++----------- .../solvers/c_solvers/idaklu/observe.hpp | 87 +-- 2 files changed, 206 insertions(+), 378 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp index e7e2e517a3..2159646f92 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -1,134 +1,52 @@ #include "observe.hpp" -void hermite_interp( - std::vector& out, - const double t_interp, - const py::detail::unchecked_reference& t, - const py::detail::unchecked_reference& y, - const py::detail::unchecked_reference& yp, - const size_t j -) { - const double h = t_interp - t(j); - const double h2 = h * h; - const double h3 = h2 * h; - - const double h_full = t(j + 1) - t(j); - const double inv_h = 1 / h_full; - const double inv_h2 = inv_h * inv_h; - const double inv_h3 = inv_h2 * inv_h; - - double c, d, y_ij, yp_ij, y_ijp1, yp_ijp1; - - for (size_t i = 0; i < out.size(); ++i) { - y_ij = y(i, j); - yp_ij = yp(i, j); - y_ijp1 = y(i, j + 1); - yp_ijp1 = yp(i, j + 1); - - c = 3 * (y_ijp1 - y_ij) * inv_h2 - (2 * yp_ij + yp_ijp1) * inv_h; - d = 2 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; - - out[i] = y_ij + yp_ij * h + c * h2 + d * h3; - } -} - -void apply_copy( - std::vector& out, - const py::detail::unchecked_reference& y, - const size_t j -) { - for (size_t i = 0; i < out.size(); ++i) { - out[i] = y(i, j); +class HermiteInterpolator { +public: + HermiteInterpolator(const py::detail::unchecked_reference& t, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp) + : t(t), y(y), yp(yp) {} + + void compute_c_d(size_t j, vector& c, vector& d) const { + const double h_full = t(j + 1) - t(j); + const double inv_h = 1.0 / h_full; + const double inv_h2 = inv_h * inv_h; + const double inv_h3 = inv_h2 * inv_h; + + for (size_t i = 0; i < y.shape(0); ++i) { + double y_ij = y(i, j); + double yp_ij = yp(i, j); + double y_ijp1 = y(i, j + 1); + double yp_ijp1 = yp(i, j + 1); + + c[i] = 3.0 * (y_ijp1 - y_ij) * inv_h2 - (2.0 * yp_ij + yp_ijp1) * inv_h; + d[i] = 2.0 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; + } } -} - -void hermite_interp_no_y( - std::vector& out, - const double t_interp, - const py::detail::unchecked_reference& t, - const py::detail::unchecked_reference& y, - const py::detail::unchecked_reference& yp, - const size_t j -) { - // This begins from a copy of y, so we don't need to copy y - // once again - const double h = t_interp - t(j); - const double h2 = h * h; - const double h3 = h2 * h; - - const double h_full = t(j + 1) - t(j); - const double inv_h = 1 / h_full; - const double inv_h2 = inv_h * inv_h; - const double inv_h3 = inv_h2 * inv_h; - double c, d, y_ij, yp_ij, y_ijp1, yp_ijp1; - - for (size_t i = 0; i < out.size(); ++i) { - y_ij = y(i, j); - yp_ij = yp(i, j); - y_ijp1 = y(i, j + 1); - yp_ijp1 = yp(i, j + 1); - - c = 3 * (y_ijp1 - y_ij) * inv_h2 - (2 * yp_ij + yp_ijp1) * inv_h; - d = 2 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; - - out[i] += yp_ij * h + c * h2 + d * h3; - } -} + void interpolate(vector& out, double t_interp, size_t j, vector& c, vector& d) const { + const double h = t_interp - t(j); + const double h2 = h * h; + const double h3 = h2 * h; -void compute_c_d( - std::vector& c_out, - std::vector& d_out, - const py::detail::unchecked_reference& t, - const py::detail::unchecked_reference& y, - const py::detail::unchecked_reference& yp, - const size_t j -) { - const double h_full = t(j + 1) - t(j); - const double inv_h = 1.0 / h_full; - const double inv_h2 = inv_h * inv_h; - const double inv_h3 = inv_h2 * inv_h; - - for (size_t i = 0; i < y.shape(0); ++i) { - double y_ij = y(i, j); - double yp_ij = yp(i, j); - double y_ijp1 = y(i, j + 1); - double yp_ijp1 = yp(i, j + 1); - - c_out[i] = 3.0 * (y_ijp1 - y_ij) * inv_h2 - (2.0 * yp_ij + yp_ijp1) * inv_h; - d_out[i] = 2.0 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; + for (size_t i = 0; i < out.size(); ++i) { + double y_ij = y(i, j); + double yp_ij = yp(i, j); + out[i] = y_ij + yp_ij * h + c[i] * h2 + d[i] * h3; + } } -} - -void apply_hermite_interp( - std::vector& out, - const double t_interp, - const double t_j, - const py::detail::unchecked_reference& y, - const py::detail::unchecked_reference& yp, - const std::vector& c, - const std::vector& d, - const size_t j -) { - const double h = t_interp - t_j; // h = t_interp - t(j) - const double h2 = h * h; - const double h3 = h2 * h; - for (size_t i = 0; i < out.size(); ++i) { - double y_ij = y(i, j); - double yp_ij = yp(i, j); +private: + const py::detail::unchecked_reference& t; + const py::detail::unchecked_reference& y; + const py::detail::unchecked_reference& yp; +}; - out[i] = y_ij + yp_ij * h + c[i] * h2 + d[i] * h3; - } -} - -const int _setup_observables(const vector& sizes) { - // Create a numpy array to manage the output - if (sizes.size() == 0) { +int setup_observable(const vector& sizes) { + if (sizes.empty()) { throw std::invalid_argument("sizes must have at least one element"); } - // Create a numpy array to manage the output int size_tot = 1; for (const auto& size : sizes) { size_tot *= size; @@ -141,204 +59,196 @@ const int _setup_observables(const vector& sizes) { return size_tot; } +class TimeSeriesProcessor { +public: + TimeSeriesProcessor(const vector& _ts, + const vector& _ys, + const vector& _inputs, + const vector& _funcs, + double* _out, + bool _is_f_contiguous, + const int _len) + : ts(_ts), ys(_ys), inputs(_inputs), funcs(_funcs), + out(_out), is_f_contiguous(_is_f_contiguous), len(_len) {} + + void process() { + vector y_buffer; + vector args; + vector results; + + int count = 0; + for (size_t i = 0; i < ts.size(); i++) { + const auto& t = ts[i].unchecked<1>(); + const auto& y = ys[i].unchecked<2>(); + const auto input = inputs[i].data(); + const auto func = *funcs[i]; + + int M = y.shape(0); + if (!is_f_contiguous && y_buffer.size() < M) { + y_buffer.resize(M); + } -/** - * @brief Loops over the solution and generates the observable output - */ -void process_time_series( - const vector& ts_np, - const vector& ys_np, - const vector& inputs_np, - const vector& funcs, - double* out, - const bool is_f_contiguous, - const int len -) { - // Buffer for non-f-contiguous arrays - vector y_buffer; - - int count = 0; - for (size_t i = 0; i < ts_np.size(); i++) { - const auto& t_i = ts_np[i].unchecked<1>(); - const auto& y_i = ys_np[i].unchecked<2>(); // y_i is 2D - const auto inputs_data_i = inputs_np[i].data(); - const auto func_i = *funcs[i]; - - int M = y_i.shape(0); - if (!is_f_contiguous && y_buffer.size() < M) { - y_buffer.resize(M); // Resize the buffer - } + for (size_t j = 0; j < t.size(); j++) { + const double t_val = t(j); + const double* y_val = is_f_contiguous ? &y(0, j) : copy_to_buffer(y_buffer, y, j); - for (size_t j = 0; j < t_i.size(); j++) { - const double t_ij = t_i(j); + args = { &t_val, y_val, input }; + results = { &out[count] }; + func(args, results); - // Use a view of y_i - if (!is_f_contiguous) { - apply_copy(y_buffer, y_i, j); + count += len; } - const double* y_ij = is_f_contiguous ? &y_i(0, j) : y_buffer.data(); - - // Prepare CasADi function arguments - vector args = { &t_ij, y_ij, inputs_data_i }; - vector results = { &out[count] }; - // Call the CasADi function with proper arguments - (func_i)(args, results); - - count += len; } } -} - -/** - * @brief Loops over the solution and generates the observable output - */ -void process_and_interp_sorted_time_series( - const np_array_realtype& t_interp_np, - const vector& ts_data_np, - const vector& ys_data_np, - const vector& yps_data_np, - const vector& inputs_np, - const vector& funcs, - double* out, - const int len -) { - // y cache - vector y_interp; - - auto t_interp = t_interp_np.unchecked<1>(); - ssize_t i_interp = 0; - int count = 0; - - auto t_interp_next = t_interp(0); - vector args; - vector results; - ssize_t N_data = 0; - const ssize_t N_interp = t_interp.size(); +private: + const double* copy_to_buffer(vector& out, const py::detail::unchecked_reference& y, size_t j) { + for (size_t i = 0; i < out.size(); ++i) { + out[i] = y(i, j); + } - for (size_t i = 0; i < ts_data_np.size(); i++) { - N_data += ts_data_np[i].size(); + return out.data(); } - const bool cache_hermite_interp = N_interp > N_data; - vector hermite_c; - vector hermite_d; + const vector& ts; + const vector& ys; + const vector& inputs; + const vector& funcs; + double* out; + bool is_f_contiguous; + int len; +}; + +class TimeSeriesInterpolator { +public: + TimeSeriesInterpolator(const np_array_realtype& t_interp_np, + const vector& ts_data, + const vector& ys_data, + const vector& yps_data, + const vector& inputs, + const vector& funcs, + double* out, + int len) + : t_interp_np(t_interp_np), ts_data_np(ts_data), ys_data_np(ys_data), + yps_data_np(yps_data), inputs_np(inputs), funcs(funcs), + out(out), len(len) {} + + void process() { + vector y_interp; + auto t_interp = t_interp_np.unchecked<1>(); + ssize_t i_interp = 0; + int count = 0; + ssize_t N_data = 0; + const ssize_t N_interp = t_interp.size(); + + for (const auto& ts : ts_data_np) { + N_data += ts.size(); + } - for (size_t i = 0; i < ts_data_np.size(); i++) { - const auto& t_data_i = ts_data_np[i].unchecked<1>(); - const auto& y_data_i = ys_data_np[i].unchecked<2>(); // y_data_i is 2D - const auto& yp_data_i = yps_data_np[i].unchecked<2>(); // yp_data_i is 2D - const auto inputs_i = inputs_np[i].data(); - const auto func_i = *funcs[i]; - const double t_data_final = t_data_i(t_data_i.size() - 1); // Access last element + // Preallocate c and d vectors + vector c, d; - // Resize y_interp buffer to match the number of rows in y_data_i - int M = y_data_i.shape(0); - if (y_interp.size() < M) { - y_interp.resize(M); - if (cache_hermite_interp) { - hermite_c.resize(M); - hermite_d.resize(M); - } + // Main processing within bounds + process_within_bounds(i_interp, count, y_interp, c, d, t_interp, N_interp); + + // Extrapolation for remaining points + if (i_interp < N_interp) { + extrapolate_remaining(i_interp, count, y_interp, c, d, t_interp, N_interp); } + } - ssize_t j = 0; - t_interp_next = t_interp(i_interp); - while (i_interp < N_interp && t_interp(i_interp) <= t_data_final) { - // Find the correct index j - for (; j < t_data_i.size() - 2; ++j) { - if (t_data_i(j) <= t_interp_next && t_interp_next <= t_data_i(j + 1)) { - break; + void process_within_bounds(ssize_t& i_interp, int& count, vector& y_interp, + vector& c, vector& d, + const py::detail::unchecked_reference& t_interp, + const ssize_t N_interp) { + vector args; + vector results; + for (size_t i = 0; i < ts_data_np.size(); i++) { + const auto& t_data = ts_data_np[i].unchecked<1>(); + const auto& y_data = ys_data_np[i].unchecked<2>(); + const auto& yp_data = yps_data_np[i].unchecked<2>(); + const auto inputs_data = inputs_np[i].data(); + const auto func = *funcs[i]; + const double t_data_final = t_data(t_data.size() - 1); + + resize_arrays(y_interp, c, d, y_data.shape(0)); + + args = { &t_interp(i_interp), y_interp.data(), inputs_data }; + + ssize_t j = 0; + ssize_t j_prev = -1; + auto itp = HermiteInterpolator(t_data, y_data, yp_data); + while (i_interp < N_interp && t_interp(i_interp) <= t_data_final) { + for (; j < t_data.size() - 2; ++j) { + if (t_data(j) <= t_interp(i_interp) && t_interp(i_interp) <= t_data(j + 1)) { + break; + } } - } - const double t_data_start = t_data_i(j); - const double t_data_next = t_data_i(j + 1); - - if (cache_hermite_interp) { - compute_c_d(hermite_c, hermite_d, t_data_i, y_data_i, yp_data_i, j); - } - args = { &t_interp_next, y_interp.data(), inputs_i }; - - // Perform Hermite interpolation for all valid t_interp values - for (ssize_t k = 0; t_interp_next <= t_data_next; ++k) { - if (k == 0 && t_interp_next == t_data_start) { - apply_copy(y_interp, y_data_i, j); - } else if (cache_hermite_interp) { - apply_hermite_interp(y_interp, t_interp_next, t_data_start, y_data_i, yp_data_i, hermite_c, hermite_d, j); - } else { - hermite_interp(y_interp, t_interp_next, t_data_i, y_data_i, yp_data_i, j); + if (j != j_prev) { + // Compute c and d for the new interval + itp.compute_c_d(j, c, d); } - // Prepare CasADi function arguments + itp.interpolate(y_interp, t_interp(i_interp), j, c, d); results = { &out[count] }; - - // Call the CasADi function with the proper arguments - func_i(args, results); + args[0] = &t_interp(i_interp); + func(args, results); count += len; - ++i_interp; // Move to the next time step for interpolation - if (i_interp >= N_interp) { - return; - } - t_interp_next = t_interp(i_interp); + ++i_interp; + j_prev = j; } } } - // Return if there are no more data points to interpolate - if (i_interp == N_interp) { - return; - } - - // Extrapolate to the right - const auto& t_data_i = ts_data_np[ts_data_np.size() - 1].unchecked<1>(); - const auto& y_data_i = ys_data_np[ys_data_np.size() - 1].unchecked<2>(); // y_data_i is 2D - const auto& yp_data_i = yps_data_np[yps_data_np.size() - 1].unchecked<2>(); // yp_data_i is 2D - const auto inputs_i = inputs_np[inputs_np.size() - 1].data(); - const auto func_i = *funcs[funcs.size() - 1]; - - const ssize_t j = t_data_i.size() - 2; - const double t_data_start = t_data_i(j); - const double t_data_final = t_data_i(j + 1); - - // Resize y_interp buffer to match the number of rows in y_data_i - int M = y_data_i.shape(0); - if (y_interp.size() < M) { - y_interp.resize(M); - if (cache_hermite_interp) { - hermite_c.resize(M); - hermite_d.resize(M); - } - } + void extrapolate_remaining(ssize_t& i_interp, int& count, vector& y_interp, + vector& c, vector& d, + const py::detail::unchecked_reference& t_interp, + const ssize_t N_interp) { + const auto& t_data = ts_data_np.back().unchecked<1>(); + const auto& y_data = ys_data_np.back().unchecked<2>(); + const auto& yp_data = yps_data_np.back().unchecked<2>(); + const auto inputs_data = inputs_np.back().data(); + const auto func = *funcs.back(); + const ssize_t j = t_data.size() - 2; - // Find the number of steps within this interval - args = { &t_interp_next, y_interp.data(), inputs_i }; + resize_arrays(y_interp, c, d, y_data.shape(0)); - // Perform Hermite interpolation for all valid t_interp values - for (; i_interp < N_interp; ++i_interp) { - t_interp_next = t_interp(i_interp); + auto itp = HermiteInterpolator(t_data, y_data, yp_data); + itp.compute_c_d(j, c, d); - if (cache_hermite_interp) { - compute_c_d(hermite_c, hermite_d, t_data_i, y_data_i, yp_data_i, j); - apply_hermite_interp(y_interp, t_interp_next, t_data_start, y_data_i, yp_data_i, hermite_c, hermite_d, j); - } else { - hermite_interp(y_interp, t_interp_next, t_data_i, y_data_i, yp_data_i, j); - } + for (; i_interp < N_interp; ++i_interp) { + const double t_interp_next = t_interp(i_interp); + itp.interpolate(y_interp, t_interp_next, j, c, d); - // Prepare CasADi function arguments - results = { &out[count] }; + vector args = { &t_interp_next, y_interp.data(), inputs_data }; + vector results = { &out[count] }; + func(args, results); - // Call the CasADi function with the proper arguments - func_i(args, results); + count += len; + } + } - count += len; + void resize_arrays(vector& y_interp, vector& c, vector& d, const int M) { + if (y_interp.size() < M) { + y_interp.resize(M); + c.resize(M); + d.resize(M); + } } -} -/** - * @brief Observe and Hermite interpolate ND variables - */ +private: + const np_array_realtype& t_interp_np; + const vector& ts_data_np; + const vector& ys_data_np; + const vector& yps_data_np; + const vector& inputs_np; + const vector& funcs; + double* out; + int len; +}; + const py::array_t observe_hermite_interp_ND( const np_array_realtype& t_interp_np, const vector& ts_np, @@ -348,21 +258,15 @@ const py::array_t observe_hermite_interp_ND( const vector& funcs, const vector sizes ) { - const int size_tot = _setup_observables(sizes); - + const int size_tot = setup_observable(sizes); py::array_t out_array(sizes); auto out = out_array.mutable_data(); - process_and_interp_sorted_time_series( - t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, out, size_tot / sizes.back() - ); + TimeSeriesInterpolator(t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, out, size_tot / sizes.back()).process(); return out_array; } -/** - * @brief Observe ND variables - */ const py::array_t observe_ND( const vector& ts_np, const vector& ys_np, @@ -371,14 +275,11 @@ const py::array_t observe_ND( const bool is_f_contiguous, const vector sizes ) { - const int size_tot = _setup_observables(sizes); - + const int size_tot = setup_observable(sizes); py::array_t out_array(sizes); auto out = out_array.mutable_data(); - process_time_series( - ts_np, ys_np, inputs_np, funcs, out, is_f_contiguous, size_tot / sizes.back() - ); + TimeSeriesProcessor(ts_np, ys_np, inputs_np, funcs, out, is_f_contiguous, size_tot / sizes.back()).process(); return out_array; } diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index d49593534e..7d2a4a86fb 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -13,90 +13,15 @@ namespace py = pybind11; -void apply_copy( - std::vector& out, - const py::detail::unchecked_reference& y, - const size_t j -); - -/** - * @brief Loops over the solution and generates the observable output - */ -void process_time_series( - const vector& ts_np, - const vector& ys_np, - const vector& inputs_np, - const vector& funcs, - double* out, - const bool is_f_contiguous, - const int len -); - -void hermite_interp( - std::vector& out, - const double t_interp, - const py::detail::unchecked_reference& t, - const py::detail::unchecked_reference& y, - const py::detail::unchecked_reference& yp, - const size_t j -); - -const double hermite_interp_scalar( - const double t_interp, - const double t_j, - const double t_jp1, - const double y_j, - const double y_jp1, - const double yp_j, - const double yp_jp1 -); - -void compute_c_d( - std::vector& c_out, - std::vector& d_out, - const py::detail::unchecked_reference& t, - const py::detail::unchecked_reference& y, - const py::detail::unchecked_reference& yp, - const size_t j -); - -void apply_hermite_interp( - std::vector& out, - const double t_interp, - const double t_j, - const py::detail::unchecked_reference& y, - const py::detail::unchecked_reference& yp, - const std::vector& c, - const std::vector& d, - const size_t j -); - -/** - * @brief Loops over the solution and generates the observable output - */ -void process_and_interp_sorted_time_series( - const np_array_realtype& t_interp_np, - const vector& ts_data_np, - const vector& ys_data_np, - const vector& yps_data_np, - const vector& inputs_np, - const vector& funcs, - double* out, - const int len -); - -const int _setup_observables(const vector& sizes); - - /** * @brief Observe and Hermite interpolate ND variables */ const py::array_t observe_hermite_interp_ND( - const np_array_realtype& t_interp_np, - const vector& ts_np, - const vector& ys_np, - const vector& yps_np, - const vector& inputs_np, + const np_array_realtype& t_interp, + const vector& ts, + const vector& ys, + const vector& yps, + const vector& inputs, const vector& funcs, const vector sizes ); @@ -114,4 +39,6 @@ const py::array_t observe_ND( const vector sizes ); +int setup_observable(const vector& sizes); + #endif // PYBAMM_CREATE_OBSERVE_HPP From bdd1dfa1ae5671dab0a886bdd8702b65cda562e5 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:34:30 -0400 Subject: [PATCH 05/18] `double` -> `realtype` --- .../solvers/c_solvers/idaklu/observe.cpp | 124 +++++++++--------- .../solvers/c_solvers/idaklu/observe.hpp | 4 +- 2 files changed, 66 insertions(+), 62 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp index 2159646f92..743911495f 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -2,44 +2,46 @@ class HermiteInterpolator { public: - HermiteInterpolator(const py::detail::unchecked_reference& t, - const py::detail::unchecked_reference& y, - const py::detail::unchecked_reference& yp) + HermiteInterpolator(const py::detail::unchecked_reference& t, + const py::detail::unchecked_reference& y, + const py::detail::unchecked_reference& yp) : t(t), y(y), yp(yp) {} - void compute_c_d(size_t j, vector& c, vector& d) const { - const double h_full = t(j + 1) - t(j); - const double inv_h = 1.0 / h_full; - const double inv_h2 = inv_h * inv_h; - const double inv_h3 = inv_h2 * inv_h; + void compute_c_d(size_t j, vector& c, vector& d) const { + // Called at the start of each interval + const realtype h_full = t(j + 1) - t(j); + const realtype inv_h = 1.0 / h_full; + const realtype inv_h2 = inv_h * inv_h; + const realtype inv_h3 = inv_h2 * inv_h; for (size_t i = 0; i < y.shape(0); ++i) { - double y_ij = y(i, j); - double yp_ij = yp(i, j); - double y_ijp1 = y(i, j + 1); - double yp_ijp1 = yp(i, j + 1); + realtype y_ij = y(i, j); + realtype yp_ij = yp(i, j); + realtype y_ijp1 = y(i, j + 1); + realtype yp_ijp1 = yp(i, j + 1); c[i] = 3.0 * (y_ijp1 - y_ij) * inv_h2 - (2.0 * yp_ij + yp_ijp1) * inv_h; d[i] = 2.0 * (y_ij - y_ijp1) * inv_h3 + (yp_ij + yp_ijp1) * inv_h2; } } - void interpolate(vector& out, double t_interp, size_t j, vector& c, vector& d) const { - const double h = t_interp - t(j); - const double h2 = h * h; - const double h3 = h2 * h; + void interpolate(vector& out, realtype t_interp, size_t j, vector& c, vector& d) const { + // Must be called after compute_c_d + const realtype h = t_interp - t(j); + const realtype h2 = h * h; + const realtype h3 = h2 * h; for (size_t i = 0; i < out.size(); ++i) { - double y_ij = y(i, j); - double yp_ij = yp(i, j); + realtype y_ij = y(i, j); + realtype yp_ij = yp(i, j); out[i] = y_ij + yp_ij * h + c[i] * h2 + d[i] * h3; } } private: - const py::detail::unchecked_reference& t; - const py::detail::unchecked_reference& y; - const py::detail::unchecked_reference& yp; + const py::detail::unchecked_reference& t; + const py::detail::unchecked_reference& y; + const py::detail::unchecked_reference& yp; }; int setup_observable(const vector& sizes) { @@ -65,16 +67,16 @@ class TimeSeriesProcessor { const vector& _ys, const vector& _inputs, const vector& _funcs, - double* _out, + realtype* _out, bool _is_f_contiguous, const int _len) : ts(_ts), ys(_ys), inputs(_inputs), funcs(_funcs), out(_out), is_f_contiguous(_is_f_contiguous), len(_len) {} void process() { - vector y_buffer; - vector args; - vector results; + vector y_buffer; + vector args; + vector results; int count = 0; for (size_t i = 0; i < ts.size(); i++) { @@ -89,8 +91,8 @@ class TimeSeriesProcessor { } for (size_t j = 0; j < t.size(); j++) { - const double t_val = t(j); - const double* y_val = is_f_contiguous ? &y(0, j) : copy_to_buffer(y_buffer, y, j); + const realtype t_val = t(j); + const realtype* y_val = is_f_contiguous ? &y(0, j) : copy_to_buffer(y_buffer, y, j); args = { &t_val, y_val, input }; results = { &out[count] }; @@ -102,7 +104,7 @@ class TimeSeriesProcessor { } private: - const double* copy_to_buffer(vector& out, const py::detail::unchecked_reference& y, size_t j) { + const realtype* copy_to_buffer(vector& out, const py::detail::unchecked_reference& y, size_t j) { for (size_t i = 0; i < out.size(); ++i) { out[i] = y(i, j); } @@ -114,27 +116,27 @@ class TimeSeriesProcessor { const vector& ys; const vector& inputs; const vector& funcs; - double* out; + realtype* out; bool is_f_contiguous; int len; }; class TimeSeriesInterpolator { public: - TimeSeriesInterpolator(const np_array_realtype& t_interp_np, - const vector& ts_data, - const vector& ys_data, - const vector& yps_data, - const vector& inputs, - const vector& funcs, - double* out, - int len) - : t_interp_np(t_interp_np), ts_data_np(ts_data), ys_data_np(ys_data), - yps_data_np(yps_data), inputs_np(inputs), funcs(funcs), - out(out), len(len) {} + TimeSeriesInterpolator(const np_array_realtype& _t_interp_np, + const vector& _ts_data, + const vector& _ys_data, + const vector& _yps_data, + const vector& _inputs, + const vector& _funcs, + realtype* _out, + int _len) + : t_interp_np(_t_interp_np), ts_data_np(_ts_data), ys_data_np(_ys_data), + yps_data_np(_yps_data), inputs_np(_inputs), funcs(_funcs), + out(_out), len(_len) {} void process() { - vector y_interp; + vector y_interp; auto t_interp = t_interp_np.unchecked<1>(); ssize_t i_interp = 0; int count = 0; @@ -146,7 +148,7 @@ class TimeSeriesInterpolator { } // Preallocate c and d vectors - vector c, d; + vector c, d; // Main processing within bounds process_within_bounds(i_interp, count, y_interp, c, d, t_interp, N_interp); @@ -157,19 +159,19 @@ class TimeSeriesInterpolator { } } - void process_within_bounds(ssize_t& i_interp, int& count, vector& y_interp, - vector& c, vector& d, - const py::detail::unchecked_reference& t_interp, + void process_within_bounds(ssize_t& i_interp, int& count, vector& y_interp, + vector& c, vector& d, + const py::detail::unchecked_reference& t_interp, const ssize_t N_interp) { - vector args; - vector results; + vector args; + vector results; for (size_t i = 0; i < ts_data_np.size(); i++) { const auto& t_data = ts_data_np[i].unchecked<1>(); const auto& y_data = ys_data_np[i].unchecked<2>(); const auto& yp_data = yps_data_np[i].unchecked<2>(); const auto inputs_data = inputs_np[i].data(); const auto func = *funcs[i]; - const double t_data_final = t_data(t_data.size() - 1); + const realtype t_data_final = t_data(t_data.size() - 1); resize_arrays(y_interp, c, d, y_data.shape(0)); @@ -202,9 +204,9 @@ class TimeSeriesInterpolator { } } - void extrapolate_remaining(ssize_t& i_interp, int& count, vector& y_interp, - vector& c, vector& d, - const py::detail::unchecked_reference& t_interp, + void extrapolate_remaining(ssize_t& i_interp, int& count, vector& y_interp, + vector& c, vector& d, + const py::detail::unchecked_reference& t_interp, const ssize_t N_interp) { const auto& t_data = ts_data_np.back().unchecked<1>(); const auto& y_data = ys_data_np.back().unchecked<2>(); @@ -217,20 +219,22 @@ class TimeSeriesInterpolator { auto itp = HermiteInterpolator(t_data, y_data, yp_data); itp.compute_c_d(j, c, d); + vector args = { &t_interp(i_interp), y_interp.data(), inputs_data }; + vector results; for (; i_interp < N_interp; ++i_interp) { - const double t_interp_next = t_interp(i_interp); + const realtype t_interp_next = t_interp(i_interp); itp.interpolate(y_interp, t_interp_next, j, c, d); - vector args = { &t_interp_next, y_interp.data(), inputs_data }; - vector results = { &out[count] }; + args[0] = &t_interp_next; + results = { &out[count] }; func(args, results); count += len; } } - void resize_arrays(vector& y_interp, vector& c, vector& d, const int M) { + void resize_arrays(vector& y_interp, vector& c, vector& d, const int M) { if (y_interp.size() < M) { y_interp.resize(M); c.resize(M); @@ -245,11 +249,11 @@ class TimeSeriesInterpolator { const vector& yps_data_np; const vector& inputs_np; const vector& funcs; - double* out; + realtype* out; int len; }; -const py::array_t observe_hermite_interp_ND( +const np_array_realtype observe_hermite_interp_ND( const np_array_realtype& t_interp_np, const vector& ts_np, const vector& ys_np, @@ -259,7 +263,7 @@ const py::array_t observe_hermite_interp_ND( const vector sizes ) { const int size_tot = setup_observable(sizes); - py::array_t out_array(sizes); + py::array_t out_array(sizes); auto out = out_array.mutable_data(); TimeSeriesInterpolator(t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, out, size_tot / sizes.back()).process(); @@ -267,7 +271,7 @@ const py::array_t observe_hermite_interp_ND( return out_array; } -const py::array_t observe_ND( +const np_array_realtype observe_ND( const vector& ts_np, const vector& ys_np, const vector& inputs_np, @@ -276,7 +280,7 @@ const py::array_t observe_ND( const vector sizes ) { const int size_tot = setup_observable(sizes); - py::array_t out_array(sizes); + py::array_t out_array(sizes); auto out = out_array.mutable_data(); TimeSeriesProcessor(ts_np, ys_np, inputs_np, funcs, out, is_f_contiguous, size_tot / sizes.back()).process(); diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index 7d2a4a86fb..34232b8869 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -16,7 +16,7 @@ namespace py = pybind11; /** * @brief Observe and Hermite interpolate ND variables */ -const py::array_t observe_hermite_interp_ND( +const np_array_realtype observe_hermite_interp_ND( const np_array_realtype& t_interp, const vector& ts, const vector& ys, @@ -30,7 +30,7 @@ const py::array_t observe_hermite_interp_ND( /** * @brief Observe ND variables */ -const py::array_t observe_ND( +const np_array_realtype observe_ND( const vector& ts_np, const vector& ys_np, const vector& inputs_np, From 35c495670f0cee8e5609a8ab3d1e6194cff2330d Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:00:34 -0400 Subject: [PATCH 06/18] clean separation --- src/pybamm/solvers/processed_variable.py | 133 ++++++++++++----------- 1 file changed, 69 insertions(+), 64 deletions(-) diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 6e39113347..65d78db560 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -85,58 +85,45 @@ def __init__( self._entries_for_interp_raw = None self._coords_raw = None - def initialise(self, t=None): - t_observe, observe_raw = self._observe_raw_data(t) - if observe_raw and self._raw_data_initialized: + def initialise(self): + t = self.t_pts + if self._raw_data_initialized: entries = self._entries_raw - is_interpolated = False entries_for_interp = self._entries_for_interp_raw coords = self._coords_raw else: - entries, is_interpolated = self.observe(t_observe, observe_raw) - entries_for_interp, coords = self._interp_setup(entries, t_observe) + entries = self.observe() + entries_for_interp, coords = self._interp_setup(entries, t) - if (not self._raw_data_initialized) and observe_raw: - self._entries_raw = entries - self._entries_for_interp_raw = entries_for_interp - self._coords_raw = coords - self._raw_data_initialized = True + self._entries_raw = entries + self._entries_for_interp_raw = entries_for_interp + self._coords_raw = coords + self._raw_data_initialized = True - return t_observe, entries, is_interpolated, entries_for_interp, coords + return entries, entries_for_interp, coords - def observe(self, t, observe_raw): + def observe_and_interp(self, t): """ - Evaluate the base variable at the given time points and y values. + Interpolate the variable at the given time points and y values. """ + entries = self._observe_hermite_cpp(t) + entries = self._observe_postfix(entries, t) + return entries - is_sorted = observe_raw or _is_sorted(t) - if not is_sorted: - idxs_sort = np.argsort(t) - t = t[idxs_sort] + def observe(self): + """ + Evaluate the base variable at the given time points and y values. + """ + t = self.t_pts - if self.hermite_interpolation and not observe_raw: - pybamm.logger.debug( - "Observing and Hermite interpolating the variable in C++" - ) - entries = self._observe_hermite_cpp(t) - is_interpolated = True - elif self._linear_observable_cpp(t): - pybamm.logger.debug("Observing the variable raw data in C++") + if self._linear_observable_cpp(t): entries = self._observe_raw_cpp() - is_interpolated = False else: pybamm.logger.debug("Observing the variable raw data in Python") entries = self._observe_raw_python() - is_interpolated = False - - if not is_sorted: - idxs_unsort = np.arange(len(t))[idxs_sort] - - t = t[idxs_unsort] - entries = entries[..., idxs_unsort] entries = self._observe_postfix(entries, t) - return entries, is_interpolated + return entries def _setup_cpp_inputs(self): pybamm.logger.debug("Setting up C++ interpolation inputs") @@ -172,6 +159,7 @@ def _setup_cpp_inputs(self): return ts, ys, yps, funcs, inputs, is_f_contiguous def _observe_hermite_cpp(self, t): + pybamm.logger.debug("Observing and Hermite interpolating the variable in C++") self._check_interp(t) ts, ys, yps, funcs, inputs, _ = self._setup_cpp_inputs() @@ -181,6 +169,7 @@ def _observe_hermite_cpp(self, t): ) def _observe_raw_cpp(self): + pybamm.logger.debug("Observing the variable raw data in C++") ts, ys, _, funcs, inputs, is_f_contiguous = self._setup_cpp_inputs() sizes = self._size(self.t_pts) @@ -238,11 +227,25 @@ def _initialize_xr_data_array(self, entries, coords): return xr.DataArray(entries, coords=coords) def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): - t_observe, entries, is_interpolated, entries_for_interp, coords = ( - self.initialise(t) - ) + # 1. Check to see if we are interpolating exactly onto the solution time points + t_observe, observe_raw = self._observe_raw_data(t) + + # 2. Check if the time points are sorted and unique + is_sorted = observe_raw or _is_sorted(t_observe) + + # Sort them if not + if not is_sorted: + idxs_sort = np.argsort(t) + t = t[idxs_sort] + idxs_unsort = np.arange(len(t))[idxs_sort] - post_interpolate = ( + hermite_interp = self.hermite_interpolation and not observe_raw + if hermite_interp: + entries = self.observe_and_interp(t_observe) + else: + entries, entries_for_interp, coords = self.initialise() + + spatial_interp = ( (x is not None) or (r is not None) or (y is not None) @@ -250,25 +253,31 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): or (R is not None) ) - if is_interpolated and not post_interpolate: - return entries - return self._xr_interpolate( - t_observe, - entries_for_interp, - coords, - is_interpolated, - t, - x, - r, - y, - z, - R, - warn, - ) + if hermite_interp and not spatial_interp: + processed_entries = entries + else: + if hermite_interp: + entries_for_interp, coords = self._interp_setup(entries, t_observe) + processed_entries = self._xr_interpolate( + entries_for_interp, + coords, + hermite_interp, + t, + x, + r, + y, + z, + R, + warn, + ) + + if not is_sorted: + processed_entries = processed_entries[..., idxs_unsort] + + return processed_entries def _xr_interpolate( self, - t_observe, entries_for_interp, coords, is_interpolated, @@ -284,16 +293,12 @@ def _xr_interpolate( Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), using interpolation """ - if is_interpolated: - xr_data_array = self._initialize_xr_data_array(entries_for_interp, coords) - else: - if self._xr_data_array_raw is None: - if not self._raw_data_initialized: - self.initialise(t=None) - self._xr_data_array_raw = self._initialize_xr_data_array( - self._entries_for_interp_raw, self._coords_raw - ) + + if not is_interpolated and self._xr_data_array_raw is not None: xr_data_array = self._xr_data_array_raw + else: + xr_data_array = self._initialize_xr_data_array(entries_for_interp, coords) + kwargs = {"t": t, "x": x, "r": r, "y": y, "z": z, "R": R} if is_interpolated: kwargs["t"] = None From 2d1efcbf67d1c3d9f68a5e0e85713315e87ead41 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:16:06 -0400 Subject: [PATCH 07/18] good again --- .../solvers/c_solvers/idaklu/observe.cpp | 128 +++++++++++++----- .../solvers/c_solvers/idaklu/observe.hpp | 8 +- src/pybamm/solvers/processed_variable.py | 77 +++++------ 3 files changed, 136 insertions(+), 77 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp index 743911495f..bdb193574d 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -1,4 +1,6 @@ #include "observe.hpp" +#include +#include class HermiteInterpolator { public: @@ -66,7 +68,7 @@ class TimeSeriesProcessor { TimeSeriesProcessor(const vector& _ts, const vector& _ys, const vector& _inputs, - const vector& _funcs, + const std::vector>& _funcs, realtype* _out, bool _is_f_contiguous, const int _len) @@ -85,6 +87,14 @@ class TimeSeriesProcessor { const auto input = inputs[i].data(); const auto func = *funcs[i]; + std::vector iw(funcs[i]->sz_iw()); + std::vector w(funcs[i]->sz_w()); + args.resize(funcs[i]->sz_arg()); + args[2] = input; + + // Output buffer + results.resize(funcs[i]->sz_res()); + int M = y.shape(0); if (!is_f_contiguous && y_buffer.size() < M) { y_buffer.resize(M); @@ -94,9 +104,11 @@ class TimeSeriesProcessor { const realtype t_val = t(j); const realtype* y_val = is_f_contiguous ? &y(0, j) : copy_to_buffer(y_buffer, y, j); - args = { &t_val, y_val, input }; - results = { &out[count] }; - func(args, results); + args[0] = &t_val; + args[1] = y_val; + results[0] = &out[count]; + + func(casadi::get_ptr(args), casadi::get_ptr(results), casadi::get_ptr(iw), casadi::get_ptr(w), 0); count += len; } @@ -115,7 +127,7 @@ class TimeSeriesProcessor { const vector& ts; const vector& ys; const vector& inputs; - const vector& funcs; + const std::vector>& funcs; realtype* out; bool is_f_contiguous; int len; @@ -128,7 +140,7 @@ class TimeSeriesInterpolator { const vector& _ys_data, const vector& _yps_data, const vector& _inputs, - const vector& _funcs, + const std::vector>& _funcs, realtype* _out, int _len) : t_interp_np(_t_interp_np), ts_data_np(_ts_data), ys_data_np(_ys_data), @@ -136,7 +148,6 @@ class TimeSeriesInterpolator { out(_out), len(_len) {} void process() { - vector y_interp; auto t_interp = t_interp_np.unchecked<1>(); ssize_t i_interp = 0; int count = 0; @@ -147,35 +158,55 @@ class TimeSeriesInterpolator { N_data += ts.size(); } - // Preallocate c and d vectors - vector c, d; + // Preallocate vectors + vector c, d, y_interp; + + vector args; + vector results; + vector iw; + vector w; // Main processing within bounds - process_within_bounds(i_interp, count, y_interp, c, d, t_interp, N_interp); + process_within_bounds(i_interp, count, t_interp, N_interp, args, results, iw, w, y_interp, c, d); // Extrapolation for remaining points if (i_interp < N_interp) { - extrapolate_remaining(i_interp, count, y_interp, c, d, t_interp, N_interp); + extrapolate_remaining(i_interp, count, t_interp, N_interp, args, results, iw, w, y_interp, c, d); } } - void process_within_bounds(ssize_t& i_interp, int& count, vector& y_interp, - vector& c, vector& d, - const py::detail::unchecked_reference& t_interp, - const ssize_t N_interp) { - vector args; - vector results; + void process_within_bounds( + ssize_t& i_interp, + int& count, + const py::detail::unchecked_reference& t_interp, + const ssize_t N_interp, + vector& args, + vector& results, + vector& iw, + vector& w, + vector& y_interp, + vector& c, + vector& d + ) { for (size_t i = 0; i < ts_data_np.size(); i++) { const auto& t_data = ts_data_np[i].unchecked<1>(); const auto& y_data = ys_data_np[i].unchecked<2>(); const auto& yp_data = yps_data_np[i].unchecked<2>(); - const auto inputs_data = inputs_np[i].data(); + const auto input = inputs_np[i].data(); const auto func = *funcs[i]; const realtype t_data_final = t_data(t_data.size() - 1); resize_arrays(y_interp, c, d, y_data.shape(0)); - args = { &t_interp(i_interp), y_interp.data(), inputs_data }; + iw.resize(funcs[i]->sz_iw()); + w.resize(funcs[i]->sz_w()); + args.resize(funcs[i]->sz_arg()); + + args[1] = y_interp.data(); + args[2] = input; + + // Output buffer + results.resize(funcs[i]->sz_res()); ssize_t j = 0; ssize_t j_prev = -1; @@ -193,9 +224,10 @@ class TimeSeriesInterpolator { } itp.interpolate(y_interp, t_interp(i_interp), j, c, d); - results = { &out[count] }; + args[0] = &t_interp(i_interp); - func(args, results); + results[0] = &out[count]; + func(casadi::get_ptr(args), casadi::get_ptr(results), casadi::get_ptr(iw), casadi::get_ptr(w), 0); count += len; ++i_interp; @@ -204,10 +236,19 @@ class TimeSeriesInterpolator { } } - void extrapolate_remaining(ssize_t& i_interp, int& count, vector& y_interp, - vector& c, vector& d, - const py::detail::unchecked_reference& t_interp, - const ssize_t N_interp) { + void extrapolate_remaining( + ssize_t& i_interp, + int& count, + const py::detail::unchecked_reference& t_interp, + const ssize_t N_interp, + vector& args, + vector& results, + vector& iw, + vector& w, + vector& y_interp, + vector& c, + vector& d + ) { const auto& t_data = ts_data_np.back().unchecked<1>(); const auto& y_data = ys_data_np.back().unchecked<2>(); const auto& yp_data = yps_data_np.back().unchecked<2>(); @@ -216,19 +257,20 @@ class TimeSeriesInterpolator { const ssize_t j = t_data.size() - 2; resize_arrays(y_interp, c, d, y_data.shape(0)); + iw.resize(funcs.back()->sz_iw()); + w.resize(funcs.back()->sz_w()); + args.resize(funcs.back()->sz_arg()); auto itp = HermiteInterpolator(t_data, y_data, yp_data); itp.compute_c_d(j, c, d); - vector args = { &t_interp(i_interp), y_interp.data(), inputs_data }; - vector results; for (; i_interp < N_interp; ++i_interp) { const realtype t_interp_next = t_interp(i_interp); itp.interpolate(y_interp, t_interp_next, j, c, d); args[0] = &t_interp_next; - results = { &out[count] }; - func(args, results); + results[0] = &out[count]; + func(casadi::get_ptr(args), casadi::get_ptr(results), casadi::get_ptr(iw), casadi::get_ptr(w), 0); count += len; } @@ -248,7 +290,7 @@ class TimeSeriesInterpolator { const vector& ys_data_np; const vector& yps_data_np; const vector& inputs_np; - const vector& funcs; + const std::vector>& funcs; realtype* out; int len; }; @@ -259,10 +301,11 @@ const np_array_realtype observe_hermite_interp_ND( const vector& ys_np, const vector& yps_np, const vector& inputs_np, - const vector& funcs, + const vector& strings, const vector sizes ) { const int size_tot = setup_observable(sizes); + const auto& funcs = setup_casadi_funcs(strings); py::array_t out_array(sizes); auto out = out_array.mutable_data(); @@ -275,11 +318,12 @@ const np_array_realtype observe_ND( const vector& ts_np, const vector& ys_np, const vector& inputs_np, - const vector& funcs, + const vector& strings, const bool is_f_contiguous, const vector sizes ) { const int size_tot = setup_observable(sizes); + const auto& funcs = setup_casadi_funcs(strings); py::array_t out_array(sizes); auto out = out_array.mutable_data(); @@ -287,3 +331,23 @@ const np_array_realtype observe_ND( return out_array; } + +const std::vector> setup_casadi_funcs(const std::vector& strings) { + std::unordered_map> function_cache; + std::vector> funcs(strings.size()); + + for (size_t i = 0; i < strings.size(); ++i) { + const std::string& str = strings[i]; + + // Check if function is already in the local cache + if (function_cache.find(str) == function_cache.end()) { + // If not in the cache, create a new casadi::Function::deserialize and store it + function_cache[str] = std::make_shared(casadi::Function::deserialize(str)); + } + + // Retrieve the function from the cache as a shared pointer + funcs[i] = function_cache[str]; + } + + return funcs; +} diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index 34232b8869..06b0ebb11a 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "common.hpp" #include @@ -22,7 +24,7 @@ const np_array_realtype observe_hermite_interp_ND( const vector& ys, const vector& yps, const vector& inputs, - const vector& funcs, + const vector& strings, const vector sizes ); @@ -34,11 +36,13 @@ const np_array_realtype observe_ND( const vector& ts_np, const vector& ys_np, const vector& inputs_np, - const vector& funcs, + const vector& strings, const bool is_f_contiguous, const vector sizes ); +const std::vector> setup_casadi_funcs(const std::vector& strings); + int setup_observable(const vector& sizes); #endif // PYBAMM_CREATE_OBSERVE_HPP diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 65d78db560..47b9211621 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -86,13 +86,14 @@ def __init__( self._coords_raw = None def initialise(self): - t = self.t_pts if self._raw_data_initialized: entries = self._entries_raw entries_for_interp = self._entries_for_interp_raw coords = self._coords_raw else: - entries = self.observe() + entries = self.observe_raw() + + t = self.t_pts entries_for_interp, coords = self._interp_setup(entries, t) self._entries_raw = entries @@ -107,23 +108,21 @@ def observe_and_interp(self, t): Interpolate the variable at the given time points and y values. """ entries = self._observe_hermite_cpp(t) - entries = self._observe_postfix(entries, t) - return entries + return self._observe_postfix(entries, t) - def observe(self): + def observe_raw(self): """ Evaluate the base variable at the given time points and y values. """ t = self.t_pts - if self._linear_observable_cpp(t): + # For small number of points, use Python + if pybamm.has_idaklu() and (t.size > 1): entries = self._observe_raw_cpp() else: - pybamm.logger.debug("Observing the variable raw data in Python") entries = self._observe_raw_python() - entries = self._observe_postfix(entries, t) - return entries + return self._observe_postfix(entries, t) def _setup_cpp_inputs(self): pybamm.logger.debug("Setting up C++ interpolation inputs") @@ -143,11 +142,7 @@ def _setup_cpp_inputs(self): for i, vars in enumerate(self.base_variables_casadi): if vars not in funcs_unique: - funcs_unique[vars] = ( - pybamm.solvers.idaklu_solver.idaklu.generate_function( - vars.serialize() - ) - ) + funcs_unique[vars] = vars.serialize() funcs[i] = funcs_unique[vars] inputs = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray( @@ -218,17 +213,9 @@ def _process_spatial_variable_names(self, spatial_variable): f"Spatial variable name not recognized for {spatial_variable}" ) - def _initialize_xr_data_array(self, entries, coords): - """ - Initialize the xarray DataArray for interpolation. We don't do this by - default as it has some overhead (~75 us) and sometimes we only need the entries - of the processed variable, not the xarray object for interpolation. - """ - return xr.DataArray(entries, coords=coords) - def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): # 1. Check to see if we are interpolating exactly onto the solution time points - t_observe, observe_raw = self._observe_raw_data(t) + t_observe, observe_raw = self._check_observe_raw(t) # 2. Check if the time points are sorted and unique is_sorted = observe_raw or _is_sorted(t_observe) @@ -239,8 +226,8 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): t = t[idxs_sort] idxs_unsort = np.arange(len(t))[idxs_sort] - hermite_interp = self.hermite_interpolation and not observe_raw - if hermite_interp: + time_interp = self.hermite_interpolation and not observe_raw + if time_interp: entries = self.observe_and_interp(t_observe) else: entries, entries_for_interp, coords = self.initialise() @@ -253,15 +240,15 @@ def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): or (R is not None) ) - if hermite_interp and not spatial_interp: + if time_interp and not spatial_interp: processed_entries = entries else: - if hermite_interp: + if time_interp: entries_for_interp, coords = self._interp_setup(entries, t_observe) processed_entries = self._xr_interpolate( entries_for_interp, coords, - hermite_interp, + time_interp, t, x, r, @@ -280,7 +267,7 @@ def _xr_interpolate( self, entries_for_interp, coords, - is_interpolated, + time_interp, t=None, x=None, r=None, @@ -294,28 +281,30 @@ def _xr_interpolate( using interpolation """ - if not is_interpolated and self._xr_data_array_raw is not None: + if not time_interp and self._xr_data_array_raw is not None: xr_data_array = self._xr_data_array_raw else: - xr_data_array = self._initialize_xr_data_array(entries_for_interp, coords) + xr_data_array = xr.DataArray(entries_for_interp, coords=coords) kwargs = {"t": t, "x": x, "r": r, "y": y, "z": z, "R": R} - if is_interpolated: + if time_interp: kwargs["t"] = None # Remove any None arguments kwargs = {key: value for key, value in kwargs.items() if value is not None} # Use xarray interpolation, return numpy array return xr_data_array.interp(**kwargs).values - def _linear_observable_cpp(self, t): - """ - For a small number of time points, it is faster to evaluate the base variable in - Python. For large number of time points, it is faster to evaluate the base - variable in C++. + def _check_observe_raw(self, t): """ - return pybamm.has_idaklu() and (t is not None) and (np.asarray(t).size > 1) + Checks if the raw data should be observed exactly at the solution time points + + Args: + t (np.ndarray, list, None): time points to observe - def _observe_raw_data(self, t): + Returns: + t_observe (np.ndarray): time points to observe + observe_raw (bool): True if observing the raw data + """ observe_raw = (t is None) or ( np.asarray(t).size == len(self.t_pts) and np.all(t == self.t_pts) ) @@ -349,10 +338,9 @@ def _check_interp(self, t): @property def entries(self): """ - Returns the raw data entries of the processed variable. This is the data that - is used for interpolation. If the processed variable has not been initialized - (i.e. the entries have not been calculated), then the processed variable is - initialized first. + Returns the raw data entries of the processed variable. If the processed + variable has not been initialized (i.e. the entries have not been + calculated), then the processed variable is initialized first. """ if not self._raw_data_initialized: self.initialise() @@ -469,6 +457,7 @@ def __init__( ) def _observe_raw_python(self): + pybamm.logger.debug("Observing the variable raw data in Python") # initialise empty array of the correct size entries = np.empty(self._size(self.t_pts)) idx = 0 @@ -546,6 +535,7 @@ def __init__( ) def _observe_raw_python(self): + pybamm.logger.debug("Observing the variable raw data in Python") entries = np.empty(self._size(self.t_pts)) # Evaluate the base_variable index-by-index @@ -661,6 +651,7 @@ def _observe_raw_python(self): """ Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. """ + pybamm.logger.debug("Observing the variable raw data in Python") first_dim_size, second_dim_size, t_size = self._size(self.t_pts) entries = np.empty((first_dim_size, second_dim_size, t_size)) From a353d0a21b774559d39170ee19e3fbadeab81e0e Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:49:31 -0400 Subject: [PATCH 08/18] private members --- .../solvers/c_solvers/idaklu/observe.cpp | 168 ++++++++---------- .../solvers/c_solvers/idaklu/observe.hpp | 16 +- 2 files changed, 82 insertions(+), 102 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp index bdb193574d..68a1a880a4 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -48,7 +48,7 @@ class HermiteInterpolator { int setup_observable(const vector& sizes) { if (sizes.empty()) { - throw std::invalid_argument("sizes must have at least one element"); + throw std::invalid_argument("output array must have at least one element"); } int size_tot = 1; @@ -57,7 +57,7 @@ int setup_observable(const vector& sizes) { } if (size_tot == 0) { - throw std::invalid_argument("sizes must have at least one element"); + throw std::invalid_argument("output array must have at least one element"); } return size_tot; @@ -68,49 +68,35 @@ class TimeSeriesProcessor { TimeSeriesProcessor(const vector& _ts, const vector& _ys, const vector& _inputs, - const std::vector>& _funcs, + const vector>& _funcs, realtype* _out, - bool _is_f_contiguous, + const bool _is_f_contiguous, const int _len) : ts(_ts), ys(_ys), inputs(_inputs), funcs(_funcs), out(_out), is_f_contiguous(_is_f_contiguous), len(_len) {} void process() { - vector y_buffer; - vector args; - vector results; - - int count = 0; + int i_out = 0; for (size_t i = 0; i < ts.size(); i++) { const auto& t = ts[i].unchecked<1>(); const auto& y = ys[i].unchecked<2>(); const auto input = inputs[i].data(); const auto func = *funcs[i]; - std::vector iw(funcs[i]->sz_iw()); - std::vector w(funcs[i]->sz_w()); - args.resize(funcs[i]->sz_arg()); + resize_arrays(y.shape(0), funcs[i]); args[2] = input; - // Output buffer - results.resize(funcs[i]->sz_res()); - - int M = y.shape(0); - if (!is_f_contiguous && y_buffer.size() < M) { - y_buffer.resize(M); - } - for (size_t j = 0; j < t.size(); j++) { const realtype t_val = t(j); const realtype* y_val = is_f_contiguous ? &y(0, j) : copy_to_buffer(y_buffer, y, j); args[0] = &t_val; args[1] = y_val; - results[0] = &out[count]; + results[0] = &out[i_out]; - func(casadi::get_ptr(args), casadi::get_ptr(results), casadi::get_ptr(iw), casadi::get_ptr(w), 0); + func(args.data(), results.data(), iw.data(), w.data(), 0); - count += len; + i_out += len; } } } @@ -124,13 +110,28 @@ class TimeSeriesProcessor { return out.data(); } + void resize_arrays(const int M, std::shared_ptr func) { + args.resize(func->sz_arg()); + results.resize(func->sz_res()); + iw.resize(func->sz_iw()); + w.resize(func->sz_w()); + if (!is_f_contiguous && y_buffer.size() < M) { + y_buffer.resize(M); + } + } + const vector& ts; const vector& ys; const vector& inputs; - const std::vector>& funcs; + const vector>& funcs; realtype* out; - bool is_f_contiguous; + const bool is_f_contiguous; int len; + vector y_buffer; + vector args; + vector results; + vector iw; + vector w; }; class TimeSeriesInterpolator { @@ -140,9 +141,9 @@ class TimeSeriesInterpolator { const vector& _ys_data, const vector& _yps_data, const vector& _inputs, - const std::vector>& _funcs, + const vector>& _funcs, realtype* _out, - int _len) + const int _len) : t_interp_np(_t_interp_np), ts_data_np(_ts_data), ys_data_np(_ys_data), yps_data_np(_yps_data), inputs_np(_inputs), funcs(_funcs), out(_out), len(_len) {} @@ -150,7 +151,7 @@ class TimeSeriesInterpolator { void process() { auto t_interp = t_interp_np.unchecked<1>(); ssize_t i_interp = 0; - int count = 0; + int i_out = 0; ssize_t N_data = 0; const ssize_t N_interp = t_interp.size(); @@ -158,59 +159,41 @@ class TimeSeriesInterpolator { N_data += ts.size(); } - // Preallocate vectors - vector c, d, y_interp; - - vector args; - vector results; - vector iw; - vector w; - // Main processing within bounds - process_within_bounds(i_interp, count, t_interp, N_interp, args, results, iw, w, y_interp, c, d); + process_within_bounds(i_interp, i_out, t_interp, N_interp); // Extrapolation for remaining points if (i_interp < N_interp) { - extrapolate_remaining(i_interp, count, t_interp, N_interp, args, results, iw, w, y_interp, c, d); + extrapolate_remaining(i_interp, i_out, t_interp, N_interp); } } void process_within_bounds( ssize_t& i_interp, - int& count, + int& i_out, const py::detail::unchecked_reference& t_interp, - const ssize_t N_interp, - vector& args, - vector& results, - vector& iw, - vector& w, - vector& y_interp, - vector& c, - vector& d + const ssize_t N_interp ) { for (size_t i = 0; i < ts_data_np.size(); i++) { const auto& t_data = ts_data_np[i].unchecked<1>(); + const realtype t_data_final = t_data(t_data.size() - 1); + // Continue if t_interp(i_interp) <= t_data_final is not true for the first step + if (t_interp(i_interp) > t_data_final) { + continue; + } + const auto& y_data = ys_data_np[i].unchecked<2>(); const auto& yp_data = yps_data_np[i].unchecked<2>(); const auto input = inputs_np[i].data(); const auto func = *funcs[i]; - const realtype t_data_final = t_data(t_data.size() - 1); - resize_arrays(y_interp, c, d, y_data.shape(0)); - - iw.resize(funcs[i]->sz_iw()); - w.resize(funcs[i]->sz_w()); - args.resize(funcs[i]->sz_arg()); - - args[1] = y_interp.data(); + resize_arrays(y_data.shape(0), funcs[i]); + args[1] = y_buffer.data(); args[2] = input; - // Output buffer - results.resize(funcs[i]->sz_res()); - ssize_t j = 0; ssize_t j_prev = -1; - auto itp = HermiteInterpolator(t_data, y_data, yp_data); + const auto itp = HermiteInterpolator(t_data, y_data, yp_data); while (i_interp < N_interp && t_interp(i_interp) <= t_data_final) { for (; j < t_data.size() - 2; ++j) { if (t_data(j) <= t_interp(i_interp) && t_interp(i_interp) <= t_data(j + 1)) { @@ -223,13 +206,13 @@ class TimeSeriesInterpolator { itp.compute_c_d(j, c, d); } - itp.interpolate(y_interp, t_interp(i_interp), j, c, d); + itp.interpolate(y_buffer, t_interp(i_interp), j, c, d); args[0] = &t_interp(i_interp); - results[0] = &out[count]; - func(casadi::get_ptr(args), casadi::get_ptr(results), casadi::get_ptr(iw), casadi::get_ptr(w), 0); + results[0] = &out[i_out]; + func(args.data(), results.data(), iw.data(), w.data(), 0); - count += len; + i_out += len; ++i_interp; j_prev = j; } @@ -238,47 +221,43 @@ class TimeSeriesInterpolator { void extrapolate_remaining( ssize_t& i_interp, - int& count, + int& i_out, const py::detail::unchecked_reference& t_interp, - const ssize_t N_interp, - vector& args, - vector& results, - vector& iw, - vector& w, - vector& y_interp, - vector& c, - vector& d + const ssize_t N_interp ) { const auto& t_data = ts_data_np.back().unchecked<1>(); const auto& y_data = ys_data_np.back().unchecked<2>(); const auto& yp_data = yps_data_np.back().unchecked<2>(); - const auto inputs_data = inputs_np.back().data(); + const auto input = inputs_np.back().data(); const auto func = *funcs.back(); const ssize_t j = t_data.size() - 2; - resize_arrays(y_interp, c, d, y_data.shape(0)); - iw.resize(funcs.back()->sz_iw()); - w.resize(funcs.back()->sz_w()); - args.resize(funcs.back()->sz_arg()); + resize_arrays(y_data.shape(0), funcs.back()); + args[1] = y_buffer.data(); + args[2] = input; - auto itp = HermiteInterpolator(t_data, y_data, yp_data); + const auto itp = HermiteInterpolator(t_data, y_data, yp_data); itp.compute_c_d(j, c, d); for (; i_interp < N_interp; ++i_interp) { const realtype t_interp_next = t_interp(i_interp); - itp.interpolate(y_interp, t_interp_next, j, c, d); + itp.interpolate(y_buffer, t_interp_next, j, c, d); args[0] = &t_interp_next; - results[0] = &out[count]; - func(casadi::get_ptr(args), casadi::get_ptr(results), casadi::get_ptr(iw), casadi::get_ptr(w), 0); + results[0] = &out[i_out]; + func(args.data(), results.data(), iw.data(), w.data(), 0); - count += len; + i_out += len; } } - void resize_arrays(vector& y_interp, vector& c, vector& d, const int M) { - if (y_interp.size() < M) { - y_interp.resize(M); + void resize_arrays(const int M, std::shared_ptr func) { + args.resize(func->sz_arg()); + results.resize(func->sz_res()); + iw.resize(func->sz_iw()); + w.resize(func->sz_w()); + if (y_buffer.size() < M) { + y_buffer.resize(M); c.resize(M); d.resize(M); } @@ -290,9 +269,16 @@ class TimeSeriesInterpolator { const vector& ys_data_np; const vector& yps_data_np; const vector& inputs_np; - const std::vector>& funcs; + const vector>& funcs; realtype* out; - int len; + const int len; + vector c; + vector d; + vector y_buffer; + vector args; + vector results; + vector iw; + vector w; }; const np_array_realtype observe_hermite_interp_ND( @@ -302,7 +288,7 @@ const np_array_realtype observe_hermite_interp_ND( const vector& yps_np, const vector& inputs_np, const vector& strings, - const vector sizes + const vector& sizes ) { const int size_tot = setup_observable(sizes); const auto& funcs = setup_casadi_funcs(strings); @@ -320,7 +306,7 @@ const np_array_realtype observe_ND( const vector& inputs_np, const vector& strings, const bool is_f_contiguous, - const vector sizes + const vector& sizes ) { const int size_tot = setup_observable(sizes); const auto& funcs = setup_casadi_funcs(strings); @@ -332,9 +318,9 @@ const np_array_realtype observe_ND( return out_array; } -const std::vector> setup_casadi_funcs(const std::vector& strings) { +const vector> setup_casadi_funcs(const vector& strings) { std::unordered_map> function_cache; - std::vector> funcs(strings.size()); + vector> funcs(strings.size()); for (size_t i = 0; i < strings.size(); ++i) { const std::string& str = strings[i]; diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index 06b0ebb11a..50c24e2b42 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -1,19 +1,13 @@ #ifndef PYBAMM_CREATE_OBSERVE_HPP #define PYBAMM_CREATE_OBSERVE_HPP -#include "IDAKLUSolverOpenMP_solvers.hpp" -#include #include -#include #include #include #include "common.hpp" - -#include -#include // For numpy support in pybind11 #include - -namespace py = pybind11; +#include +using std::vector; /** * @brief Observe and Hermite interpolate ND variables @@ -25,7 +19,7 @@ const np_array_realtype observe_hermite_interp_ND( const vector& yps, const vector& inputs, const vector& strings, - const vector sizes + const vector& sizes ); @@ -38,10 +32,10 @@ const np_array_realtype observe_ND( const vector& inputs_np, const vector& strings, const bool is_f_contiguous, - const vector sizes + const vector& sizes ); -const std::vector> setup_casadi_funcs(const std::vector& strings); +const vector> setup_casadi_funcs(const vector& strings); int setup_observable(const vector& sizes); From 8df12c48d24b3d273e1b5220c07e91dd6d168218 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:08:35 -0400 Subject: [PATCH 09/18] cleanup --- src/pybamm/solvers/c_solvers/idaklu.cpp | 28 ++--- .../solvers/c_solvers/idaklu/common.hpp | 4 - .../solvers/c_solvers/idaklu/observe.cpp | 112 +++++++++--------- .../solvers/c_solvers/idaklu/observe.hpp | 10 +- src/pybamm/solvers/processed_variable.py | 26 ++-- 5 files changed, 90 insertions(+), 90 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu.cpp b/src/pybamm/solvers/c_solvers/idaklu.cpp index 173fade8fd..82a3cbe91c 100644 --- a/src/pybamm/solvers/c_solvers/idaklu.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu.cpp @@ -75,25 +75,25 @@ PYBIND11_MODULE(idaklu, m) py::arg("options"), py::return_value_policy::take_ownership); - m.def("observe_ND", &observe_ND, - "Observe ND variables", - py::arg("ts_np"), - py::arg("ys_np"), - py::arg("inputs_np"), + m.def("observe", &observe, + "Observe variables", + py::arg("ts"), + py::arg("ys"), + py::arg("inputs"), py::arg("funcs"), py::arg("is_f_contiguous"), - py::arg("sizes"), + py::arg("shape"), py::return_value_policy::take_ownership); - m.def("observe_hermite_interp_ND", &observe_hermite_interp_ND, - "Observe ND variables", - py::arg("t_interp_np"), - py::arg("ts_np"), - py::arg("ys_np"), - py::arg("yps_np"), - py::arg("inputs_np"), + m.def("observe_hermite_interp", &observe_hermite_interp, + "Observe and Hermite interpolate variables", + py::arg("t_interp"), + py::arg("ts"), + py::arg("ys"), + py::arg("yps"), + py::arg("inputs"), py::arg("funcs"), - py::arg("sizes"), + py::arg("shape"), py::return_value_policy::take_ownership); #ifdef IREE_ENABLE diff --git a/src/pybamm/solvers/c_solvers/idaklu/common.hpp b/src/pybamm/solvers/c_solvers/idaklu/common.hpp index 8318d9596f..90672080b6 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/common.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/common.hpp @@ -27,10 +27,6 @@ #include /* access to sparse SUNMatrix */ #include /* access to dense SUNMatrix */ -#include -#include -#include - #include #include diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp index 68a1a880a4..3980a2d8ea 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -9,7 +9,7 @@ class HermiteInterpolator { const py::detail::unchecked_reference& yp) : t(t), y(y), yp(yp) {} - void compute_c_d(size_t j, vector& c, vector& d) const { + void compute_c_d(const size_t j, vector& c, vector& d) const { // Called at the start of each interval const realtype h_full = t(j + 1) - t(j); const realtype inv_h = 1.0 / h_full; @@ -27,16 +27,20 @@ class HermiteInterpolator { } } - void interpolate(vector& out, realtype t_interp, size_t j, vector& c, vector& d) const { + void interpolate(vector& entries, + realtype t_interp, + const size_t j, + vector& c, + vector& d) const { // Must be called after compute_c_d const realtype h = t_interp - t(j); const realtype h2 = h * h; const realtype h3 = h2 * h; - for (size_t i = 0; i < out.size(); ++i) { + for (size_t i = 0; i < entries.size(); ++i) { realtype y_ij = y(i, j); realtype yp_ij = yp(i, j); - out[i] = y_ij + yp_ij * h + c[i] * h2 + d[i] * h3; + entries[i] = y_ij + yp_ij * h + c[i] * h2 + d[i] * h3; } } @@ -46,21 +50,18 @@ class HermiteInterpolator { const py::detail::unchecked_reference& yp; }; -int setup_observable(const vector& sizes) { - if (sizes.empty()) { - throw std::invalid_argument("output array must have at least one element"); - } - - int size_tot = 1; - for (const auto& size : sizes) { - size_tot *= size; +int _setup_len_spatial(const std::vector& shape) { + // Calculate the product of all dimensions except the last (spatial dimensions) + int size_spatial = 1; + for (size_t i = 0; i < shape.size() - 1; ++i) { + size_spatial *= shape[i]; } - if (size_tot == 0) { + if (size_spatial == 0 || shape.back() == 0) { throw std::invalid_argument("output array must have at least one element"); } - return size_tot; + return size_spatial; } class TimeSeriesProcessor { @@ -69,14 +70,14 @@ class TimeSeriesProcessor { const vector& _ys, const vector& _inputs, const vector>& _funcs, - realtype* _out, + realtype* _entries, const bool _is_f_contiguous, - const int _len) + const int _size_spatial) : ts(_ts), ys(_ys), inputs(_inputs), funcs(_funcs), - out(_out), is_f_contiguous(_is_f_contiguous), len(_len) {} + entries(_entries), is_f_contiguous(_is_f_contiguous), size_spatial(_size_spatial) {} void process() { - int i_out = 0; + int i_entries = 0; for (size_t i = 0; i < ts.size(); i++) { const auto& t = ts[i].unchecked<1>(); const auto& y = ys[i].unchecked<2>(); @@ -92,22 +93,25 @@ class TimeSeriesProcessor { args[0] = &t_val; args[1] = y_val; - results[0] = &out[i_out]; + results[0] = &entries[i_entries]; func(args.data(), results.data(), iw.data(), w.data(), 0); - i_out += len; + i_entries += size_spatial; } } } private: - const realtype* copy_to_buffer(vector& out, const py::detail::unchecked_reference& y, size_t j) { - for (size_t i = 0; i < out.size(); ++i) { - out[i] = y(i, j); + const realtype* copy_to_buffer( + vector& entries, + const py::detail::unchecked_reference& y, + size_t j) { + for (size_t i = 0; i < entries.size(); ++i) { + entries[i] = y(i, j); } - return out.data(); + return entries.data(); } void resize_arrays(const int M, std::shared_ptr func) { @@ -124,9 +128,9 @@ class TimeSeriesProcessor { const vector& ys; const vector& inputs; const vector>& funcs; - realtype* out; + realtype* entries; const bool is_f_contiguous; - int len; + int size_spatial; vector y_buffer; vector args; vector results; @@ -136,22 +140,22 @@ class TimeSeriesProcessor { class TimeSeriesInterpolator { public: - TimeSeriesInterpolator(const np_array_realtype& _t_interp_np, + TimeSeriesInterpolator(const np_array_realtype& _t_interp, const vector& _ts_data, const vector& _ys_data, const vector& _yps_data, const vector& _inputs, const vector>& _funcs, - realtype* _out, - const int _len) - : t_interp_np(_t_interp_np), ts_data_np(_ts_data), ys_data_np(_ys_data), + realtype* _entries, + const int _size_spatial) + : t_interp_np(_t_interp), ts_data_np(_ts_data), ys_data_np(_ys_data), yps_data_np(_yps_data), inputs_np(_inputs), funcs(_funcs), - out(_out), len(_len) {} + entries(_entries), size_spatial(_size_spatial) {} void process() { auto t_interp = t_interp_np.unchecked<1>(); ssize_t i_interp = 0; - int i_out = 0; + int i_entries = 0; ssize_t N_data = 0; const ssize_t N_interp = t_interp.size(); @@ -160,17 +164,17 @@ class TimeSeriesInterpolator { } // Main processing within bounds - process_within_bounds(i_interp, i_out, t_interp, N_interp); + process_within_bounds(i_interp, i_entries, t_interp, N_interp); // Extrapolation for remaining points if (i_interp < N_interp) { - extrapolate_remaining(i_interp, i_out, t_interp, N_interp); + extrapolate_remaining(i_interp, i_entries, t_interp, N_interp); } } void process_within_bounds( ssize_t& i_interp, - int& i_out, + int& i_entries, const py::detail::unchecked_reference& t_interp, const ssize_t N_interp ) { @@ -209,10 +213,10 @@ class TimeSeriesInterpolator { itp.interpolate(y_buffer, t_interp(i_interp), j, c, d); args[0] = &t_interp(i_interp); - results[0] = &out[i_out]; + results[0] = &entries[i_entries]; func(args.data(), results.data(), iw.data(), w.data(), 0); - i_out += len; + i_entries += size_spatial; ++i_interp; j_prev = j; } @@ -221,7 +225,7 @@ class TimeSeriesInterpolator { void extrapolate_remaining( ssize_t& i_interp, - int& i_out, + int& i_entries, const py::detail::unchecked_reference& t_interp, const ssize_t N_interp ) { @@ -244,10 +248,10 @@ class TimeSeriesInterpolator { itp.interpolate(y_buffer, t_interp_next, j, c, d); args[0] = &t_interp_next; - results[0] = &out[i_out]; + results[0] = &entries[i_entries]; func(args.data(), results.data(), iw.data(), w.data(), 0); - i_out += len; + i_entries += size_spatial; } } @@ -270,8 +274,8 @@ class TimeSeriesInterpolator { const vector& yps_data_np; const vector& inputs_np; const vector>& funcs; - realtype* out; - const int len; + realtype* entries; + const int size_spatial; vector c; vector d; vector y_buffer; @@ -281,39 +285,39 @@ class TimeSeriesInterpolator { vector w; }; -const np_array_realtype observe_hermite_interp_ND( +const np_array_realtype observe_hermite_interp( const np_array_realtype& t_interp_np, const vector& ts_np, const vector& ys_np, const vector& yps_np, const vector& inputs_np, const vector& strings, - const vector& sizes + const vector& shape ) { - const int size_tot = setup_observable(sizes); + const int size_spatial = _setup_len_spatial(shape); const auto& funcs = setup_casadi_funcs(strings); - py::array_t out_array(sizes); - auto out = out_array.mutable_data(); + py::array_t out_array(shape); + auto entries = out_array.mutable_data(); - TimeSeriesInterpolator(t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, out, size_tot / sizes.back()).process(); + TimeSeriesInterpolator(t_interp_np, ts_np, ys_np, yps_np, inputs_np, funcs, entries, size_spatial).process(); return out_array; } -const np_array_realtype observe_ND( +const np_array_realtype observe( const vector& ts_np, const vector& ys_np, const vector& inputs_np, const vector& strings, const bool is_f_contiguous, - const vector& sizes + const vector& shape ) { - const int size_tot = setup_observable(sizes); + const int size_spatial = _setup_len_spatial(shape); const auto& funcs = setup_casadi_funcs(strings); - py::array_t out_array(sizes); - auto out = out_array.mutable_data(); + py::array_t out_array(shape); + auto entries = out_array.mutable_data(); - TimeSeriesProcessor(ts_np, ys_np, inputs_np, funcs, out, is_f_contiguous, size_tot / sizes.back()).process(); + TimeSeriesProcessor(ts_np, ys_np, inputs_np, funcs, entries, is_f_contiguous, size_spatial).process(); return out_array; } diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index 50c24e2b42..e8dc432240 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -12,31 +12,31 @@ using std::vector; /** * @brief Observe and Hermite interpolate ND variables */ -const np_array_realtype observe_hermite_interp_ND( +const np_array_realtype observe_hermite_interp( const np_array_realtype& t_interp, const vector& ts, const vector& ys, const vector& yps, const vector& inputs, const vector& strings, - const vector& sizes + const vector& shape ); /** * @brief Observe ND variables */ -const np_array_realtype observe_ND( +const np_array_realtype observe( const vector& ts_np, const vector& ys_np, const vector& inputs_np, const vector& strings, const bool is_f_contiguous, - const vector& sizes + const vector& shape ); const vector> setup_casadi_funcs(const vector& strings); -int setup_observable(const vector& sizes); +int _setup_len_spatial(const vector& shape); #endif // PYBAMM_CREATE_OBSERVE_HPP diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 47b9211621..97b29f8b9e 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -158,18 +158,18 @@ def _observe_hermite_cpp(self, t): self._check_interp(t) ts, ys, yps, funcs, inputs, _ = self._setup_cpp_inputs() - sizes = self._size(t) - return pybamm.solvers.idaklu_solver.idaklu.observe_hermite_interp_ND( - t, ts, ys, yps, inputs, funcs, sizes + shapes = self._shape(t) + return pybamm.solvers.idaklu_solver.idaklu.observe_hermite_interp( + t, ts, ys, yps, inputs, funcs, shapes ) def _observe_raw_cpp(self): pybamm.logger.debug("Observing the variable raw data in C++") ts, ys, _, funcs, inputs, is_f_contiguous = self._setup_cpp_inputs() - sizes = self._size(self.t_pts) + shapes = self._shape(self.t_pts) - return pybamm.solvers.idaklu_solver.idaklu.observe_ND( - ts, ys, inputs, funcs, is_f_contiguous, sizes + return pybamm.solvers.idaklu_solver.idaklu.observe( + ts, ys, inputs, funcs, is_f_contiguous, shapes ) def _observe_raw_python(self): @@ -181,7 +181,7 @@ def _observe_postfix(self, entries, t): def _interp_setup(self, entries, t): pass # pragma: no cover - def _size(self, t): + def _shape(self, t): pass # pragma: no cover def _process_spatial_variable_names(self, spatial_variable): @@ -459,7 +459,7 @@ def __init__( def _observe_raw_python(self): pybamm.logger.debug("Observing the variable raw data in Python") # initialise empty array of the correct size - entries = np.empty(self._size(self.t_pts)) + entries = np.empty(self._shape(self.t_pts)) idx = 0 # Evaluate the base_variable index-by-index for ts, ys, inputs, base_var_casadi in zip( @@ -488,7 +488,7 @@ def _interp_setup(self, entries, t): return entries_for_interp, coords_for_interp - def _size(self, t): + def _shape(self, t): return [len(t)] @@ -536,7 +536,7 @@ def __init__( def _observe_raw_python(self): pybamm.logger.debug("Observing the variable raw data in Python") - entries = np.empty(self._size(self.t_pts)) + entries = np.empty(self._shape(self.t_pts)) # Evaluate the base_variable index-by-index idx = 0 @@ -588,7 +588,7 @@ def _interp_setup(self, entries, t): return entries_for_interp, coords_for_interp - def _size(self, t): + def _shape(self, t): t_size = len(t) space_size = self.base_eval_shape[0] return [space_size, t_size] @@ -652,7 +652,7 @@ def _observe_raw_python(self): Initialise a 2D object that depends on x and r, x and z, x and R, or R and r. """ pybamm.logger.debug("Observing the variable raw data in Python") - first_dim_size, second_dim_size, t_size = self._size(self.t_pts) + first_dim_size, second_dim_size, t_size = self._shape(self.t_pts) entries = np.empty((first_dim_size, second_dim_size, t_size)) # Evaluate the base_variable index-by-index @@ -758,7 +758,7 @@ def _interp_setup(self, entries, t): return entries_for_interp, coords_for_interp - def _size(self, t): + def _shape(self, t): first_dim_size = self.first_dim_size second_dim_size = self.second_dim_size t_size = len(t) From 9648a29c68fdbee50aa0dcaf12b88d8e406a205b Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:13:49 -0400 Subject: [PATCH 10/18] fix codecov, tests --- src/pybamm/plotting/quick_plot.py | 42 +- .../solvers/c_solvers/idaklu/observe.cpp | 203 ++++---- .../solvers/c_solvers/idaklu/observe.hpp | 6 +- src/pybamm/solvers/processed_variable.py | 137 +++-- .../solvers/processed_variable_computed.py | 4 +- src/pybamm/solvers/solution.py | 7 +- tests/unit/test_plotting/test_quick_plot.py | 28 +- tests/unit/test_solvers/test_idaklu_solver.py | 43 +- .../test_solvers/test_processed_variable.py | 486 +++++++++++------- .../test_processed_variable_computed.py | 11 +- tests/unit/test_solvers/test_solution.py | 6 +- 11 files changed, 541 insertions(+), 432 deletions(-) diff --git a/src/pybamm/plotting/quick_plot.py b/src/pybamm/plotting/quick_plot.py index cba4128658..cddce58d77 100644 --- a/src/pybamm/plotting/quick_plot.py +++ b/src/pybamm/plotting/quick_plot.py @@ -84,13 +84,6 @@ class QuickPlot: variable_limits : str or dict of str, optional How to set the axis limits (for 0D or 1D variables) or colorbar limits (for 2D variables). Options are: - N_t_max: int, optonal - The maximum number of time points to plot. If the number of time points is - greater than this, the time points are downsampled to fit. - N_t_linear: int, optional - The number of linearly spaced time points added to the t axis when the number of - time points is less than N_t_max. - Note: this is only used if the solution has hermite interpolation enabled. - "fixed" (default): keep all axes fixes so that all data is visible - "tight": make axes tight to plot at each time @@ -112,8 +105,6 @@ def __init__( time_unit=None, spatial_unit="um", variable_limits="fixed", - N_t_max=10000, - N_t_linear=100, ): solutions = self.preprocess_solutions(solutions) @@ -178,22 +169,6 @@ def __init__( min_t = np.min([t[0] for t in self.ts_seconds]) max_t = np.max([t[-1] for t in self.ts_seconds]) - N_t = sum(len(t) for t in self.ts_seconds) - hermite_interp = all(sol.hermite_interpolation for sol in solutions) - - if hermite_interp and ( - N_t + hermite_interp * N_t_linear * len(solutions) <= N_t_max - ): - - def t_evenly_sample(sol): - t_linspace = np.linspace(sol.t[0], sol.t[-1], N_t_linear)[1:-1] - return np.union1d(sol.t, t_linspace) - - self.ts_seconds = [t_evenly_sample(sol) for sol in solutions] - else: - # Linearly spaced time points - self.ts_seconds = [sol.t for sol in solutions] - # Set timescale if time_unit is None: # defaults depend on how long the simulation is @@ -444,14 +419,14 @@ def reset_axis(self): spatial_vars = self.spatial_variable_dict[key] var_min = np.min( [ - ax_min(var(self.ts_seconds[i], **spatial_vars, warn=False)) + ax_min(var(self.ts_seconds[i], **spatial_vars)) for i, variable_list in enumerate(variable_lists) for var in variable_list ] ) var_max = np.max( [ - ax_max(var(self.ts_seconds[i], **spatial_vars, warn=False)) + ax_max(var(self.ts_seconds[i], **spatial_vars)) for i, variable_list in enumerate(variable_lists) for var in variable_list ] @@ -537,7 +512,7 @@ def plot(self, t, dynamic=False): full_t = self.ts_seconds[i] (self.plots[key][i][j],) = ax.plot( full_t / self.time_scaling_factor, - variable(full_t, warn=False), + variable(full_t), color=self.colors[i], linestyle=linestyle, ) @@ -573,7 +548,7 @@ def plot(self, t, dynamic=False): linestyle = self.linestyles[j] (self.plots[key][i][j],) = ax.plot( self.first_spatial_variable[key], - variable(t_in_seconds, **spatial_vars, warn=False), + variable(t_in_seconds, **spatial_vars), color=self.colors[i], linestyle=linestyle, zorder=10, @@ -595,13 +570,13 @@ def plot(self, t, dynamic=False): y_name = next(iter(spatial_vars.keys()))[0] x = self.second_spatial_variable[key] y = self.first_spatial_variable[key] - var = variable(t_in_seconds, **spatial_vars, warn=False) + var = variable(t_in_seconds, **spatial_vars) else: x_name = next(iter(spatial_vars.keys()))[0] y_name = list(spatial_vars.keys())[1][0] x = self.first_spatial_variable[key] y = self.second_spatial_variable[key] - var = variable(t_in_seconds, **spatial_vars, warn=False).T + var = variable(t_in_seconds, **spatial_vars).T ax.set_xlabel(f"{x_name} [{self.spatial_unit}]") ax.set_ylabel(f"{y_name} [{self.spatial_unit}]") vmin, vmax = self.variable_limits[key] @@ -735,7 +710,6 @@ def slider_update(self, t): var = variable( time_in_seconds, **self.spatial_variable_dict[key], - warn=False, ) plot[i][j].set_ydata(var) var_min = min(var_min, ax_min(var)) @@ -754,11 +728,11 @@ def slider_update(self, t): if self.x_first_and_y_second[key] is False: x = self.second_spatial_variable[key] y = self.first_spatial_variable[key] - var = variable(time_in_seconds, **spatial_vars, warn=False) + var = variable(time_in_seconds, **spatial_vars) else: x = self.first_spatial_variable[key] y = self.second_spatial_variable[key] - var = variable(time_in_seconds, **spatial_vars, warn=False).T + var = variable(time_in_seconds, **spatial_vars).T # store the plot and the var data (for testing) as cant access # z data from QuadMesh or QuadContourSet object if self.is_y_z[key] is True: diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp index 3980a2d8ea..dc13081e81 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -1,7 +1,20 @@ #include "observe.hpp" -#include -#include +int _setup_len_spatial(const std::vector& shape) { + // Calculate the product of all dimensions except the last (spatial dimensions) + int size_spatial = 1; + for (size_t i = 0; i < shape.size() - 1; ++i) { + size_spatial *= shape[i]; + } + + if (size_spatial == 0 || shape.back() == 0) { + throw std::invalid_argument("output array must have at least one element"); + } + + return size_spatial; +} + +// Coupled observe and Hermite interpolation of variables class HermiteInterpolator { public: HermiteInterpolator(const py::detail::unchecked_reference& t, @@ -9,7 +22,7 @@ class HermiteInterpolator { const py::detail::unchecked_reference& yp) : t(t), y(y), yp(yp) {} - void compute_c_d(const size_t j, vector& c, vector& d) const { + void compute_knots(const size_t j, vector& c, vector& d) const { // Called at the start of each interval const realtype h_full = t(j + 1) - t(j); const realtype inv_h = 1.0 / h_full; @@ -32,7 +45,7 @@ class HermiteInterpolator { const size_t j, vector& c, vector& d) const { - // Must be called after compute_c_d + // Must be called after compute_knots const realtype h = t_interp - t(j); const realtype h2 = h * h; const realtype h3 = h2 * h; @@ -50,94 +63,6 @@ class HermiteInterpolator { const py::detail::unchecked_reference& yp; }; -int _setup_len_spatial(const std::vector& shape) { - // Calculate the product of all dimensions except the last (spatial dimensions) - int size_spatial = 1; - for (size_t i = 0; i < shape.size() - 1; ++i) { - size_spatial *= shape[i]; - } - - if (size_spatial == 0 || shape.back() == 0) { - throw std::invalid_argument("output array must have at least one element"); - } - - return size_spatial; -} - -class TimeSeriesProcessor { -public: - TimeSeriesProcessor(const vector& _ts, - const vector& _ys, - const vector& _inputs, - const vector>& _funcs, - realtype* _entries, - const bool _is_f_contiguous, - const int _size_spatial) - : ts(_ts), ys(_ys), inputs(_inputs), funcs(_funcs), - entries(_entries), is_f_contiguous(_is_f_contiguous), size_spatial(_size_spatial) {} - - void process() { - int i_entries = 0; - for (size_t i = 0; i < ts.size(); i++) { - const auto& t = ts[i].unchecked<1>(); - const auto& y = ys[i].unchecked<2>(); - const auto input = inputs[i].data(); - const auto func = *funcs[i]; - - resize_arrays(y.shape(0), funcs[i]); - args[2] = input; - - for (size_t j = 0; j < t.size(); j++) { - const realtype t_val = t(j); - const realtype* y_val = is_f_contiguous ? &y(0, j) : copy_to_buffer(y_buffer, y, j); - - args[0] = &t_val; - args[1] = y_val; - results[0] = &entries[i_entries]; - - func(args.data(), results.data(), iw.data(), w.data(), 0); - - i_entries += size_spatial; - } - } - } - -private: - const realtype* copy_to_buffer( - vector& entries, - const py::detail::unchecked_reference& y, - size_t j) { - for (size_t i = 0; i < entries.size(); ++i) { - entries[i] = y(i, j); - } - - return entries.data(); - } - - void resize_arrays(const int M, std::shared_ptr func) { - args.resize(func->sz_arg()); - results.resize(func->sz_res()); - iw.resize(func->sz_iw()); - w.resize(func->sz_w()); - if (!is_f_contiguous && y_buffer.size() < M) { - y_buffer.resize(M); - } - } - - const vector& ts; - const vector& ys; - const vector& inputs; - const vector>& funcs; - realtype* entries; - const bool is_f_contiguous; - int size_spatial; - vector y_buffer; - vector args; - vector results; - vector iw; - vector w; -}; - class TimeSeriesInterpolator { public: TimeSeriesInterpolator(const np_array_realtype& _t_interp, @@ -181,8 +106,9 @@ class TimeSeriesInterpolator { for (size_t i = 0; i < ts_data_np.size(); i++) { const auto& t_data = ts_data_np[i].unchecked<1>(); const realtype t_data_final = t_data(t_data.size() - 1); - // Continue if t_interp(i_interp) <= t_data_final is not true for the first step - if (t_interp(i_interp) > t_data_final) { + realtype t_interp_next = t_interp(i_interp); + // Continue if the next interpolation point is beyond the final data point + if (t_interp_next > t_data_final) { continue; } @@ -198,16 +124,16 @@ class TimeSeriesInterpolator { ssize_t j = 0; ssize_t j_prev = -1; const auto itp = HermiteInterpolator(t_data, y_data, yp_data); - while (i_interp < N_interp && t_interp(i_interp) <= t_data_final) { + while (t_interp_next <= t_data_final) { for (; j < t_data.size() - 2; ++j) { - if (t_data(j) <= t_interp(i_interp) && t_interp(i_interp) <= t_data(j + 1)) { + if (t_data(j) <= t_interp_next && t_interp_next <= t_data(j + 1)) { break; } } if (j != j_prev) { // Compute c and d for the new interval - itp.compute_c_d(j, c, d); + itp.compute_knots(j, c, d); } itp.interpolate(y_buffer, t_interp(i_interp), j, c, d); @@ -216,8 +142,12 @@ class TimeSeriesInterpolator { results[0] = &entries[i_entries]; func(args.data(), results.data(), iw.data(), w.data(), 0); - i_entries += size_spatial; ++i_interp; + if (i_interp == N_interp) { + return; + } + t_interp_next = t_interp(i_interp); + i_entries += size_spatial; j_prev = j; } } @@ -241,7 +171,7 @@ class TimeSeriesInterpolator { args[2] = input; const auto itp = HermiteInterpolator(t_data, y_data, yp_data); - itp.compute_c_d(j, c, d); + itp.compute_knots(j, c, d); for (; i_interp < N_interp; ++i_interp) { const realtype t_interp_next = t_interp(i_interp); @@ -285,6 +215,81 @@ class TimeSeriesInterpolator { vector w; }; +// Observe the raw data +class TimeSeriesProcessor { +public: + TimeSeriesProcessor(const vector& _ts, + const vector& _ys, + const vector& _inputs, + const vector>& _funcs, + realtype* _entries, + const bool _is_f_contiguous, + const int _size_spatial) + : ts(_ts), ys(_ys), inputs(_inputs), funcs(_funcs), + entries(_entries), is_f_contiguous(_is_f_contiguous), size_spatial(_size_spatial) {} + + void process() { + int i_entries = 0; + for (size_t i = 0; i < ts.size(); i++) { + const auto& t = ts[i].unchecked<1>(); + const auto& y = ys[i].unchecked<2>(); + const auto input = inputs[i].data(); + const auto func = *funcs[i]; + + resize_arrays(y.shape(0), funcs[i]); + args[2] = input; + + for (size_t j = 0; j < t.size(); j++) { + const realtype t_val = t(j); + const realtype* y_val = is_f_contiguous ? &y(0, j) : copy_to_buffer(y_buffer, y, j); + + args[0] = &t_val; + args[1] = y_val; + results[0] = &entries[i_entries]; + + func(args.data(), results.data(), iw.data(), w.data(), 0); + + i_entries += size_spatial; + } + } + } + +private: + const realtype* copy_to_buffer( + vector& entries, + const py::detail::unchecked_reference& y, + size_t j) { + for (size_t i = 0; i < entries.size(); ++i) { + entries[i] = y(i, j); + } + + return entries.data(); + } + + void resize_arrays(const int M, std::shared_ptr func) { + args.resize(func->sz_arg()); + results.resize(func->sz_res()); + iw.resize(func->sz_iw()); + w.resize(func->sz_w()); + if (!is_f_contiguous && y_buffer.size() < M) { + y_buffer.resize(M); + } + } + + const vector& ts; + const vector& ys; + const vector& inputs; + const vector>& funcs; + realtype* entries; + const bool is_f_contiguous; + int size_spatial; + vector y_buffer; + vector args; + vector results; + vector iw; + vector w; +}; + const np_array_realtype observe_hermite_interp( const np_array_realtype& t_interp_np, const vector& ts_np, diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index e8dc432240..fbad3cee99 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -12,7 +12,7 @@ using std::vector; /** * @brief Observe and Hermite interpolate ND variables */ -const np_array_realtype observe_hermite_interp( +const np_array_realtype observe_hermite_interp_ND( const np_array_realtype& t_interp, const vector& ts, const vector& ys, @@ -26,7 +26,7 @@ const np_array_realtype observe_hermite_interp( /** * @brief Observe ND variables */ -const np_array_realtype observe( +const np_array_realtype observe_ND( const vector& ts_np, const vector& ys_np, const vector& inputs_np, @@ -37,6 +37,6 @@ const np_array_realtype observe( const vector> setup_casadi_funcs(const vector& strings); -int _setup_len_spatial(const vector& shape); +int _setup_len_space(const vector& shape); #endif // PYBAMM_CREATE_OBSERVE_HPP diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 97b29f8b9e..e2aa08ef36 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -28,9 +28,6 @@ class ProcessedVariable: `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` The solution object to be used to create the processed variables - warn : bool, optional - Whether to raise warnings when trying to evaluate time and length scales. - Default is True. """ def __init__( @@ -38,7 +35,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=True, cumtrapz_ic=None, ): self.base_variables = base_variables @@ -55,7 +51,6 @@ def __init__( self.mesh = base_variables[0].mesh self.domain = base_variables[0].domain self.domains = base_variables[0].domains - self.warn = warn self.cumtrapz_ic = cumtrapz_ic # Process spatial variables @@ -101,14 +96,22 @@ def initialise(self): self._coords_raw = coords self._raw_data_initialized = True - return entries, entries_for_interp, coords - - def observe_and_interp(self, t): + def observe_and_interp(self, t, fill_value): """ Interpolate the variable at the given time points and y values. + t must be a sorted array of time points. """ + entries = self._observe_hermite_cpp(t) - return self._observe_postfix(entries, t) + processed_entries = self._observe_postfix(entries, t) + + tf = self.t_pts[-1] + if t[-1] > tf and fill_value != "extrapolate": + # fill the rest + idx = np.searchsorted(t, tf, side="right") + processed_entries[..., idx:] = fill_value + + return processed_entries def observe_raw(self): """ @@ -155,7 +158,6 @@ def _setup_cpp_inputs(self): def _observe_hermite_cpp(self, t): pybamm.logger.debug("Observing and Hermite interpolating the variable in C++") - self._check_interp(t) ts, ys, yps, funcs, inputs, _ = self._setup_cpp_inputs() shapes = self._shape(t) @@ -213,86 +215,106 @@ def _process_spatial_variable_names(self, spatial_variable): f"Spatial variable name not recognized for {spatial_variable}" ) - def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): - # 1. Check to see if we are interpolating exactly onto the solution time points + def __call__( + self, + t=None, + x=None, + r=None, + y=None, + z=None, + R=None, + fill_value=np.nan, + ): + # Check to see if we are interpolating exactly onto the raw solution time points t_observe, observe_raw = self._check_observe_raw(t) - # 2. Check if the time points are sorted and unique + # Check if the time points are sorted and unique is_sorted = observe_raw or _is_sorted(t_observe) # Sort them if not if not is_sorted: - idxs_sort = np.argsort(t) - t = t[idxs_sort] - idxs_unsort = np.arange(len(t))[idxs_sort] + idxs_sort = np.argsort(t_observe) + t_observe = t_observe[idxs_sort] - time_interp = self.hermite_interpolation and not observe_raw - if time_interp: - entries = self.observe_and_interp(t_observe) - else: - entries, entries_for_interp, coords = self.initialise() - - spatial_interp = ( - (x is not None) - or (r is not None) - or (y is not None) - or (z is not None) - or (R is not None) + hermite_time_interp = ( + pybamm.has_idaklu() and self.hermite_interpolation and not observe_raw ) - if time_interp and not spatial_interp: - processed_entries = entries - else: - if time_interp: + if hermite_time_interp: + entries = self.observe_and_interp(t_observe, fill_value) + + spatial_interp = any(a is not None for a in [x, r, y, z, R]) + + xr_interp = spatial_interp or not hermite_time_interp + + if xr_interp: + if hermite_time_interp: + # Already interpolated in time + t = None entries_for_interp, coords = self._interp_setup(entries, t_observe) + else: + self.initialise() + entries_for_interp, coords = ( + self._entries_for_interp_raw, + self._coords_raw, + ) + processed_entries = self._xr_interpolate( entries_for_interp, coords, - time_interp, t, x, r, y, z, R, - warn, + fill_value, ) + else: + processed_entries = entries if not is_sorted: + idxs_unsort = np.zeros_like(idxs_sort) + idxs_unsort[idxs_sort] = np.arange(len(t_observe)) + processed_entries = processed_entries[..., idxs_unsort] + # Remove a singleton time dimension if we interpolate in time with hermite + if hermite_time_interp and t_observe.size == 1: + processed_entries = np.squeeze(processed_entries, axis=-1) + return processed_entries def _xr_interpolate( self, entries_for_interp, coords, - time_interp, t=None, x=None, r=None, y=None, z=None, R=None, - warn=True, + fill_value=None, ): """ Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), using interpolation """ - - if not time_interp and self._xr_data_array_raw is not None: + if t is not None and self._xr_data_array_raw is not None: xr_data_array = self._xr_data_array_raw else: xr_data_array = xr.DataArray(entries_for_interp, coords=coords) kwargs = {"t": t, "x": x, "r": r, "y": y, "z": z, "R": R} - if time_interp: - kwargs["t"] = None + # Remove any None arguments kwargs = {key: value for key, value in kwargs.items() if value is not None} + # Use xarray interpolation, return numpy array - return xr_data_array.interp(**kwargs).values + out = xr_data_array.interp(**kwargs, kwargs={"fill_value": fill_value}).values + + return out def _check_observe_raw(self, t): """ @@ -318,23 +340,13 @@ def _check_observe_raw(self, t): else: t_observe = t - return t_observe, observe_raw - - def _check_interp(self, t): - """ - Check if the time points are sorted and unique - - Args: - t (np.ndarray): array to check - - Returns: - bool: True if array is sorted and unique - """ - if t[0] < self.t_pts[0]: + if t_observe[0] < self.t_pts[0]: raise ValueError( "The interpolation points must be greater than or equal to the initial solution time" ) + return t_observe, observe_raw + @property def entries(self): """ @@ -444,7 +456,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=True, cumtrapz_ic=None, ): self.dimensions = 0 @@ -452,7 +463,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=warn, cumtrapz_ic=cumtrapz_ic, ) @@ -512,9 +522,6 @@ class ProcessedVariable1D(ProcessedVariable): `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` The solution object to be used to create the processed variables - warn : bool, optional - Whether to raise warnings when trying to evaluate time and length scales. - Default is True. """ def __init__( @@ -522,7 +529,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=True, cumtrapz_ic=None, ): self.dimensions = 1 @@ -530,7 +536,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=warn, cumtrapz_ic=cumtrapz_ic, ) @@ -614,9 +619,6 @@ class ProcessedVariable2D(ProcessedVariable): `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` The solution object to be used to create the processed variables - warn : bool, optional - Whether to raise warnings when trying to evaluate time and length scales. - Default is True. """ def __init__( @@ -624,7 +626,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=True, cumtrapz_ic=None, ): self.dimensions = 2 @@ -632,7 +633,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=warn, cumtrapz_ic=cumtrapz_ic, ) first_dim_nodes = self.mesh.nodes @@ -785,9 +785,6 @@ class ProcessedVariable2DSciKitFEM(ProcessedVariable2D): `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` The solution object to be used to create the processed variables - warn : bool, optional - Whether to raise warnings when trying to evaluate time and length scales. - Default is True. """ def __init__( @@ -795,7 +792,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=True, cumtrapz_ic=None, ): self.dimensions = 2 @@ -803,7 +799,6 @@ def __init__( base_variables, base_variables_casadi, solution, - warn=warn, cumtrapz_ic=cumtrapz_ic, ) y_sol = self.mesh.edges["y"] diff --git a/src/pybamm/solvers/processed_variable_computed.py b/src/pybamm/solvers/processed_variable_computed.py index d025a90908..01db7ffbdf 100644 --- a/src/pybamm/solvers/processed_variable_computed.py +++ b/src/pybamm/solvers/processed_variable_computed.py @@ -45,7 +45,6 @@ def __init__( base_variables_casadi, base_variables_data, solution, - warn=True, cumtrapz_ic=None, ): self.base_variables = base_variables @@ -60,7 +59,6 @@ def __init__( self.mesh = base_variables[0].mesh self.domain = base_variables[0].domain self.domains = base_variables[0].domains - self.warn = warn self.cumtrapz_ic = cumtrapz_ic # Sensitivity starts off uninitialized, only set when called @@ -424,7 +422,7 @@ def initialise_2D_scikit_fem(self): coords={"y": y_sol, "z": z_sol, "t": self.t_pts}, ) - def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None, warn=True): + def __call__(self, t=None, x=None, r=None, y=None, z=None, R=None): """ Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), using interpolation diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index 0512a94409..b472e605a8 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -639,9 +639,12 @@ def process_casadi_var(self, var_pybamm, inputs, ys_shape): "enable_jacobian": False, } + # Casadi has a bug where it does not correctly handle arrays with + # zeros padded at the beginning or end. To avoid this, we add and + # subtract a small number to the variable to reinforce the + # variable bounds. epsilon = 5e-324 - var_sym = var_sym - epsilon - var_sym = var_sym + epsilon + var_sym = (var_sym - epsilon) + epsilon var_casadi = casadi.Function( "variable", diff --git a/tests/unit/test_plotting/test_quick_plot.py b/tests/unit/test_plotting/test_quick_plot.py index 6f99266862..d5d994117d 100644 --- a/tests/unit/test_plotting/test_quick_plot.py +++ b/tests/unit/test_plotting/test_quick_plot.py @@ -60,20 +60,8 @@ def test_simple_ode_model(self): disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) solver = model.default_solver - t_eval = np.linspace(0, 2, 201) - if solver.supports_interp: - t_interp = t_eval - else: - t_interp = None - - solution = solver.solve(model, t_eval, t_interp=t_interp) - - if solution.hermite_interpolation: - t_linspace = np.linspace(t_eval[0], t_eval[-1], 100) - t_plot = np.union1d(t_eval, t_linspace) - else: - t_plot = t_eval - + t_eval = np.linspace(0, 2, 100) + solution = solver.solve(model, t_eval) quick_plot = pybamm.QuickPlot( solution, [ @@ -161,28 +149,28 @@ def test_simple_ode_model(self): quick_plot.plot(0) assert quick_plot.time_scaling_factor == 1 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_plot + quick_plot.plots[("a",)][0][0].get_xdata(), t_eval ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="minutes") quick_plot.plot(0) assert quick_plot.time_scaling_factor == 60 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_plot / 60 + quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 60 ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval ) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="hours") quick_plot.plot(0) assert quick_plot.time_scaling_factor == 3600 np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_xdata(), t_plot / 3600 + quick_plot.plots[("a",)][0][0].get_xdata(), t_eval / 3600 ) np.testing.assert_array_almost_equal( - quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_plot + quick_plot.plots[("a",)][0][0].get_ydata(), 0.2 * t_eval ) with pytest.raises(ValueError, match="time unit"): pybamm.QuickPlot(solution, ["a"], time_unit="bad unit") diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index ac7efc89e6..39918d73a4 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -1105,19 +1105,40 @@ def test_simulation_period(self): def test_interpolate_time_step_start_offset(self): model = pybamm.lithium_ion.SPM() - experiment = pybamm.Experiment( - [ - "Discharge at C/10 for 10 seconds", - "Charge at C/10 for 10 seconds", - ], - period="1 seconds", - ) + + def experiment_setup(period=None): + return pybamm.Experiment( + [ + "Discharge at C/10 for 10 seconds", + "Charge at C/10 for 10 seconds", + ], + period=period, + ) + + experiment_1s = experiment_setup(period="1 seconds") solver = pybamm.IDAKLUSolver() - sim = pybamm.Simulation(model, experiment=experiment, solver=solver) - sol = sim.solve() + sim_1s = pybamm.Simulation(model, experiment=experiment_1s, solver=solver) + sol_1s = sim_1s.solve() np.testing.assert_equal( - np.nextafter(sol.sub_solutions[0].t[-1], np.inf), - sol.sub_solutions[1].t[0], + np.nextafter(sol_1s.sub_solutions[0].t[-1], np.inf), + sol_1s.sub_solutions[1].t[0], + ) + + assert not sol_1s.hermite_interpolation + + experiment = experiment_setup(period=None) + sim = pybamm.Simulation(model, experiment=experiment, solver=solver) + sol = sim.solve(model) + + assert sol.hermite_interpolation + + rtol = solver.rtol + atol = solver.atol + np.testing.assert_allclose( + sol_1s["Voltage [V]"].data, + sol["Voltage [V]"](sol_1s.t), + rtol=rtol, + atol=atol, ) def test_python_idaklu_deprecation_errors(self): diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 33c561b134..ba10b77119 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -8,6 +8,13 @@ import numpy as np import pytest +from scipy.interpolate import CubicHermiteSpline + + +if pybamm.has_idaklu(): + _hermite_args = [True, False] +else: + _hermite_args = [False] def to_casadi(var_pybamm, y, inputs=None): @@ -27,55 +34,84 @@ def to_casadi(var_pybamm, y, inputs=None): return var_casadi -def process_and_check_2D_variable( - var, first_spatial_var, second_spatial_var, disc=None, geometry_options=None -): - # first_spatial_var should be on the "smaller" domain, i.e "r" for an "r-x" variable - if geometry_options is None: - geometry_options = {} - if disc is None: - disc = tests.get_discretisation_for_testing() - disc.set_variable_slices([var]) - - first_sol = disc.process_symbol(first_spatial_var).entries[:, 0] - second_sol = disc.process_symbol(second_spatial_var).entries[:, 0] - - # Keep only the first iteration of entries - first_sol = first_sol[: len(first_sol) // len(second_sol)] - var_sol = disc.process_symbol(var) - t_sol = np.linspace(0, 1) - y_sol = np.ones(len(second_sol) * len(first_sol))[:, np.newaxis] * np.linspace(0, 5) - - var_casadi = to_casadi(var_sol, y_sol) - model = tests.get_base_model_with_battery_geometry(**geometry_options) - processed_var = pybamm.process_variable( - [var_sol], - [var_casadi], - pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, - ) - np.testing.assert_array_equal( - processed_var.entries, - np.reshape(y_sol, [len(first_sol), len(second_sol), len(t_sol)]), - ) - return y_sol, first_sol, second_sol, t_sol +class TestProcessedVariable: + @staticmethod + def _get_yps(y, hermite_interp, values=1): + if hermite_interp: + yp_sol = values * np.ones_like(y) + else: + yp_sol = None + return yp_sol + + @staticmethod + def _sol_default(t_sol, y_sol, yp_sol=None, model=None, inputs=None): + if inputs is None: + inputs = {} + if model is None: + model = tests.get_base_model_with_battery_geometry() + return pybamm.Solution( + t_sol, + y_sol, + model, + inputs, + all_yps=yp_sol, + ) + + def _process_and_check_2D_variable( + self, + var, + first_spatial_var, + second_spatial_var, + disc=None, + geometry_options=None, + hermite_interp=False, + ): + # first_spatial_var should be on the "smaller" domain, i.e "r" for an "r-x" variable + if geometry_options is None: + geometry_options = {} + if disc is None: + disc = tests.get_discretisation_for_testing() + disc.set_variable_slices([var]) + first_sol = disc.process_symbol(first_spatial_var).entries[:, 0] + second_sol = disc.process_symbol(second_spatial_var).entries[:, 0] -class TestProcessedVariable: - def test_processed_variable_0D(self): + # Keep only the first iteration of entries + first_sol = first_sol[: len(first_sol) // len(second_sol)] + var_sol = disc.process_symbol(var) + t_sol = np.linspace(0, 1) + y_sol = 5 * t_sol * np.ones(len(second_sol) * len(first_sol))[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) + + var_casadi = to_casadi(var_sol, y_sol) + model = tests.get_base_model_with_battery_geometry(**geometry_options) + processed_var = pybamm.process_variable( + [var_sol], + [var_casadi], + self._sol_default(t_sol, y_sol, yp_sol, model), + ) + np.testing.assert_array_equal( + processed_var.entries, + np.reshape(y_sol, [len(first_sol), len(second_sol), len(t_sol)]), + ) + return y_sol, first_sol, second_sol, t_sol, yp_sol + + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_0D(self, hermite_interp): # without space t = pybamm.t y = pybamm.StateVector(slice(0, 1)) var = t * y var.mesh = None + model = pybamm.BaseModel() t_sol = np.linspace(0, 1) y_sol = np.array([np.linspace(0, 5)]) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var, y_sol) processed_var = pybamm.process_variable( [var], [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol, model), ) np.testing.assert_array_equal(processed_var.entries, t_sol * y_sol[0]) @@ -84,18 +120,26 @@ def test_processed_variable_0D(self): var.mesh = None t_sol = np.array([0]) y_sol = np.array([1])[:, np.newaxis] + yp_sol = np.array([1])[:, np.newaxis] var_casadi = to_casadi(var, y_sol) processed_var = pybamm.process_variable( [var], [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol, model), ) np.testing.assert_array_equal(processed_var.entries, y_sol[0]) - # check empty sensitivity works + # check that repeated calls return the same data + data1 = processed_var.data + + assert processed_var._raw_data_initialized + + data2 = processed_var.data + + np.testing.assert_array_equal(data1, data2) - def test_processed_variable_0D_no_sensitivity(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_0D_no_sensitivity(self, hermite_interp): # without space t = pybamm.t y = pybamm.StateVector(slice(0, 1)) @@ -103,12 +147,12 @@ def test_processed_variable_0D_no_sensitivity(self): var.mesh = None t_sol = np.linspace(0, 1) y_sol = np.array([np.linspace(0, 5)]) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var, y_sol) processed_var = pybamm.process_variable( [var], [var_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol, pybamm.BaseModel()), ) # test no inputs (i.e. no sensitivity) @@ -128,14 +172,14 @@ def test_processed_variable_0D_no_sensitivity(self): [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), inputs), - warn=False, ) # test no sensitivity raises error with pytest.raises(ValueError, match="Cannot compute sensitivities"): print(processed_var.sensitivities) - def test_processed_variable_1D(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_1D(self, hermite_interp): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) @@ -149,15 +193,13 @@ def test_processed_variable_1D(self): eqn_sol = disc.process_symbol(eqn) t_sol = np.linspace(0, 1) y_sol = np.ones_like(x_sol)[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal(processed_var.entries, y_sol) np.testing.assert_array_almost_equal(processed_var(t_sol, x_sol), y_sol) @@ -165,10 +207,7 @@ def test_processed_variable_1D(self): processed_eqn = pybamm.process_variable( [eqn_sol], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_almost_equal( processed_eqn(t_sol, x_sol), t_sol * y_sol + x_sol[:, np.newaxis] @@ -187,10 +226,7 @@ def test_processed_variable_1D(self): processed_x_s_edge = pybamm.process_variable( [x_s_edge], [x_s_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal( x_s_edge.entries[:, 0], processed_x_s_edge.entries[:, 0] @@ -201,20 +237,19 @@ def test_processed_variable_1D(self): eqn_sol = disc.process_symbol(eqn) t_sol = np.array([0]) y_sol = np.ones_like(x_sol)[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp, values=0) eqn_casadi = to_casadi(eqn_sol, y_sol) processed_eqn2 = pybamm.process_variable( [eqn_sol], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal( processed_eqn2.entries, y_sol + x_sol[:, np.newaxis] ) - def test_processed_variable_1D_unknown_domain(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_1D_unknown_domain(self, hermite_interp): x = pybamm.SpatialVariable("x", domain="SEI layer", coord_sys="cartesian") geometry = pybamm.Geometry( {"SEI layer": {x: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}}} @@ -227,6 +262,7 @@ def test_processed_variable_1D_unknown_domain(self): nt = 100 y_sol = np.zeros((var_pts[x], nt)) + yp_sol = self._get_yps(y_sol, hermite_interp) model = tests.get_base_model_with_battery_geometry() model._geometry = geometry solution = pybamm.Solution( @@ -237,14 +273,16 @@ def test_processed_variable_1D_unknown_domain(self): np.linspace(0, 1, 1), np.zeros(var_pts[x]), "test", + all_yps=yp_sol, ) c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) c.mesh = mesh["SEI layer"] c_casadi = to_casadi(c, y_sol) - pybamm.process_variable([c], [c_casadi], solution, warn=False) + pybamm.process_variable([c], [c_casadi], solution) - def test_processed_variable_2D_x_r(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_x_r(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -258,9 +296,12 @@ def test_processed_variable_2D_x_r(self): ) disc = tests.get_p2d_discretisation_for_testing() - process_and_check_2D_variable(var, r, x, disc=disc) + self._process_and_check_2D_variable( + var, r, x, disc=disc, hermite_interp=hermite_interp + ) - def test_processed_variable_2D_R_x(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_R_x(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle size"], @@ -274,15 +315,17 @@ def test_processed_variable_2D_R_x(self): x = pybamm.SpatialVariable("x", domain=["negative electrode"]) disc = tests.get_size_distribution_disc_for_testing() - process_and_check_2D_variable( + self._process_and_check_2D_variable( var, R, x, disc=disc, geometry_options={"options": {"particle size": "distribution"}}, + hermite_interp=hermite_interp, ) - def test_processed_variable_2D_R_z(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_R_z(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle size"], @@ -296,15 +339,17 @@ def test_processed_variable_2D_R_z(self): z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_size_distribution_disc_for_testing() - process_and_check_2D_variable( + self._process_and_check_2D_variable( var, R, z, disc=disc, geometry_options={"options": {"particle size": "distribution"}}, + hermite_interp=hermite_interp, ) - def test_processed_variable_2D_r_R(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_r_R(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -318,15 +363,17 @@ def test_processed_variable_2D_r_R(self): R = pybamm.SpatialVariable("R", domain=["negative particle size"]) disc = tests.get_size_distribution_disc_for_testing() - process_and_check_2D_variable( + self._process_and_check_2D_variable( var, r, R, disc=disc, geometry_options={"options": {"particle size": "distribution"}}, + hermite_interp=hermite_interp, ) - def test_processed_variable_2D_x_z(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_x_z(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative electrode", "separator"], @@ -340,7 +387,9 @@ def test_processed_variable_2D_x_z(self): z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_1p1d_discretisation_for_testing() - y_sol, x_sol, z_sol, t_sol = process_and_check_2D_variable(var, x, z, disc=disc) + y_sol, x_sol, z_sol, t_sol, yp_sol = self._process_and_check_2D_variable( + var, x, z, disc=disc, hermite_interp=hermite_interp + ) del x_sol # On edges @@ -355,16 +404,14 @@ def test_processed_variable_2D_x_z(self): processed_x_s_edge = pybamm.process_variable( [x_s_edge], [x_s_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal( x_s_edge.entries.flatten(), processed_x_s_edge.entries[:, :, 0].T.flatten() ) - def test_processed_variable_2D_space_only(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_space_only(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -386,22 +433,21 @@ def test_processed_variable_2D_space_only(self): var_sol = disc.process_symbol(var) t_sol = np.array([0]) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal( processed_var.entries, np.reshape(y_sol, [len(r_sol), len(x_sol), len(t_sol)]), ) - def test_processed_variable_2D_scikit(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_scikit(self, hermite_interp): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -412,21 +458,20 @@ def test_processed_variable_2D_scikit(self): var_sol.mesh = disc.mesh["current collector"] t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(u_sol, hermite_interp) var_casadi = to_casadi(var_sol, u_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, u_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, u_sol, yp_sol), ) np.testing.assert_array_equal( processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) ) - def test_processed_variable_2D_fixed_t_scikit(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_variable_2D_fixed_t_scikit(self, hermite_interp): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -437,6 +482,7 @@ def test_processed_variable_2D_fixed_t_scikit(self): var_sol.mesh = disc.mesh["current collector"] t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] + yp_sol = self._get_yps(u_sol, hermite_interp) var_casadi = to_casadi(var_sol, u_sol) model = tests.get_base_model_with_battery_geometry( @@ -445,14 +491,14 @@ def test_processed_variable_2D_fixed_t_scikit(self): processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution(t_sol, u_sol, model, {}), - warn=False, + pybamm.Solution(t_sol, u_sol, model, {}, all_yps=yp_sol), ) np.testing.assert_array_equal( processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) ) - def test_processed_var_0D_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_0D_interpolation(self, hermite_interp): # without spatial dependence t = pybamm.t y = pybamm.StateVector(slice(0, 1)) @@ -462,40 +508,39 @@ def test_processed_var_0D_interpolation(self): eqn.mesh = None t_sol = np.linspace(0, 1, 1000) - y_sol = np.array([np.linspace(0, 5, 1000)]) + y_sol = np.array([5 * t_sol]) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var, y_sol) processed_var = pybamm.process_variable( [var], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # vector np.testing.assert_array_equal(processed_var(t_sol), y_sol[0]) # scalar - np.testing.assert_array_equal(processed_var(0.5), 2.5) - np.testing.assert_array_equal(processed_var(0.7), 3.5) + np.testing.assert_array_almost_equal(processed_var(0.5), 2.5) + np.testing.assert_array_almost_equal(processed_var(0.7), 3.5) eqn_casadi = to_casadi(eqn, y_sol) processed_eqn = pybamm.process_variable( [eqn], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal(processed_eqn(t_sol), t_sol * y_sol[0]) - np.testing.assert_array_almost_equal(processed_eqn(0.5), 0.5 * 2.5) + assert processed_eqn(0.5).shape == () + + np.testing.assert_array_almost_equal(processed_eqn(0.5), 0.5 * 2.5) + np.testing.assert_array_equal(processed_eqn(2, fill_value=100), 100) # Suppress warning for this test pybamm.set_logging_level("ERROR") np.testing.assert_array_equal(processed_eqn(2), np.nan) pybamm.set_logging_level("WARNING") - def test_processed_var_0D_fixed_t_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_0D_fixed_t_interpolation(self, hermite_interp): y = pybamm.StateVector(slice(0, 1)) var = y eqn = 2 * y @@ -504,17 +549,18 @@ def test_processed_var_0D_fixed_t_interpolation(self): t_sol = np.array([10]) y_sol = np.array([[100]]) + yp_sol = self._get_yps(y_sol, hermite_interp) eqn_casadi = to_casadi(eqn, y_sol) processed_var = pybamm.process_variable( [eqn], [eqn_casadi], - pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol, pybamm.BaseModel()), ) - np.testing.assert_array_equal(processed_var(), 200) + assert processed_var() == 200 - def test_processed_var_1D_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_1D_interpolation(self, hermite_interp): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) @@ -526,23 +572,23 @@ def test_processed_var_1D_interpolation(self): var_sol = disc.process_symbol(var) eqn_sol = disc.process_symbol(eqn) t_sol = np.linspace(0, 1) - y_sol = x_sol[:, np.newaxis] * np.linspace(0, 5) + y_sol = x_sol[:, np.newaxis] * (5 * t_sol) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) + # 2 vectors np.testing.assert_array_almost_equal(processed_var(t_sol, x_sol), y_sol) # 1 vector, 1 scalar np.testing.assert_array_almost_equal(processed_var(0.5, x_sol), 2.5 * x_sol) - np.testing.assert_array_equal( - processed_var(t_sol, x_sol[-1]), x_sol[-1] * np.linspace(0, 5) + np.testing.assert_array_almost_equal( + processed_var(t_sol, x_sol[-1]), + x_sol[-1] * np.linspace(0, 5), ) # 2 scalars np.testing.assert_array_almost_equal( @@ -552,10 +598,7 @@ def test_processed_var_1D_interpolation(self): processed_eqn = pybamm.process_variable( [eqn_sol], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 2 vectors np.testing.assert_array_almost_equal( @@ -574,10 +617,7 @@ def test_processed_var_1D_interpolation(self): processed_x = pybamm.process_variable( [x_disc], [x_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_almost_equal(processed_x(t=0, x=x_sol), x_sol) @@ -590,10 +630,7 @@ def test_processed_var_1D_interpolation(self): processed_r_n = pybamm.process_variable( [r_n], [r_n_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) np.testing.assert_array_equal(r_n.entries[:, 0], processed_r_n.entries[:, 0]) r_test = np.linspace(0, 0.5) @@ -612,13 +649,13 @@ def test_processed_var_1D_interpolation(self): [R_n], [R_n_casadi], pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, ) np.testing.assert_array_equal(R_n.entries[:, 0], processed_R_n.entries[:, 0]) R_test = np.linspace(0, 1) np.testing.assert_array_almost_equal(processed_R_n(0, R=R_test), R_test) - def test_processed_var_1D_fixed_t_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_1D_fixed_t_interpolation(self, hermite_interp): var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) eqn = var + x @@ -629,15 +666,13 @@ def test_processed_var_1D_fixed_t_interpolation(self): eqn_sol = disc.process_symbol(eqn) t_sol = np.array([1]) y_sol = x_sol[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp) eqn_casadi = to_casadi(eqn_sol, y_sol) processed_var = pybamm.process_variable( [eqn_sol], [eqn_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # vector @@ -647,7 +682,8 @@ def test_processed_var_1D_fixed_t_interpolation(self): # scalar np.testing.assert_array_almost_equal(processed_var(x=0.5), 1) - def test_processed_var_wrong_spatial_variable_names(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_wrong_spatial_variable_names(self, hermite_interp): var = pybamm.Variable( "var", domain=["domain A", "domain B"], @@ -677,6 +713,7 @@ def test_processed_var_wrong_spatial_variable_names(self): var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(a_sol) * len(b_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) model = pybamm.BaseModel() @@ -690,11 +727,11 @@ def test_processed_var_wrong_spatial_variable_names(self): pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, + pybamm.Solution(t_sol, y_sol, model, {}, all_yps=yp_sol), ).initialise() - def test_processed_var_2D_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_interpolation(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -716,15 +753,13 @@ def test_processed_var_2D_interpolation(self): var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( @@ -771,17 +806,15 @@ def test_processed_var_2D_interpolation(self): processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( processed_var(t_sol, x_sol, r_sol).shape, (10, 35, 50) ) - def test_processed_var_2D_fixed_t_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_fixed_t_interpolation(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative particle"], @@ -803,15 +836,13 @@ def test_processed_var_2D_fixed_t_interpolation(self): var_sol = disc.process_symbol(var) t_sol = np.array([0]) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 2 vectors np.testing.assert_array_equal( @@ -823,7 +854,8 @@ def test_processed_var_2D_fixed_t_interpolation(self): # 2 scalars np.testing.assert_array_equal(processed_var(t=0, x=0.2, r=0.2).shape, ()) - def test_processed_var_2D_secondary_broadcast(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_secondary_broadcast(self, hermite_interp): var = pybamm.Variable("var", domain=["negative particle"]) broad_var = pybamm.SecondaryBroadcast(var, "negative electrode") x = pybamm.SpatialVariable("x", domain=["negative electrode"]) @@ -836,15 +868,13 @@ def test_processed_var_2D_secondary_broadcast(self): var_sol = disc.process_symbol(broad_var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp) var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( @@ -877,22 +907,21 @@ def test_processed_var_2D_secondary_broadcast(self): var_sol = disc.process_symbol(broad_var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(r_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, y_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( processed_var(t_sol, x_sol, r_sol).shape, (10, 35, 50) ) - def test_processed_var_2_d_scikit_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2_d_scikit_interpolation(self, hermite_interp): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -903,15 +932,13 @@ def test_processed_var_2_d_scikit_interpolation(self): var_sol.mesh = disc.mesh["current collector"] t_sol = np.linspace(0, 1) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(u_sol, hermite_interp) var_casadi = to_casadi(var_sol, u_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, u_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, u_sol, yp_sol), ) # 3 vectors np.testing.assert_array_equal( @@ -938,7 +965,8 @@ def test_processed_var_2_d_scikit_interpolation(self): # 3 scalars np.testing.assert_array_equal(processed_var(0.2, y=0.2, z=0.2).shape, ()) - def test_processed_var_2D_fixed_t_scikit_interpolation(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_fixed_t_scikit_interpolation(self, hermite_interp): var = pybamm.Variable("var", domain=["current collector"]) disc = tests.get_2p1d_discretisation_for_testing() @@ -949,15 +977,13 @@ def test_processed_var_2D_fixed_t_scikit_interpolation(self): var_sol.mesh = disc.mesh["current collector"] t_sol = np.array([0]) u_sol = np.ones(var_sol.shape[0])[:, np.newaxis] + yp_sol = self._get_yps(u_sol, hermite_interp) var_casadi = to_casadi(var_sol, u_sol) processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, u_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, u_sol, yp_sol), ) # 2 vectors np.testing.assert_array_equal( @@ -969,7 +995,8 @@ def test_processed_var_2D_fixed_t_scikit_interpolation(self): # 2 scalars np.testing.assert_array_equal(processed_var(t=0, y=0.2, z=0.2).shape, ()) - def test_processed_var_2D_unknown_domain(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_processed_var_2D_unknown_domain(self, hermite_interp): var = pybamm.Variable( "var", domain=["domain B"], @@ -1007,6 +1034,7 @@ def test_processed_var_2D_unknown_domain(self): var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(z_sol))[:, np.newaxis] * np.linspace(0, 5) + yp_sol = self._get_yps(y_sol, hermite_interp, values=5) var_casadi = to_casadi(var_sol, y_sol) model = pybamm.BaseModel() @@ -1019,8 +1047,7 @@ def test_processed_var_2D_unknown_domain(self): processed_var = pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, + pybamm.Solution(t_sol, y_sol, model, {}, all_yps=yp_sol), ) # 3 vectors np.testing.assert_array_equal( @@ -1047,7 +1074,8 @@ def test_processed_var_2D_unknown_domain(self): # 3 scalars np.testing.assert_array_equal(processed_var(t=0.2, x=0.2, z=0.2).shape, ()) - def test_3D_raises_error(self): + @pytest.mark.parametrize("hermite_interp", _hermite_args) + def test_3D_raises_error(self, hermite_interp): var = pybamm.Variable( "var", domain=["negative electrode"], @@ -1059,16 +1087,14 @@ def test_3D_raises_error(self): var_sol = disc.process_symbol(var) t_sol = np.array([0, 1, 2]) u_sol = np.ones(var_sol.shape[0] * 3)[:, np.newaxis] + yp_sol = self._get_yps(u_sol, hermite_interp, values=0) var_casadi = to_casadi(var_sol, u_sol) with pytest.raises(NotImplementedError, match="Shape not recognized"): pybamm.process_variable( [var_sol], [var_casadi], - pybamm.Solution( - t_sol, u_sol, tests.get_base_model_with_battery_geometry(), {} - ), - warn=False, + self._sol_default(t_sol, u_sol, yp_sol), ) def test_process_spatial_variable_names(self): @@ -1084,7 +1110,6 @@ def test_process_spatial_variable_names(self): [var], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, ) # Test empty list returns None @@ -1108,3 +1133,110 @@ def test_process_spatial_variable_names(self): # Test error raised if spatial variable name not recognised with pytest.raises(NotImplementedError, match="Spatial variable name"): processed_var._process_spatial_variable_names(["var1", "var2"]) + + def test_hermite_interpolator(self): + if not pybamm.has_idaklu(): + pytest.skip("Cannot test Hermite interpolation without IDAKLU") + + # initialise dummy solution to access method + def solution_setup(t_sol, sign): + y_sol = np.array([sign * np.sin(t_sol)]) + yp_sol = np.array([sign * np.cos(t_sol)]) + sol = self._sol_default(t_sol, y_sol, yp_sol) + return sol + + # without spatial dependence + t = pybamm.t + y = pybamm.StateVector(slice(0, 1)) + var = y + eqn = t * y + var.mesh = None + eqn.mesh = None + + sign1 = +1 + t_sol1 = np.linspace(0, 1, 100) + sol1 = solution_setup(t_sol1, sign1) + + # Discontinuity in the solution + sign2 = -1 + t_sol2 = np.linspace(np.nextafter(t_sol1[-1], np.inf), t_sol1[-1] + 3, 99) + sol2 = solution_setup(t_sol2, sign2) + + sol = sol1 + sol2 + var_casadi = to_casadi(var, sol.all_ys[0]) + processed_var = pybamm.process_variable( + [var] * len(sol.all_ts), + [var_casadi] * len(sol.all_ts), + sol, + ) + + # Ground truth spline interpolants from scipy + spls = [ + CubicHermiteSpline(t, y, yp, axis=1) + for t, y, yp in zip(sol.all_ts, sol.all_ys, sol.all_yps) + ] + + def spl(t): + t = np.array(t) + out = np.zeros(len(t)) + for i, spl in enumerate(spls): + t0 = sol.all_ts[i][0] + tf = sol.all_ts[i][-1] + + mask = t >= t0 + # Extrapolation is allowed for the final solution + if i < len(spls) - 1: + mask &= t <= tf + + out[mask] = spl(t[mask]).flatten() + return out + + t0 = sol.t[0] + tf = sol.t[-1] + + # Test extrapolation before the first solution time + t_left_extrap = t0 - 1 + with pytest.raises( + ValueError, match="interpolation points must be greater than" + ): + processed_var(t_left_extrap) + + # Test extrapolation after the last solution time + t_right_extrap = [tf + 1] + np.testing.assert_almost_equal( + spl(t_right_extrap), + processed_var(t_right_extrap, fill_value="extrapolate"), + decimal=8, + ) + + t_dense = np.linspace(t0, tf + 1, 1000) + np.testing.assert_almost_equal( + spl(t_dense), + processed_var(t_dense, fill_value="extrapolate"), + decimal=8, + ) + + t_extended = np.union1d(sol.t, sol.t[-1] + 1) + np.testing.assert_almost_equal( + spl(t_extended), + processed_var(t_extended, fill_value="extrapolate"), + decimal=8, + ) + + ## Unsorted arrays + t_unsorted = np.array([0.5, 0.4, 0.6, 0, 1]) + idxs_sort = np.argsort(t_unsorted) + t_sorted = np.sort(t_unsorted) + + y_sorted = processed_var(t_sorted) + + idxs_unsort = np.zeros_like(idxs_sort) + idxs_unsort[idxs_sort] = np.arange(len(t_unsorted)) + + # Check that the unsorted and sorted arrays are the same + assert np.all(t_sorted == t_unsorted[idxs_sort]) + + y_unsorted = processed_var(t_unsorted) + + # Check that the unsorted and sorted arrays are the same + assert np.all(y_unsorted == y_sorted[idxs_unsort]) diff --git a/tests/unit/test_solvers/test_processed_variable_computed.py b/tests/unit/test_solvers/test_processed_variable_computed.py index 7eae2a2d3c..0fa46b4414 100644 --- a/tests/unit/test_solvers/test_processed_variable_computed.py +++ b/tests/unit/test_solvers/test_processed_variable_computed.py @@ -57,7 +57,6 @@ def process_and_check_2D_variable( [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, model, {}), - warn=False, ) # NB: ProcessedVariableComputed does not interpret y in the same way as # ProcessedVariable; a better test of equivalence is to check that the @@ -82,7 +81,6 @@ def test_processed_variable_0D(self): [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, ) # Assert that the processed variable is the same as the solution np.testing.assert_array_equal(processed_var.entries, y_sol[0]) @@ -111,7 +109,6 @@ def test_processed_variable_0D_no_sensitivity(self): [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, ) # test no inputs (i.e. no sensitivity) @@ -132,7 +129,6 @@ def test_processed_variable_0D_no_sensitivity(self): [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), inputs), - warn=False, ) # test no sensitivity raises error @@ -157,7 +153,6 @@ def test_processed_variable_1D(self): [var_casadi], [y_sol], sol, - warn=False, ) # Ordering from idaklu with output_variables set is different to @@ -220,7 +215,7 @@ def test_processed_variable_1D_unknown_domain(self): c = pybamm.StateVector(slice(0, var_pts[x]), domain=["SEI layer"]) c.mesh = mesh["SEI layer"] c_casadi = to_casadi(c, y_sol) - pybamm.ProcessedVariableComputed([c], [c_casadi], [y_sol], solution, warn=False) + pybamm.ProcessedVariableComputed([c], [c_casadi], [y_sol], solution) def test_processed_variable_2D_x_r(self): var = pybamm.Variable( @@ -336,7 +331,6 @@ def test_processed_variable_2D_x_z(self): pybamm.Solution( t_sol, y_sol, tests.get_base_model_with_battery_geometry(), {} ), - warn=False, ) np.testing.assert_array_equal( x_s_edge.entries.flatten(), processed_x_s_edge.entries[:, :, 0].T.flatten() @@ -371,7 +365,6 @@ def test_processed_variable_2D_space_only(self): [var_casadi], [y_sol], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), - warn=False, ) np.testing.assert_array_equal( processed_var.entries, @@ -408,7 +401,6 @@ def test_processed_variable_2D_fixed_t_scikit(self): [var_casadi], [u_sol], pybamm.Solution(t_sol, u_sol, pybamm.BaseModel(), {}), - warn=False, ) np.testing.assert_array_equal( processed_var.entries, np.reshape(u_sol, [len(y), len(z), len(t_sol)]) @@ -434,5 +426,4 @@ def test_3D_raises_error(self): [var_casadi], [u_sol], pybamm.Solution(t_sol, u_sol, pybamm.BaseModel(), {}), - warn=False, ) diff --git a/tests/unit/test_solvers/test_solution.py b/tests/unit/test_solvers/test_solution.py index 5a584fabbf..951ebb0a64 100644 --- a/tests/unit/test_solvers/test_solution.py +++ b/tests/unit/test_solvers/test_solution.py @@ -70,14 +70,16 @@ def test_add_solutions(self): # Set up first solution t1 = np.linspace(0, 1) y1 = np.tile(t1, (20, 1)) - sol1 = pybamm.Solution(t1, y1, pybamm.BaseModel(), {"a": 1}) + yp1 = np.tile(t1, (30, 1)) + sol1 = pybamm.Solution(t1, y1, pybamm.BaseModel(), {"a": 1}, all_yps=yp1) sol1.solve_time = 1.5 sol1.integration_time = 0.3 # Set up second solution t2 = np.linspace(1, 2) y2 = np.tile(t2, (20, 1)) - sol2 = pybamm.Solution(t2, y2, pybamm.BaseModel(), {"a": 2}) + yp2 = np.tile(t1, (30, 1)) + sol2 = pybamm.Solution(t2, y2, pybamm.BaseModel(), {"a": 2}, all_yps=yp2) sol2.solve_time = 1 sol2.integration_time = 0.5 From 3bdc02b70f926f229ffcda0790504686b751f4e0 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:22:54 -0400 Subject: [PATCH 11/18] naming --- src/pybamm/solvers/c_solvers/idaklu/observe.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp index fbad3cee99..e8dc432240 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.hpp @@ -12,7 +12,7 @@ using std::vector; /** * @brief Observe and Hermite interpolate ND variables */ -const np_array_realtype observe_hermite_interp_ND( +const np_array_realtype observe_hermite_interp( const np_array_realtype& t_interp, const vector& ts, const vector& ys, @@ -26,7 +26,7 @@ const np_array_realtype observe_hermite_interp_ND( /** * @brief Observe ND variables */ -const np_array_realtype observe_ND( +const np_array_realtype observe( const vector& ts_np, const vector& ys_np, const vector& inputs_np, @@ -37,6 +37,6 @@ const np_array_realtype observe_ND( const vector> setup_casadi_funcs(const vector& strings); -int _setup_len_space(const vector& shape); +int _setup_len_spatial(const vector& shape); #endif // PYBAMM_CREATE_OBSERVE_HPP From 43a53562e5a2a37f549591cf2e73315cc30a54dd Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:04:08 -0400 Subject: [PATCH 12/18] codecov --- src/pybamm/solvers/processed_variable.py | 8 +++++++- tests/unit/test_solvers/test_processed_variable.py | 11 ++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index e2aa08ef36..8c68c53384 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -262,6 +262,7 @@ def __call__( processed_entries = self._xr_interpolate( entries_for_interp, coords, + observe_raw, t, x, r, @@ -289,6 +290,7 @@ def _xr_interpolate( self, entries_for_interp, coords, + observe_raw, t=None, x=None, r=None, @@ -301,7 +303,11 @@ def _xr_interpolate( Evaluate the variable at arbitrary *dimensional* t (and x, r, y, z and/or R), using interpolation """ - if t is not None and self._xr_data_array_raw is not None: + if observe_raw: + if self._xr_data_array_raw is None: + self._xr_data_array_raw = xr.DataArray( + entries_for_interp, coords=coords + ) xr_data_array = self._xr_data_array_raw else: xr_data_array = xr.DataArray(entries_for_interp, coords=coords) diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index ba10b77119..6013bd90a2 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -121,11 +121,12 @@ def test_processed_variable_0D(self, hermite_interp): t_sol = np.array([0]) y_sol = np.array([1])[:, np.newaxis] yp_sol = np.array([1])[:, np.newaxis] + sol = self._sol_default(t_sol, y_sol, yp_sol, model) var_casadi = to_casadi(var, y_sol) processed_var = pybamm.process_variable( [var], [var_casadi], - self._sol_default(t_sol, y_sol, yp_sol, model), + sol, ) np.testing.assert_array_equal(processed_var.entries, y_sol[0]) @@ -138,6 +139,14 @@ def test_processed_variable_0D(self, hermite_interp): np.testing.assert_array_equal(data1, data2) + data_t1 = processed_var(sol.t) + + assert processed_var._xr_data_array_raw is not None + + data_t2 = processed_var(sol.t) + + np.testing.assert_array_equal(data_t1, data_t2) + @pytest.mark.parametrize("hermite_interp", _hermite_args) def test_processed_variable_0D_no_sensitivity(self, hermite_interp): # without space From 6823c71a4fa4ee92e625f8476dadabaa98d2841a Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:25:40 -0400 Subject: [PATCH 13/18] codacy, cse/expand --- .../solvers/c_solvers/idaklu/observe.cpp | 5 ---- src/pybamm/solvers/processed_variable.py | 30 +++++++++++-------- src/pybamm/solvers/solution.py | 15 ++++++---- .../test_solvers/test_processed_variable.py | 4 +-- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp index dc13081e81..8f1d90e55d 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/observe.cpp +++ b/src/pybamm/solvers/c_solvers/idaklu/observe.cpp @@ -81,13 +81,8 @@ class TimeSeriesInterpolator { auto t_interp = t_interp_np.unchecked<1>(); ssize_t i_interp = 0; int i_entries = 0; - ssize_t N_data = 0; const ssize_t N_interp = t_interp.size(); - for (const auto& ts : ts_data_np) { - N_data += ts.size(); - } - // Main processing within bounds process_within_bounds(i_interp, i_entries, t_interp, N_interp); diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 8c68c53384..4eb09827aa 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -46,8 +46,6 @@ def __init__( self.all_inputs = solution.all_inputs self.all_inputs_casadi = solution.all_inputs_casadi - self.hermite_interpolation = solution.hermite_interpolation - self.mesh = base_variables[0].mesh self.domain = base_variables[0].domain self.domains = base_variables[0].domains @@ -74,14 +72,13 @@ def __init__( self.base_eval_size = self.base_variables[0].size # xr_data_array is initialized - self._raw_data_initialized = False - self._xr_data_array_raw = None + self._xr_array_raw = None self._entries_raw = None self._entries_for_interp_raw = None self._coords_raw = None def initialise(self): - if self._raw_data_initialized: + if self.entries_raw_initialized: entries = self._entries_raw entries_for_interp = self._entries_for_interp_raw coords = self._coords_raw @@ -94,7 +91,6 @@ def initialise(self): self._entries_raw = entries self._entries_for_interp_raw = entries_for_interp self._coords_raw = coords - self._raw_data_initialized = True def observe_and_interp(self, t, fill_value): """ @@ -304,11 +300,9 @@ def _xr_interpolate( using interpolation """ if observe_raw: - if self._xr_data_array_raw is None: - self._xr_data_array_raw = xr.DataArray( - entries_for_interp, coords=coords - ) - xr_data_array = self._xr_data_array_raw + if not self.xr_array_raw_initialized: + self._xr_array_raw = xr.DataArray(entries_for_interp, coords=coords) + xr_data_array = self._xr_array_raw else: xr_data_array = xr.DataArray(entries_for_interp, coords=coords) @@ -360,7 +354,7 @@ def entries(self): variable has not been initialized (i.e. the entries have not been calculated), then the processed variable is initialized first. """ - if not self._raw_data_initialized: + if not self.entries_raw_initialized: self.initialise() return self._entries_raw @@ -369,6 +363,14 @@ def data(self): """Same as entries, but different name""" return self.entries + @property + def entries_raw_initialized(self): + return self._entries_raw is not None + + @property + def xr_array_raw_initialized(self): + return self._xr_array_raw is not None + @property def sensitivities(self): """ @@ -455,6 +457,10 @@ def initialise_sensitivity_explicit_forward(self): # Save attribute self._sensitivities = sensitivities + @property + def hermite_interpolation(self): + return self.all_yps is not None + class ProcessedVariable0D(ProcessedVariable): def __init__( diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index b472e605a8..2d579921b2 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -89,8 +89,7 @@ def __init__( self._all_ys_and_sens = all_ys self._all_models = all_models - self._hermite_interpolation = all_yps is not None - if self.hermite_interpolation and not isinstance(all_yps, list): + if (all_yps is not None) and not isinstance(all_yps, list): all_yps = [all_yps] self._all_yps = all_yps @@ -419,7 +418,7 @@ def all_yps(self): @property def hermite_interpolation(self): - return self._hermite_interpolation + return self.all_yps is not None @property def t_event(self): @@ -565,8 +564,7 @@ def update(self, variables): # Single variable if isinstance(variables, str): - self._update_variable(variables) - return + variables = [variables] # Process for variable in variables: @@ -631,6 +629,7 @@ def process_casadi_var(self, var_pybamm, inputs, ys_shape): var_sym = var_pybamm.to_casadi(t_MX, y_MX, inputs=inputs_MX_dict) opts = { + "cse": True, "inputs_check": False, "is_diff_in": [False, False, False], "is_diff_out": [False], @@ -653,6 +652,12 @@ def process_casadi_var(self, var_pybamm, inputs, ys_shape): opts, ) + # Some variables, like interpolants, cannot be expanded + try: + var_casadi = var_casadi.expand() + except Exception: + pass + return var_casadi def __getitem__(self, key): diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 6013bd90a2..82cd476fad 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -133,7 +133,7 @@ def test_processed_variable_0D(self, hermite_interp): # check that repeated calls return the same data data1 = processed_var.data - assert processed_var._raw_data_initialized + assert processed_var.entries_raw_initialized data2 = processed_var.data @@ -141,7 +141,7 @@ def test_processed_variable_0D(self, hermite_interp): data_t1 = processed_var(sol.t) - assert processed_var._xr_data_array_raw is not None + assert processed_var.xr_array_raw_initialized data_t2 = processed_var(sol.t) From 7b7dbcdfae9680a265a40d3b540d8416206850f6 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:10:39 -0400 Subject: [PATCH 14/18] fix `try/except` --- src/pybamm/solvers/solution.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index 2d579921b2..3c557435bf 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -654,11 +654,11 @@ def process_casadi_var(self, var_pybamm, inputs, ys_shape): # Some variables, like interpolants, cannot be expanded try: - var_casadi = var_casadi.expand() - except Exception: - pass + var_casadi_out = var_casadi.expand() + except RuntimeError: + var_casadi_out = var_casadi - return var_casadi + return var_casadi_out def __getitem__(self, key): """Read a variable from the solution. Variables are created 'just in time', i.e. From fe2ab96900b767a09dd278609baf777e942a24f0 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:06:55 -0400 Subject: [PATCH 15/18] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f70aae0272..fd590fbc12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464)) - Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415)) - Added OpenMP parallelization to IDAKLU solver for lists of input parameters ([#4449](https://github.com/pybamm-team/PyBaMM/pull/4449)) - Added phase-dependent particle options to LAM From 276b9133de9ef9701cf506e17a15026d56f164ed Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:10:36 -0400 Subject: [PATCH 16/18] address comments --- .../c_solvers/idaklu/IDAKLUSolverOpenMP.hpp | 12 +- .../c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 6 +- src/pybamm/solvers/idaklu_solver.py | 47 +++---- src/pybamm/solvers/processed_variable.py | 129 ++++++++++++------ .../solvers/processed_variable_computed.py | 2 +- src/pybamm/solvers/solution.py | 11 +- .../test_simulation_with_experiment.py | 8 +- .../test_solvers/test_processed_variable.py | 6 + 8 files changed, 138 insertions(+), 83 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp index 9997e537fe..ee2c03abff 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp @@ -207,12 +207,12 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver * @brief Set the step values */ void SetStep( - realtype &tval, - realtype *y_val, - realtype *yp_val, - vector const &yS_val, - vector const &ypS_val, - int &i_save + realtype &tval, + realtype *y_val, + realtype *yp_val, + vector const &yS_val, + vector const &ypS_val, + int &i_save ); /** diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index 6139e2570f..c8e53e4a7a 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -1,10 +1,6 @@ #include "Expressions/Expressions.hpp" #include "sundials_functions.hpp" #include - -// import the IDAMem class -#include - #include "common.hpp" #include "SolutionData.hpp" @@ -372,6 +368,8 @@ SolutionData IDAKLUSolverOpenMP::solve( const int number_of_evals = t_eval.size(); const int number_of_interps = t_interp.size(); + // Hermite interpolation is only available when saving + // 1. adaptive steps and 2. the full solution save_hermite = ( solver_opts.hermite_interpolation && save_adaptive_steps && diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 9e266dde14..80eaffebf4 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -762,6 +762,16 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None): The times (in seconds) at which to interpolate the solution. Defaults to `None`, which returns the adaptive time-stepping times. """ + if not ( + model.convert_to_format == "casadi" + or ( + model.convert_to_format == "jax" + and self._options["jax_evaluator"] == "iree" + ) + ): # pragma: no cover + # Shouldn't ever reach this point + raise pybamm.SolverError("Unsupported IDAKLU solver configuration.") + inputs_list = inputs_list or [{}] # stack inputs so that they are a 2D array of shape (number_of_inputs, number_of_parameters) @@ -775,8 +785,6 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None): else: inputs = np.array([[]]) - save_adaptive_steps = t_interp is None or len(t_interp) == 0 - # stack y0full and ydot0full so they are a 2D array of shape (number_of_inputs, number_of_states + number_of_parameters * number_of_states) # note that y0full and ydot0full are currently 1D arrays (i.e. independent of inputs), but in the future we will support # different initial conditions for different inputs (see https://github.com/pybamm-team/PyBaMM/pull/4260). For now we just repeat the same initial conditions for each input @@ -787,32 +795,21 @@ def _integrate(self, model, t_eval, inputs_list=None, t_interp=None): atol = self._check_atol_type(atol, y0full.size) timer = pybamm.Timer() - if model.convert_to_format == "casadi" or ( - model.convert_to_format == "jax" - and self._options["jax_evaluator"] == "iree" - ): - solns = self._setup["solver"].solve( - t_eval, - t_interp, - y0full, - ydot0full, - inputs, - ) - else: # pragma: no cover - # Shouldn't ever reach this point - raise pybamm.SolverError("Unsupported IDAKLU solver configuration.") + solns = self._setup["solver"].solve( + t_eval, + t_interp, + y0full, + ydot0full, + inputs, + ) integration_time = timer.time() return [ - self._post_process_solution( - soln, model, integration_time, inputs_dict, save_adaptive_steps - ) + self._post_process_solution(soln, model, integration_time, inputs_dict) for soln, inputs_dict in zip(solns, inputs_list) ] - def _post_process_solution( - self, sol, model, integration_time, inputs_dict, save_adaptive_steps - ): + def _post_process_solution(self, sol, model, integration_time, inputs_dict): number_of_sensitivity_parameters = self._setup[ "number_of_sensitivity_parameters" ] @@ -851,11 +848,7 @@ def _post_process_solution( else: raise pybamm.SolverError(f"FAILURE {self._solver_flag(sol.flag)}") - if ( - self._options["hermite_interpolation"] - and save_adaptive_steps - and (not save_outputs_only) - ): + if sol.yp.size > 0: yp = sol.yp.reshape((number_of_timesteps, number_of_states)).T else: yp = None diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index 4eb09827aa..debb7ed3db 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -6,6 +6,7 @@ import pybamm from scipy.integrate import cumulative_trapezoid import xarray as xr +import bisect class ProcessedVariable: @@ -23,7 +24,7 @@ class ProcessedVariable: Note that this can be any kind of node in the expression tree, not just a :class:`pybamm.Variable`. When evaluated, returns an array of size (m,n) - base_variable_casadis : list of :class:`casadi.Function` + base_variables_casadi : list of :class:`casadi.Function` A list of casadi functions. When evaluated, returns the same thing as `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` @@ -71,7 +72,6 @@ def __init__( self.base_eval_shape = self.base_variables[0].shape self.base_eval_size = self.base_variables[0].size - # xr_data_array is initialized self._xr_array_raw = None self._entries_raw = None self._entries_for_interp_raw = None @@ -79,18 +79,16 @@ def __init__( def initialise(self): if self.entries_raw_initialized: - entries = self._entries_raw - entries_for_interp = self._entries_for_interp_raw - coords = self._coords_raw - else: - entries = self.observe_raw() + return + + entries = self.observe_raw() - t = self.t_pts - entries_for_interp, coords = self._interp_setup(entries, t) + t = self.t_pts + entries_for_interp, coords = self._interp_setup(entries, t) - self._entries_raw = entries - self._entries_for_interp_raw = entries_for_interp - self._coords_raw = coords + self._entries_raw = entries + self._entries_for_interp_raw = entries_for_interp + self._coords_raw = coords def observe_and_interp(self, t, fill_value): """ @@ -116,46 +114,59 @@ def observe_raw(self): t = self.t_pts # For small number of points, use Python - if pybamm.has_idaklu() and (t.size > 1): + if pybamm.has_idaklu(): entries = self._observe_raw_cpp() else: entries = self._observe_raw_python() return self._observe_postfix(entries, t) - def _setup_cpp_inputs(self): + def _setup_cpp_inputs(self, t, full_range): pybamm.logger.debug("Setting up C++ interpolation inputs") - ts = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(self.all_ts) - ys = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(self.all_ys) + ts = self.all_ts + ys = self.all_ys + yps = self.all_yps + inputs = self.all_inputs_casadi + # Find the indices of the time points to observe + if full_range: + idxs = range(len(ts)) + else: + idxs = _find_ts_indices(ts, t) + + if isinstance(idxs, list): + # Extract the time points and inputs + ts = [ts[idx] for idx in idxs] + ys = [ys[idx] for idx in idxs] + if self.hermite_interpolation: + yps = [yps[idx] for idx in idxs] + inputs = [self.all_inputs_casadi[idx] for idx in idxs] + + is_f_contiguous = _is_f_contiguous(ys) + + ts = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(ts) + ys = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(ys) if self.hermite_interpolation: - yps = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray( - self.all_yps - ) + yps = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(yps) else: yps = None + inputs = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray(inputs) # Generate the serialized C++ functions only once funcs_unique = {} - funcs = [None] * len(self.base_variables_casadi) - - for i, vars in enumerate(self.base_variables_casadi): + funcs = [None] * len(idxs) + for i in range(len(idxs)): + vars = self.base_variables_casadi[idxs[i]] if vars not in funcs_unique: funcs_unique[vars] = vars.serialize() funcs[i] = funcs_unique[vars] - inputs = pybamm.solvers.idaklu_solver.idaklu.VectorRealtypeNdArray( - self.all_inputs_casadi - ) - - is_f_contiguous = _is_f_contiguous(self.all_ys) - return ts, ys, yps, funcs, inputs, is_f_contiguous def _observe_hermite_cpp(self, t): pybamm.logger.debug("Observing and Hermite interpolating the variable in C++") - ts, ys, yps, funcs, inputs, _ = self._setup_cpp_inputs() + ts, ys, yps, funcs, inputs, _ = self._setup_cpp_inputs(t, full_range=False) shapes = self._shape(t) return pybamm.solvers.idaklu_solver.idaklu.observe_hermite_interp( t, ts, ys, yps, inputs, funcs, shapes @@ -163,7 +174,10 @@ def _observe_hermite_cpp(self, t): def _observe_raw_cpp(self): pybamm.logger.debug("Observing the variable raw data in C++") - ts, ys, _, funcs, inputs, is_f_contiguous = self._setup_cpp_inputs() + t = self.t_pts + ts, ys, _, funcs, inputs, is_f_contiguous = self._setup_cpp_inputs( + t, full_range=True + ) shapes = self._shape(self.t_pts) return pybamm.solvers.idaklu_solver.idaklu.observe( @@ -171,16 +185,16 @@ def _observe_raw_cpp(self): ) def _observe_raw_python(self): - pass # pragma: no cover + raise NotImplementedError # pragma: no cover def _observe_postfix(self, entries, t): return entries def _interp_setup(self, entries, t): - pass # pragma: no cover + raise NotImplementedError # pragma: no cover def _shape(self, t): - pass # pragma: no cover + raise NotImplementedError # pragma: no cover def _process_spatial_variable_names(self, spatial_variable): if len(spatial_variable) == 0: @@ -354,8 +368,7 @@ def entries(self): variable has not been initialized (i.e. the entries have not been calculated), then the processed variable is initialized first. """ - if not self.entries_raw_initialized: - self.initialise() + self.initialise() return self._entries_raw @property @@ -529,7 +542,7 @@ class ProcessedVariable1D(ProcessedVariable): Note that this can be any kind of node in the expression tree, not just a :class:`pybamm.Variable`. When evaluated, returns an array of size (m,n) - base_variable_casadis : list of :class:`casadi.Function` + base_variables_casadi : list of :class:`casadi.Function` A list of casadi functions. When evaluated, returns the same thing as `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` @@ -626,7 +639,7 @@ class ProcessedVariable2D(ProcessedVariable): Note that this can be any kind of node in the expression tree, not just a :class:`pybamm.Variable`. When evaluated, returns an array of size (m,n) - base_variable_casadis : list of :class:`casadi.Function` + base_variables_casadi : list of :class:`casadi.Function` A list of casadi functions. When evaluated, returns the same thing as `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` @@ -792,7 +805,7 @@ class ProcessedVariable2DSciKitFEM(ProcessedVariable2D): Note that this can be any kind of node in the expression tree, not just a :class:`pybamm.Variable`. When evaluated, returns an array of size (m,n) - base_variable_casadis : list of :class:`casadi.Function` + base_variables_casadi : list of :class:`casadi.Function` A list of casadi functions. When evaluated, returns the same thing as `base_Variable.evaluate` (but more efficiently). solution : :class:`pybamm.Solution` @@ -905,3 +918,43 @@ def _is_sorted(t): bool: True if array is sorted """ return np.all(t[:-1] <= t[1:]) + + +def _find_ts_indices(ts, t): + """ + Parameters: + - ts: A list of numpy arrays (each sorted) whose values are successively increasing. + - t: A sorted list or array of values to find within ts. + + Returns: + - indices: A list of indices from `ts` such that at least one value of `t` falls within ts[idx]. + """ + + indices = [] + + # Get the minimum and maximum values of the target values `t` + t_min, t_max = t[0], t[-1] + + # Step 1: Use binary search to find the range of `ts` arrays where t_min and t_max could lie + low_idx = bisect.bisect_left([ts_arr[-1] for ts_arr in ts], t_min) + high_idx = bisect.bisect_right([ts_arr[0] for ts_arr in ts], t_max) + + # Step 2: Iterate over the identified range + for idx in range(low_idx, high_idx): + ts_min, ts_max = ts[idx][0], ts[idx][-1] + + # Binary search within `t` to check if any value falls within [ts_min, ts_max] + i = bisect.bisect_left(t, ts_min) + if i < len(t) and t[i] <= ts_max: + # At least one value of t is within ts[idx] + indices.append(idx) + + # extrapolating + if (t[-1] > ts[-1][-1]) and (len(indices) == 0 or indices[-1] != len(ts) - 1): + indices.append(len(ts) - 1) + + if len(indices) == len(ts): + # All indices are included + return range(len(ts)) + + return indices diff --git a/src/pybamm/solvers/processed_variable_computed.py b/src/pybamm/solvers/processed_variable_computed.py index 01db7ffbdf..4f0cccc8c3 100644 --- a/src/pybamm/solvers/processed_variable_computed.py +++ b/src/pybamm/solvers/processed_variable_computed.py @@ -27,7 +27,7 @@ class ProcessedVariableComputed: Note that this can be any kind of node in the expression tree, not just a :class:`pybamm.Variable`. When evaluated, returns an array of size (m,n) - base_variable_casadis : list of :class:`casadi.Function` + base_variables_casadi : list of :class:`casadi.Function` A list of casadi functions. When evaluated, returns the same thing as `base_Variable.evaluate` (but more efficiently). base_variable_data : list of :numpy:array diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index 3c557435bf..3df7257664 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -616,7 +616,6 @@ def _update_variable(self, variable): vars_pybamm, vars_casadi, self, cumtrapz_ic=cumtrapz_ic ) - # Save variable self._variables[variable] = var def process_casadi_var(self, var_pybamm, inputs, ys_shape): @@ -640,9 +639,9 @@ def process_casadi_var(self, var_pybamm, inputs, ys_shape): # Casadi has a bug where it does not correctly handle arrays with # zeros padded at the beginning or end. To avoid this, we add and - # subtract a small number to the variable to reinforce the - # variable bounds. - epsilon = 5e-324 + # subtract the same number to the variable to reinforce the + # variable bounds. This does not affect the answer + epsilon = 1.0 var_sym = (var_sym - epsilon) + epsilon var_casadi = casadi.Function( @@ -655,7 +654,9 @@ def process_casadi_var(self, var_pybamm, inputs, ys_shape): # Some variables, like interpolants, cannot be expanded try: var_casadi_out = var_casadi.expand() - except RuntimeError: + except RuntimeError as error: + if "'eval_sx' not defined for" not in str(error): + raise error # pragma: no cover var_casadi_out = var_casadi return var_casadi_out diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index 394d64b257..b455e72393 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -83,8 +83,12 @@ def test_run_experiment(self): assert len(sol.cycles) == 1 # Test outputs - np.testing.assert_array_equal(sol.cycles[0].steps[0]["C-rate"].data, 1 / 20) - np.testing.assert_array_equal(sol.cycles[0].steps[1]["Current [A]"].data, -1) + np.testing.assert_array_almost_equal( + sol.cycles[0].steps[0]["C-rate"].data, 1 / 20 + ) + np.testing.assert_array_almost_equal( + sol.cycles[0].steps[1]["Current [A]"].data, -1 + ) np.testing.assert_array_almost_equal( sol.cycles[0].steps[2]["Voltage [V]"].data, 4.1, decimal=5 ) diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index 82cd476fad..cf1176aa48 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -147,6 +147,12 @@ def test_processed_variable_0D(self, hermite_interp): np.testing.assert_array_equal(data_t1, data_t2) + # check that C++ and Python give the same result + if pybamm.has_idaklu(): + np.testing.assert_array_equal( + processed_var._observe_raw_cpp(), processed_var._observe_raw_python() + ) + @pytest.mark.parametrize("hermite_interp", _hermite_args) def test_processed_variable_0D_no_sensitivity(self, hermite_interp): # without space From a4aa9fe17dceadad6ac70e566663b824c3b78ddf Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:48:50 -0400 Subject: [PATCH 17/18] initialize `save_hermite` --- .../solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl | 3 +++ src/pybamm/solvers/solution.py | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl index c8e53e4a7a..d128ae1809 100644 --- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl +++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl @@ -86,6 +86,9 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP( // The default is to solve a DAE for generality. This may be changed // to an ODE during the Initialize() call is_ODE = false; + + // Will be overwritten during the solve() call + save_hermite = solver_opts.hermite_interpolation; } template diff --git a/src/pybamm/solvers/solution.py b/src/pybamm/solvers/solution.py index 3df7257664..c884e79e34 100644 --- a/src/pybamm/solvers/solution.py +++ b/src/pybamm/solvers/solution.py @@ -888,19 +888,22 @@ def __add__(self, other): return new_sol # Update list of sub-solutions + hermite_interpolation = ( + other.hermite_interpolation and self.hermite_interpolation + ) if other.all_ts[0][0] == self.all_ts[-1][-1]: # Skip first time step if it is repeated all_ts = self.all_ts + [other.all_ts[0][1:]] + other.all_ts[1:] all_ys = self.all_ys + [other.all_ys[0][:, 1:]] + other.all_ys[1:] - if self.hermite_interpolation: + if hermite_interpolation: all_yps = self.all_yps + [other.all_yps[0][:, 1:]] + other.all_yps[1:] else: all_ts = self.all_ts + other.all_ts all_ys = self.all_ys + other.all_ys - if self.hermite_interpolation: + if hermite_interpolation: all_yps = self.all_yps + other.all_yps - if not self.hermite_interpolation: + if not hermite_interpolation: all_yps = None # sensitivities can be: From 4e708af5b6c40a4fbf835fa777a653d1e57c3ce1 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:22:10 -0400 Subject: [PATCH 18/18] fix codecov --- src/pybamm/solvers/processed_variable.py | 4 +++- tests/unit/test_solvers/test_processed_variable.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pybamm/solvers/processed_variable.py b/src/pybamm/solvers/processed_variable.py index debb7ed3db..5cf928ca7f 100644 --- a/src/pybamm/solvers/processed_variable.py +++ b/src/pybamm/solvers/processed_variable.py @@ -117,7 +117,9 @@ def observe_raw(self): if pybamm.has_idaklu(): entries = self._observe_raw_cpp() else: - entries = self._observe_raw_python() + # Fallback method for when IDAKLU is not available. To be removed + # when the C++ code is migrated to a new repo + entries = self._observe_raw_python() # pragma: no cover return self._observe_postfix(entries, t) diff --git a/tests/unit/test_solvers/test_processed_variable.py b/tests/unit/test_solvers/test_processed_variable.py index cf1176aa48..04de88963d 100644 --- a/tests/unit/test_solvers/test_processed_variable.py +++ b/tests/unit/test_solvers/test_processed_variable.py @@ -94,6 +94,13 @@ def _process_and_check_2D_variable( processed_var.entries, np.reshape(y_sol, [len(first_sol), len(second_sol), len(t_sol)]), ) + + # check that C++ and Python give the same result + if pybamm.has_idaklu(): + np.testing.assert_array_equal( + processed_var._observe_raw_cpp(), processed_var._observe_raw_python() + ) + return y_sol, first_sol, second_sol, t_sol, yp_sol @pytest.mark.parametrize("hermite_interp", _hermite_args) @@ -263,6 +270,12 @@ def test_processed_variable_1D(self, hermite_interp): processed_eqn2.entries, y_sol + x_sol[:, np.newaxis] ) + # check that C++ and Python give the same result + if pybamm.has_idaklu(): + np.testing.assert_array_equal( + processed_eqn2._observe_raw_cpp(), processed_eqn2._observe_raw_python() + ) + @pytest.mark.parametrize("hermite_interp", _hermite_args) def test_processed_variable_1D_unknown_domain(self, hermite_interp): x = pybamm.SpatialVariable("x", domain="SEI layer", coord_sys="cartesian")