Skip to content

Commit a802faf

Browse files
author
camilo
committed
Improvements to pid & ltisys. Updated docs. bump to 1.3.9
1 parent 0066aca commit a802faf

File tree

10 files changed

+256
-40
lines changed

10 files changed

+256
-40
lines changed

doc/qltisys.dox

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,136 @@
176176
* }
177177
* }
178178
* @endcode
179+
*
180+
* @section qltisys_transportdelay Transport-Delay
181+
*
182+
* The qlibs::transportDelay class models a "pure time delay" in a discrete-time
183+
* system.
184+
* It holds incoming samples in a buffer and outputs a sample from the past after
185+
* a fixed number of discrete steps.
186+
*
187+
* Mathematically, a transport delay can be expressed as:
188+
* <center>
189+
* \f$ y[k] = x[k - N] \f$
190+
* </center>
191+
*
192+
* where:
193+
* - \f$ y[k] \f$ is the current output sample,
194+
* - \f$ x[k] \f$ is the current input sample,
195+
* - \f$ N \f$ is the number of discrete delay steps (@a numberOfDelays).
196+
*
197+
* This is useful for:
198+
* - Simulating signal propagation delays.
199+
* - Implementing dead-time elements in control systems.
200+
* - Testing systems where data arrival is intentionally delayed.
201+
*
202+
* The delay amount is determined in discrete steps, not directly in seconds.
203+
* To convert a time delay in seconds to discrete steps, you can use:
204+
* - The qlibs::delayFromTime() helper function, or
205+
* - The qlibs::timeDelay literal facility @c _td .
206+
*
207+
* In simulation or discrete-time signal processing, a pure delay shifts the
208+
* input signal in time without altering its shape or magnitude.
209+
* In continuous-time systems, a pure time delay is represented by:
210+
*
211+
* <center>
212+
* \f$ G(s) = e^{-sT_d} \f$
213+
* </center>
214+
*
215+
* where \f$ T_d \f$ is the delay time.
216+
* When discretized for simulation (with a time step \f$ \Delta t \f$), the delay
217+
* becomes a **FIFO buffer** of length:
218+
*
219+
* <center>
220+
* \f$ N = \frac{T_d}{\Delta t} \f$
221+
* </center>
222+
*
223+
* The qlibs::transportDelay class implements this buffer with:
224+
* - @b Insertion of the newest sample at each step.
225+
* - @b Extraction of the oldest stored sample as the delayed output.
226+
*
227+
* This is especially important in control system simulations where transport
228+
* delays affect stability and performance.
229+
*
230+
* @subsection td_basic Basic Example
231+
*
232+
* @code{.c}
233+
* #include <qlibs.h>
234+
*
235+
* using namespace qlibs;
236+
*
237+
* constexpr real_t dt = 0.1_re; // 100 ms simulation step
238+
* transportDelay<3> myDelay; // Delay by 3 discrete steps (0.3 seconds)
239+
*
240+
* void simulate() {
241+
* real_t inputSignal = 1.0_re;
242+
* real_t outputSignal = myDelay.delay(inputSignal);
243+
* // outputSignal contains the value of inputSignal from 0.3 seconds ago
244+
* }
245+
* @endcode
246+
*
247+
* @subsection td_timeconv Using Time Conversion
248+
*
249+
* If you want a delay in seconds but only know the simulation step (dt),
250+
* use qlibs::delayFromTime() or @c _td:
251+
*
252+
* @code{.c}
253+
* constexpr real_t dt = 0.1_re;
254+
*
255+
* // Delay by 2.5 seconds
256+
* transportDelay<delayFromTime(2.5, dt)> myDelay1;
257+
*
258+
* // Using the _td literal:
259+
* transportDelay<2.5_td(dt)> myDelay2;
260+
*
261+
* // Using operator[] form:
262+
* transportDelay<4.3_td[dt]> myDelay3;
263+
* @endcode
264+
*
265+
* @subsection td_continuous Example: Continuous System with Delay
266+
*
267+
* This example simulates a first-order system with a pure transport delay.
268+
* The system is:
269+
*
270+
* <center>
271+
* \f$ G(s) = \frac{1}{\tau s + 1} \cdot e^{-sT_d} \f$
272+
* </center>
273+
*
274+
* The delay is implemented with qlibs::transportDelay after the continuous system
275+
* output.
276+
*
277+
* @code{.c}
278+
* #include <iostream>
279+
* #include <chrono>
280+
* #include <thread>
281+
* #include <qlibs.h>
282+
*
283+
* using namespace qlibs;
284+
*
285+
* void xTaskSystemSimulate( void )
286+
* {
287+
* // Continuous system parameters
288+
* constexpr real_t tau = 1.0_re; // Time constant
289+
* constexpr real_t dt = 0.01_re; // Simulation step
290+
* constexpr real_t Td = 0.5_re; // Transport delay in seconds
291+
* std::chrono::milliseconds delay(static_cast<long long>( dt*1000 ) );
292+
*
293+
* continuousTF<1> ctf= {
294+
* { 0.0f, 1.0f, },
295+
* { tau, 1.0f, },
296+
* };
297+
* continuousSystem gc( ctf, dt );
298+
* transportDelay<delayFromTime(Td, dt)> delayBlock;
299+
* real_t ut, yt;
300+
*
301+
* for( ;; ) {
302+
* ut = BSP_InputGet();
303+
* yt = delayBlock( gc.excite( ut ) );
304+
*
305+
* std::this_thread::sleep_for(delay);
306+
* std::cout << "u(t) = " << ut << " y(t) = " << yt << std::endl;
307+
* }
308+
* }
309+
* @endcode
310+
*
179311
*/

doc/qtdl.dox

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
/*! @page qtdl_desc Tapped Delay Line in O(1)
22
* The class \ref qtdl is an implementation of the Tapped Delay Line (TDL) structure.
3-
* A TDL is a discrete element in digital filter theory, that allows a signal to
3+
* A TDL is a discrete element in digital filter theory, that allows a signal to
44
* be delayed by a number of samples and provides an output signal for each delay.
5-
* Here, a TDL is implemented as a circular buffer. This means
5+
* Here, a TDL is implemented as a circular buffer. This means
66
* that the complexity of the enqueue and dequeue operations is constant @c O(1), so
77
* integer delays can be computed very efficiently.
88
*
9-
* The delay by one sample is notated \f$z^{-1}\f$ and delays of \f$N\f$ samples
10-
* is notated as \f$z^{-N}\f$ motivated by the role the z-transform plays in
9+
* The delay by one sample is notated \f$z^{-1}\f$ and delays of \f$N\f$ samples
10+
* is notated as \f$z^{-N}\f$ motivated by the role the z-transform plays in
1111
* describing digital filter structures.
1212
*
1313
* To create a TDL, you just need to define an instance of type \ref qlibs::tdl and
1414
* then, configure it by using the constructor or \ref qlibs::tdl::setup(),
15-
* where you can define the number of lines to be delayed and the initial values
15+
* where you can define the number of lines to be delayed and the initial values
1616
* for all of them. Then, you can start operating over this structure by inserting
1717
* samples of the input signal by using \ref qlibs::tdl::insertSample().
1818
* You can also get any specific delay from it by using:
1919
*
2020
* - \ref qlibs::tdl::getOldest(), to get the oldest delay at tap \f$z^{-N}\f$
21-
* You can also use the index at @c -1 as follows: @c "delay[ -1 ]"
2221
* - \ref qlibs::tdl::getAtIndex(), to get the delay at tap \f$z^{-i}\f$.
2322
* You can also use the index using an unsigned value as follows: @c "delay[ i ]"
2423
* - Index operator
@@ -27,7 +26,7 @@
2726
* base component of some aspects of \ref qlibs::smoother and \ref qlibs::ltisys.
2827
*
2928
* @section qtdl_ex1 Example : Code snippet to instantiate a TDL to hold up to 256 delays.
30-
*
29+
*
3130
* @code{.c}
3231
* real_t storage[ 256 ] = { 0.0f };
3332
* tdl delay( storage );
@@ -41,6 +40,6 @@
4140
*
4241
* @code{.c}
4342
* auto d3 = delay[ 3 ]; // get delay at t-3, same as delay.getAtIndex( 3 )
44-
* auto dOldest = delay[ -1 ]; // get delay at t-255 , same as delay.getOldest()
43+
* auto dOldest = delay.getOldest(); // get the oldest sample at t-N
4544
* @endcode
4645
*/

library.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"maintainer": true
1717
}
1818
],
19-
"version": "1.3.8",
19+
"version": "1.3.9",
2020
"license": "MIT",
2121
"frameworks": "arduino",
2222
"platforms": "*"

library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name=qlibs
2-
version=1.3.8
2+
version=1.3.9
33
license=MIT
44
author=J. Camilo Gomez C. <[email protected]>
55
maintainer=J. Camilo Gomez C. <[email protected]>

src/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cmake_minimum_required( VERSION 3.2 )
22
project( qlibs-cpp
3-
VERSION 1.3.8
3+
VERSION 1.3.9
44
DESCRIPTION "A collection of useful C++ libraries for embedded systems"
55
LANGUAGES CXX )
66

src/include/ltisys.hpp

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ namespace qlibs {
249249

250250
/**
251251
* @brief Returns the number of delay steps configured for this instance.
252-
*
252+
*
253253
* @return The number of delays (equal to the template parameter).
254254
*/
255255
size_t getNumberOfDelays() const noexcept {
@@ -802,6 +802,10 @@ namespace qlibs {
802802
bool setIntegrationMethod( integrationMethod m );
803803
};
804804

805+
806+
using customProcessModel = real_t(*)(real_t, void*);
807+
808+
805809
/**
806810
* @brief A Smith Predictor implementation for compensating time delays in
807811
* control systems.
@@ -812,10 +816,12 @@ namespace qlibs {
812816
*/
813817
class smithPredictor {
814818
private:
815-
ltisys *model;
819+
ltisys *model{nullptr};
820+
customProcessModel modelAlternate{ nullptr };
816821
ITransportDelay *modelDelay;
817822
ltisys *filter{ nullptr };
818823
real_t yp_hat;
824+
void *alternateData{ nullptr };
819825
public:
820826
virtual ~smithPredictor() {}
821827
/**
@@ -835,6 +841,22 @@ namespace qlibs {
835841
const real_t initialCondition = 0.0_re )
836842
: model(&modelTf), modelDelay(&mDelay), yp_hat(initialCondition) {}
837843

844+
/**
845+
* @brief Constructs a Smith Predictor with a plant model, delay model,
846+
* and optional initial output estimate.
847+
* @param[in] modelCustom Reference to the system model representing the
848+
* delay-free plant. User should define a custom function that recreates
849+
* the system dynamics.
850+
* @param[in] mDelay Reference to the transport delay block modeling the
851+
* plant’s dead time.
852+
* @param[in] initialCondition Initial value for the internal output
853+
* prediction (@c yp_hat). Default is 0.0.
854+
*/
855+
smithPredictor( customProcessModel modelCustom,
856+
ITransportDelay& mDelay,
857+
const real_t initialCondition = 0.0_re )
858+
: modelAlternate(modelCustom), modelDelay(&mDelay), yp_hat(initialCondition) {}
859+
838860
/**
839861
* @brief Constructs a Smith Predictor with a plant model, delay model,
840862
* and optional initial output estimate.
@@ -855,6 +877,27 @@ namespace qlibs {
855877
const real_t initialCondition = 0.0_re )
856878
: model(&modelTf), modelDelay(&mDelay), filter(&filterTf), yp_hat(initialCondition) {}
857879

880+
/**
881+
* @brief Constructs a Smith Predictor with a plant model, delay model,
882+
* and optional initial output estimate.
883+
* @param[in] modelCustom Reference to the system model representing the
884+
* delay-free plant. User should define a custom function that recreates
885+
* the system dynamics.
886+
* @param[in] mDelay Reference to the transport delay block modeling the
887+
* plant’s dead time.
888+
* @param[in] filterTf Reference to the LTI system used as the robustness
889+
* filter.
890+
* @param[in] initialCondition Initial value for the internal output
891+
* prediction (@c yp_hat). Default is 0.0.
892+
*
893+
* @note Both the model and delay block are passed by reference and stored internally as pointers.
894+
*/
895+
smithPredictor( customProcessModel modelCustom,
896+
ITransportDelay& mDelay,
897+
ltisys& filterTf,
898+
const real_t initialCondition = 0.0_re )
899+
: modelAlternate(modelCustom), modelDelay(&mDelay), filter(&filterTf), yp_hat(initialCondition) {}
900+
858901
/**
859902
* @brief Updates the internal prediction based on control input and
860903
* measured plant output.
@@ -895,8 +938,27 @@ namespace qlibs {
895938
* and does not affect the plant or delay block directly.
896939
*/
897940
bool setFilter( ltisys& filterTf ) noexcept {
898-
filter = &filterTf;
899-
return true;
941+
bool retValue = filter != &filterTf;
942+
if ( retValue ) {
943+
filter = &filterTf;
944+
}
945+
return retValue;
946+
}
947+
948+
/**
949+
* @brief Sets the model data when alternate when a custom delay-free
950+
* plant model is used
951+
*
952+
* @param[in] data The data passed to the user-defined model at the moment
953+
* of evaluation
954+
* @return @c true on success. False otherwise
955+
*/
956+
bool setModelData( void* data ) noexcept {
957+
bool retValue = data != alternateData;
958+
if ( retValue ) {
959+
alternateData = data;
960+
}
961+
return retValue;
900962
}
901963
};
902964

src/include/pid.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,9 @@ namespace qlibs {
162162
*/
163163
class pidController : public pidGains, public nState, private nonCopyable {
164164
private:
165-
real_t b, c, sat_Min, sat_Max, epsilon, kw, kt, D, u1, beta, uSat;
165+
real_t b, c, sat_Min, sat_Max, epsilon, kw, kt, D, u1, beta, uSat, gainBlend;
166166
real_t dt{ 1.0_re };
167+
pidGains nextGains;
167168
real_t m, mInput;
168169
const real_t *yr{ nullptr };
169170
real_t alpha, gamma; /*MRAC additive controller parameters*/
@@ -419,11 +420,13 @@ namespace qlibs {
419420
* @param[in] Mu Algorithm momentum. [ 0 <= Mu <= 1 ].
420421
* @param[in] Alpha Final controller speed adjustment. [ 0 < Alpha <= 1 ].
421422
* @param[in] lambda Algorithm forgetting factor [ 0.8 <= lambda <= 1 ].
423+
* @param[in] Tb Gains blend-time[ Tb > 0 ].
422424
* @return @c true on success, @c false on failure.
423425
*/
424426
bool setAutoTuningParameters( const real_t Mu,
425427
const real_t Alpha,
426-
const real_t lambda ) noexcept;
428+
const real_t lambda,
429+
const real_t Tb = 3.0_re ) noexcept;
427430

428431
/**
429432
* @brief Select the PID type to tune.

src/ltisys.cpp

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,26 @@ bool continuousSystem::setIntegrationMethod( integrationMethod m )
280280
bool smithPredictor::updatePrediction( const real_t ut,
281281
const real_t yt ) noexcept
282282
{
283-
const real_t yt_hat_d = model->excite( ut );
284-
const real_t yt_hat = modelDelay->delay( yt_hat_d );
285-
const real_t ep_hat = yt - yt_hat;
283+
bool retValue = true;
284+
real_t yt_hat_d = 0.0_re;
286285

287-
yp_hat = yt_hat_d + ( ( nullptr != filter ) ? filter->excite( ep_hat ) : ep_hat );
288-
return true;
286+
if ( nullptr != model ) {
287+
yt_hat_d = model->excite( ut );
288+
}
289+
else if ( nullptr != modelAlternate ) {
290+
yt_hat_d = modelAlternate( ut, alternateData );
291+
}
292+
else {
293+
retValue = false;
294+
}
295+
296+
if ( retValue ) {
297+
const real_t yt_hat = modelDelay->delay( yt_hat_d );
298+
const real_t ep_hat = yt - yt_hat;
299+
300+
yp_hat = yt_hat_d + ( ( nullptr != filter ) ? filter->excite( ep_hat )
301+
: ep_hat );
302+
}
303+
return retValue;
289304
}
290305
/*============================================================================*/

0 commit comments

Comments
 (0)