From 7937f1acee166b6185ff24a20411ae0397baea6e Mon Sep 17 00:00:00 2001 From: mongibellili Date: Wed, 23 Oct 2024 14:02:10 +0200 Subject: [PATCH] update the docs: internals explanation --- docs/src/assets/img/diagram.png | Bin 0 -> 32062 bytes docs/src/dependencies.md | 28 +++ docs/src/developerGuide.md | 4 +- docs/src/implementation.md | 406 ++++++++++++++++++++++++++++++++ 4 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 docs/src/assets/img/diagram.png create mode 100644 docs/src/dependencies.md create mode 100644 docs/src/implementation.md diff --git a/docs/src/assets/img/diagram.png b/docs/src/assets/img/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..df2b2bd60a3c6f87798eb4a4a73fa208915fefe0 GIT binary patch literal 32062 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

fIAYPmC!R0%4uz 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 KcFC#pH~$wb>mZu| literal 0 HcmV?d00001 diff --git a/docs/src/dependencies.md b/docs/src/dependencies.md new file mode 100644 index 0000000..9047a8f --- /dev/null +++ b/docs/src/dependencies.md @@ -0,0 +1,28 @@ +# Dependencies + +In addition, the package contains several shared helper functions used during the integration +process by the algorithm such as the scheduler that organizes which variable of the system to +update at any specific time of the simulation. The solver uses other packages such as MacroTools for user-code parsing, SymEngine for Jacobian computation and dependencies extraction, and a modified TaylorSeries that uses caching to obtain free Taylor variable operations as the current version of TaylorSeries creates a heap allocated object for every operation. The approximation through Taylor variables transforms any complicated equations to polynomials, which makes root finding cheaper, which the QSS methods relies heavily on it. +## MacroTools +MacroTools offers a comprehensive library of tools for manipulating Julia expressions, enabling deep code transformations through features such as π‘π‘œπ‘ π‘‘π‘€π‘Žπ‘™π‘˜ and @π‘π‘Žπ‘π‘‘π‘’π‘Ÿπ‘’. The π‘π‘œπ‘ π‘‘π‘€π‘Žπ‘™π‘˜ function plays a pivotal role within the NLodeProblem function, particularly for user code parsing. +By systematically traversing each operation, π‘π‘œπ‘ π‘‘π‘€π‘Žπ‘™π‘˜ allows the code to recognize and differentiate between symbols. For instance, it is crucial for integrating parameters and helper functions directly into the equations. Furthermore, π‘π‘œπ‘ π‘‘π‘€π‘Žπ‘™π‘˜ is used to traverse both the right-hand side of differential equations and zero-crossing functions, facilitating the construction of the Jacobian matrix and identifying variable dependencies. It also transforms specific expressions like π‘ž[1] into π‘ž[1] [0] within events, and converts π‘ž[𝑖] to π‘žπ‘–, making the equations more tractable for differentiation and Jacobian construction. Additionally, the @π‘π‘Žπ‘π‘‘π‘’π‘Ÿπ‘’ function efficiently handles the case where differential equations are defined as expressions within a for loop, ensuring smooth parsing and processing of these equations. The ability to transform and capture expressions ensures that user-defined code is processed efficiently, making MacroTools an indispensable component in the QSS solver workflow. +## SymEngine +SymEngine is a high-performance symbolic manipulation library designed for efficient mathematical computations. A key feature of SymEngine is its ability to convert expressions into a symbolic form using the convert(Basic, m) function, where m is typically an expression, such as the right-hand side of a differential equation. This conversion transforms the expression into a Basic type, which is a fundamental structure in SymEngine for symbolic computation. Once the expression is in Basic form, the diff(basi, symarg) function can be applied to perform symbolic differentiation, where basi is the converted expression and symarg is the symbol with respect to which the derivative is taken. This returns the partial derivative of the expression, making it particularly useful for deriving system Jacobians. SymEngine’s ability to handle symbolic differentiation with speed and accuracy makes it an essential tool in Jacobian computation for the QSS solver. +## Taylor Variables +In the quantized system solver, and similar to the Taylor struct from the package TaylorSeries.jl, the Taylor0 struct serves as a convenient representation of Taylor series approximations. Each instance encapsulates an array of coefficients, which correspond to the Taylor series expansion, along with an order indicating the degree of the expansion. This structure allows for efficient storage of state values and their derivatives, as the first coefficient represents the variable value, while subsequent coefficients represent higher-order derivatives. The use of Taylor variables enables the computation of derivatives for complex expressions seamlessly, enhancing the solver’s capability to analyze and simulate dynamic systems. The implementation includes various constructors and methods to manipulate and evaluate the Taylor series, ensuring that users can easily derive the necessary values from the stored Taylor variables, thus facilitating precise calculations +of system behavior over time. +New functions are created to match each old function but with a different name and added +caches in the parameters. These new functions are designed to optimize performance by avoiding memory allocation during their execution. This is achieved through careful design and implementation of the functions, where arithmetic operations and mathematical functions leverage the existing cached data rather than creating new instances of taylor variables. In addition, the old Taylor is kept, with minimum functionalities, as a fallback in case there is an expression that does not use the available cache vector. As a result, this approach enhances the efficiency of the Taylor variable framework, making it more suitable for complex simulations and calculations without sacrificing flexibility or usability. + +arithmetic operations: The provided code introduces personalized arithmetic operations +that optimize performance by eliminating memory allocation during calculations. Functions +such as addsub, subsub, and mulT are designed to operate directly on existing data structures, utilizing a caching mechanism to store intermediate results. This is in stark contrast to traditional arithmetic operations that typically allocate memory with each computation, creating overhead and potentially slowing down performance in computationally intensive tasks. The previous approach required reallocation for each arithmetic operation, leading to inefficiencies when processing large datasets or performing numerous calculations. +mathematical functions: Similar to arithmetic operations, new mathematical functions are +designed to leverage in-place operations, which reduces the overhead associated with creating +new memory for intermediate results. For instance, functions like exp, log, sin, and cos, sqrt, power iterate over the input Taylor series and apply the corresponding operation without allocating additional arrays. +By implementing these new methods, the code significantly reduces the number of allocations, improving both execution speed and resource management, while still maintaining the +flexibility and functionality required for complex mathematical operations. +transformation of expressions: The transformF function is designed to translate userdefined mathematical expressions into optimized forms that leverage the custom arithmetic and +function implementations. By traversing the expression tree with the prewalk function, it identifies operations such as addition, subtraction, multiplication, division, and specific mathematical functions (like exponential and logarithmic functions). For each identified operation, the code modifies the expression to call specialized versions (e.g., subT, addT, mulT) that avoid memory allocation, enhancing performance during calculations. Additionally, it tracks the number of caches needed for these operations, which are essential for efficient computation of Taylor series. +Each time a transformation occurs, a reference to a cache is added, ensuring that a necessary +cache is available for the optimized functions. This systematic approach effectively restructures the original expressions, maintaining their functionality while improving computational efficiency by reducing unnecessary allocations. diff --git a/docs/src/developerGuide.md b/docs/src/developerGuide.md index 4ae0028..ada7e4d 100644 --- a/docs/src/developerGuide.md +++ b/docs/src/developerGuide.md @@ -2,7 +2,9 @@ While the package is optimized to be fast, extensibility is not compromised. It is divided into 3 entities that can be extended separately: Problem, Algorithm, and Solution. The package uses other packages such as MacroTools.jl for user-code parsing, SymEngine.jl for Jacobian computation, and a modified TaylorSeries.jl that uses caching to obtain free Taylor variables. The approximation through Taylor variables transforms any complicated equations to polynomials, which makes root finding cheaper. - +### [Implementation ](@ref) +### [Dependencies ](@ref) +### [Algorithm Extension ](@ref) ### [Algorithm Extension ](@ref) ### [Problem Extension](@ref) ### [Solution Extension](@ref) diff --git a/docs/src/implementation.md b/docs/src/implementation.md new file mode 100644 index 0000000..2d5eb6e --- /dev/null +++ b/docs/src/implementation.md @@ -0,0 +1,406 @@ +# Implementation +### Package Structure {#ch5:section:Package-description} + +While the package is optimized to be fast, extensibility is not +compromised. It is divided into 3 entities that can be extended +separately: Problem, Algorithm, and Solution. The rest of the code is to +create these entities and glue them together as shown in the Figure. + +::: center +![The QSS Solver Structure.](./assets/img/diagram.png){#fig:qssdiag width="10cm" +height="6cm"} +::: + +The API was designed to provide an easier way to handle events than the +approach provided by the classic integration solvers. Inside an +NLodeProblem function, the user may introduce any parameters, variables, +equations, and events: + + odeprob = NLodeProblem(quote + name + parameters + discrete and continous variables + helper expressions + differential equations + if-statments for events) + +The output of this function is an object of type Problem, and it is +passed to the solve function along any other configuration arguments +such as the algorithm type, the time span and the tolerance. The solve +function dispatches on the given algorithm and start the numerical +integration. + + tspan = (start time, end time) + sol= solve(odeprob,algorithm,tspan,abstol=...,reltol=...) + +A the end, a solution object is produced that can be queried and +plotted. + + sol(time,idxs=variable index) + sol.stats + plot(sol) + + +### Internal Code Explained {#ch5:section:code-explained} + +The solver structure is designed for modularity, allowing for different +QSS algorithms and quantizer orders to be used in the simulation. The +core components of the solver include the NLodeProblem function, the +solve function, the scheduler, the quantizer, and the solution object. +These components work together to parse user-provided code, solve the +system of ODEs using QSS algorithms, and store the results for analysis. +The following sections provide an in-depth explanation of the key +components and their interactions within the solver framework. + +#### creating the problem + +The NLodeProblem function is the entry point for defining a new problem +to be solved by the QSS solver. It takes user-provided code, which +includes system parameters, variables, equations, and event logic, and +constructs a Problem object that encapsulates all the necessary +information for the solver to simulate the system such as problem +dimensions, dependencies, and equations. The function works by parsing +the user code and extracting relevant data to populate the Problem +object. + +*NLODEDiscProblem{PRTYPE,T,Z,Y,CS}:* This is the struct that holds all +the necessary data for a nonlinear ordinary differential equation (ODE) +problem with discrete events. The structure includes various fields such +as initial conditions, discrete variables, Jacobians, event +dependencies, and other data related to how the problem is formulated. +This structure serves as the core data holder for the problem and will +be used in the solver. It is a parametric abstract type that has the +following parameters: + +PRTYPE: The type of the problem (to distinguish between various types, +and allow future extension of the solver to handle new types). + +T: The number of continuous variables (state variables). + +Z: The number of zero-crossing functions, which are used to detect +events. + +Y: The actual number of events. + +CS: Cache size, which is used to store intermediate operations. + +The use of abstract types in this context allows for flexibility and +extensibility in the solver. By defining these abstract types, the code +can be easily adapted to handle different types of problems, algorithms, +and solutions without needing to modify the core solver logic. This +design choice enhances the maintainability and scalability of the +solver, making it easier to add new features or support additional +problem types in the future. + +**NLodeProblemFunc:** After an initial preparation performed by the The +NLodeProblem function, The function NLodeProblemFunc takes the resulting +expressions to continue constructing an instance of the NLODEDiscProblem +structure. It works in several key stages: + +*Initialization:* The function begins by initializing vectors and +dictionaries that will hold equations (equs), Jacobian dependencies +(jac), zero-crossing functions (ZCjac), and event dependencies. These +serve to store the different types of equations and their relationships. + +*Processing ODEs:* It loops through each of the ODE expressions provided +by the user. Depending on the type of expression (discrete variables, +differential equations, or loop constructs), it processes the right-hand +side (RHS) of the equation. For differential equations, it extracts +dependencies to build the Jacobian and transform the equations into a +more appropriate form for further use. Special cases are handled, such +as if the RHS is a number or a symbol. + +*Handling Events:* The function also processes event-related constructs +(if conditions) that correspond to different points where the system +might undergo discrete changes. It process the RHS of the event +equations, transforms them into a suitable form, and builds the +necessary dependency structures. Specifically, it constructs how +discrete and continuous variables influence one another through the +events. + +*Constructing the Function Code:* After processing all ODEs and events, +the function dynamically generates a Julia function code needed to store +the system of ODEs and events. This code is built into a function that +handles different cases (i.e., which equation to evaluate based on an +index of a state change or an event). + +**Building Dependencies:** Several helper functions that build the +dependencies between variables, events. They build dependency vectors +that track how discrete and continous variables influence the system. +This is used to know what variables to update and determine when +specific events should be checked. By tracking the relationships between +variables and events, the solver can determine the appropriate actions +to take at each time step. The dependencies are stored in the following +vectors: + +\-$jac$: It determines which variables affect a derivative. + +\-$ZCjac$: It determines which variables affect a zero-crossing +function. + +\-$SD$: It determines which derivatives that are affected by a given +variable. + +\-$SZ$: It determines which zero-crossing functions that are affected by +a given variable. + +\-$HZ$: It tells which Zero-crossing functions influenced by a given +event. + +\-$HD$: It tells which derivatives influenced by a given event. + +Here's a quick summary and what each helper function is doing: + +*extractJacDepNormal:* It Extracts the dependencies for normal +(non-loop) expressions. It updates the Jacobian matrix $jac$ and a +dictionary $dD$ for tracking dependencies of derivatives to discrete +variables. + +*extractJacDepLoop:* Similar to extractJacDepNormal, but specifically +for loop expressions. It tracks dependencies across loop iterations. + +*extractZCJacDepNormal:* It Extracts zero-crossing Jacobian dependencies +for discrete variables ($dZ$), and it updates $zcjac$, $SZ$. + +*createDependencyToEventsDiscr:* It maps discrete dependencies (dD, dZ) +to specific events, it and constructs dependency matrices HZ and HD from +the discrete variables only. + +*createDependencyToEventsCont:* Similar to +createDependencyToEventsDiscr, but for continuous dependencies (SD, sZ), +and it updates the matrices HZ and HD from the continuous variables +only. + +*unionDependency:* Merges the two previous sets of dependencies +(continuous and discrete) into the final matrices HZ and HD. + +#### The solve function + +The solve function is the primary interface for solving non-linear ODE +problems using various QSS (Quantized State Systems) algorithms. It does +this by dispatching on the problem and algorithm types to select the +right solver. + +**QSS Algorithm:** The diagram shows three specific algorithms: (QSS +Algorithm, LiQSS Algorithm, mLiQSS Algorithm). The Algorithm type is +parametric on the solver name (N) and order (O). When a user doesn't +provide a solver explicitly, the solve function defaults to using the +modified second-order implicit algorith (mLiQSS2). This approach allows +flexibility: the user can select a different algorithm by providing an +alternative QSS Algorithm without needing to modify the solve function. +Based on the QSS Algorithm provided, it either selects a basic QSS +integration method or, in the case of LiQSS, constructs additional data +structures needed for implicit integration. The method defaults to +sparse handling as false (Val(false)), tolerances (abstol=1e-4 and +reltol=1e-3), and a maximum number of iterations (maxiters=1e7). These +parameters can be adjusted based on the problem's complexity and desired +accuracy. + +**Helper Functions:** + +*createCommonData:* This function sets up the common data required by +the QSS solver. It initializes all necessary vectors (x, q, tx, tq, +nextStateTime, etc.) used in the integration process, and it +pre-allocates a cache of Taylor series to avoid repeated memory +allocation during integration (taylorOpsCache). + +*getClosure:* This function creates a closure for the Jacobian and the +dependency matrices, allowing in a flexible in a way that enables easy +extension. The closure is used to access the Jacobian matrix during the +integration process. + +*createLiqssData:* For LiQSS solvers, this function sets up additional +data structures and auxiliary variables used for storing linear +approximation coefficients. + +#### The scheduler function + +The scheduler component retrieves the next index (a state change or an +event) for processing. It determines the next action by comparing three +types of timing events: state updates, events, and input updates. It +initializes minimum time variables for each action type to infinity and +iterates through the provided vectors to find the minimum times and +their corresponding indices. Based on these comparisons, it decides +whether the next scheduled action is an input update, an event, or a +state update. If no valid actions are found (indicated by a zero index), +the function assigns a default state indicating the next action is a +state update at time infinity. Finally, it returns a tuple containing +the index of the next action, its time, and a symbol representing the +type of action to take next (:STINPUT, :STEVENT, or :STSTATE). This +structure ensures that the simulation progresses accurately and +efficiently. + +#### The Quantizer functions + +The system handles different quantizer orders (Order 1 and Order 2). It +defines methods for state integration, derivative computation, event +time computation, updating quantized values, and cycle detection +updates. + +**Utils:** This section contains utility functions like root finding +that assist in the solution process. + +**computeNextTime:** for first-order, the state of the system changes at +a constant rate. The core calculation takes place when the first +derivative of the state, represented by $x[i][1]$, is non-zero. In this +case, the function determines the time to the next event by dividing a +quantum threshold by this derivative. Additionally, to prevent numerical +issues, it ensures that this calculated time-step does not fall below a +predefined minimum value, $absDeltaT$. If the first derivative is +extremely small or essentially zero, the function adjusts it to avoid +potential numerical instabilities that could arise from very small time +increments. + +For second-order, the system state evolves with both a rate of change +(first derivative) and acceleration (second derivative). The core +calculation occurs when the second derivative of the state, represented +by $x[i][2]$, is non-zero. In this case, the function computes the time +to the next event by using the square root of the ratio between a +quantum threshold and the second derivative. This ensures that the +time-step reflects the influence of the system's acceleration. +Additionally, to prevent numerical issues (such as division by zero or +overly small time-steps), a minimum delta time (absDeltaT) is enforced. +If the computed time-step is smaller than this threshold, the function +adjusts the second derivative to maintain stability. If the second +derivative is zero but the first derivative is non-zero, the time to the +next event is calculated based on the first derivative. The function +ensures that the time-step does not drop below absDeltaT, adjusting the +first derivative if necessary. If both derivatives are zero, the system +is assumed to have no change, and the next event time is set to infinity +(Inf). + +**recomputeNextTime:** The reComputeNextTime functions are used in the +explicit QSS algorithms to enable the recalculation of the next time +after interactions between different variables, such as variable i and +variable j. These functions determine the time until the system crosses +the quantum threshold by solving polynomial equations derived from the +difference between the quantized state and the actual state. In +situations where the quantum threshold has already been surpassed, they +promptly return a very small time increment (e.g., simt + 1e-12) to +trigger an immediate update of the system's state. + +**LiqssrecomputeNextTime:** In the LIQSS methods of first-order, the +recomputeNextTime function calculates the next event time based on the +current state (x), its first derivative (x1), and the quantized state +(q). First, if the difference between the current state and the +quantized state exceeds twice the quantum size, the next event is +scheduled almost immediately with a small time-step. Otherwise, if the +derivative is non-zero, the function computes the time-step by dividing +the state difference by the derivative. If the result is positive, this +time is added to the current simulation time (simt). If negative, it +adjusts the time-step by either adding or subtracting twice the quantum +size based on the direction of change. If the derivative is zero, +indicating no change, the event time is set to infinity. Lastly, if the +computed time is in the past, the function resets it to a far future +time to prevent any premature events. This ensures that the system +evolves smoothly and accurately. + +In the second-order LIQSS method, the recomputeNextTime function +calculates the next event time by considering both the state (x) and its +first (x1) and second (x2) derivatives, along with the quantized state +(q). The function constructs a polynomial using the current state, +derivative values, and the second derivative, then calculates the next +event time by finding the smallest positive root of this polynomial. + +**computeNextInputTime:** The computeNextInputTime functions focus on +computing the next action when derivatives depend only on time. They +assess changes in the derivatives over a specified elapsed time to +compute the time increment until the next input action. If the +derivatives are null, the function reverts to handling the system in a +lower-order thus simplifying the calculation. + +**computeNextEventTime:** The computeNextEventTime function calculates +the next event time based on the zero-crossing function ($ZCFun$) of a +system. It first checks if a sign change has occurred in the +zero-crossing function, indicating that the system is leaving zero and +should be considered an event, provided the previous value is +significantly different from zero (to prevent duplicate events). If a +sign change is detected, the function updates the event time to the +current simulation time. If both the old and new values of the +zero-crossing function are zero, it sets the next event time to +infinity, indicating no event should occur. For cases where the old and +new values have the same sign, it calculates the minimum positive root +of the zero-crossing function, representing the time of the next event, +ensuring that this time is not too close to zero to avoid spurious +events. The function then updates the old sign values for future +comparisons. + +#### The solution + +This is where the results of the simulation are stored, including: +Settings, Time points (times\[\]), Variables (vars\[\]), Statistics +(stats). + +**The solution Struct** The LightSol struct is specifically designed to +hold the solution of a system of ODEs. It contains several fields, +including size and order, which denote the number of continuous +variables and the order of the algorithm used, respectively. The +savedTimes and savedVars fields are vectors that store the time steps +and corresponding values of the continuous variables at which they were +recorded during the simulation. Other fields include algName and +sysName, which specify the names of the algorithm and system being +solved, alongside absQ, the absolute tolerance used during the +simulation. The stats field contains an instance of the Stats struct, +providing access to the performance metrics, while ft holds the final +time of the simulation. The Stats struct includes fields for totalSteps, +which counts the total number of steps taken throughout the simulation, +simulStepCount, representing the number of simultaneous updates during +the simulation, and evCount, which tallies the number of events that +occurred. Additionally, the numSteps vector keeps track of the number of +steps taken for each continuous variable involved in the simulation, +allowing for detailed analysis of the performance for individual +components. + +The functions defined in conjunction with these structs enable various +operations related to the simulation's solutions. The createSol function +initializes a LightSol instance with the specified parameters, while the +getindex function provides a helper to access either the saved times or +variables easily. The evaluateSol function allows for the evaluation of +the solution at a specified time, supporting linear interpolation +between saved points. Furthermore, the solInterpolated functions enable +the generation of interpolated solutions across specified intervals, +creating new solution objects based on the interpolated values and +times. The show function is tailored to present the statistics in a +user-friendly format, offering insights into the performance of the +simulation. Overall, these constructs provide a robust framework for +managing and analyzing the outcomes of simulations involving ODEs. + +**Plotting the Solution:** The plotSol function is designed to visually +represent the solution of a system of ODEs stored in the solution +object. It accepts a variety of optional parameters, allowing users to +customize their plots significantly. The function takes in a solution +object sol along with indices for the variables to be plotted (xvars). +Users can also specify a title, axis limits (xlims and ylims), and +visual styles, such as marker types and whether to display a legend. If +a title is not provided, it defaults to a composite of various +parameters, including the system name, algorithm name, absolute +tolerance, total steps taken, and more.Within the function, each +selected variable is plotted against its corresponding saved time, +utilizing different line styles for better distinction. If no specific +variables are selected, all variables in the solution are plotted by +default. After constructing the plot, the function checks for specified +axis limits and applies them accordingly. Finally, the function returns +the plot object, which can be further modified or saved. In addition to +basic plotting, the saveSol function allows users to save their plots as +PNG files, automatically generating filenames based on relevant +parameters and timestamps. This functionality is crucial for +documentation and sharing results in a reproducible manner. This +comprehensive plotting capability ensures that users can effectively +visualize the behavior of their systems, making the analysis of results +intuitive and informative. + +**Finding the Error:** The getError function computes the relative error +of a solution compared to a specified reference function, enabling users +to assess the accuracy of their numerical results. The function iterates +over the saved time points, and it calculates the sum of the squares of +the differences between the numerical solution and the true values +provided by the reference function, as well as the sum of the squares of +the true values. The relative error is then derived as the square root +of the ratio of these sums, providing a quantitative measure of the +solution's deviation from the expected results. This comprehensive error +analysis is essential for validating the performance of numerical +methods and ensuring that the solutions produced are both reliable and +precise. By comparing the numerical results to known reference values, +users can gain insights into the accuracy and effectiveness of the +solver, enabling them to make informed decisions about their simulations +and analyses. \ No newline at end of file