|
@@ -0,0 +1,154 @@
|
|
1
|
+@** A Rate of Change Indicator.
|
|
2
|
+
|
|
3
|
+\noindent A common metric used for understanding roast profiles is the rate of
|
|
4
|
+temperature change over a given amount of time. When roasters discuss roast
|
|
5
|
+profiles it is not uncommon to hear references to the change in temperature per
|
|
6
|
+30 seconds or per minute, often with the sloppy shorthand $\Delta$ or with the
|
|
7
|
+term Rate of Rise (RoR). This is most commonly calculated from the secant line
|
|
8
|
+defined by two measurement points at the desired separation, however this may
|
|
9
|
+not be the most useful way to calculate this value.
|
|
10
|
+
|
|
11
|
+The rate of change can be considered as its own data series which happens to be
|
|
12
|
+derived from a primary measurement series. The interface for producing this
|
|
13
|
+series can sensibly match other classes which store, forward, or manipulate
|
|
14
|
+measurement data.
|
|
15
|
+
|
|
16
|
+@<Class declarations@>=
|
|
17
|
+class RateOfChange : public QObject
|
|
18
|
+{
|
|
19
|
+ Q_OBJECT
|
|
20
|
+ public:
|
|
21
|
+ RateOfChange(int cachetime = 1, int scaletime = 1);
|
|
22
|
+ public slots:
|
|
23
|
+ void newMeasurement(Measurement measure);
|
|
24
|
+ void setCacheTime(int seconds);
|
|
25
|
+ void setScaleTime(int seconds);
|
|
26
|
+ signals:
|
|
27
|
+ void measurement(Measurement measure);
|
|
28
|
+ private:
|
|
29
|
+ int ct;
|
|
30
|
+ int st;
|
|
31
|
+ QList<Measurement> cache;
|
|
32
|
+};
|
|
33
|
+
|
|
34
|
+@ The interesting part of this class is in the |newMeasurement()| method. This
|
|
35
|
+is a slot method that will be called for every new measurement in the primary
|
|
36
|
+series. We require at least two measurements before calculating a rate of
|
|
37
|
+temperature change.
|
|
38
|
+
|
|
39
|
+@<RateOfChange implementation@>=
|
|
40
|
+void RateOfChange::newMeasurement(Measurement measure)
|
|
41
|
+{
|
|
42
|
+ cache.append(measure);
|
|
43
|
+ @<Remove stale measurements from rate cache@>@;
|
|
44
|
+ if(cache.size() >= 2)
|
|
45
|
+ {
|
|
46
|
+ @<Calculate rate of change@>@;
|
|
47
|
+ }
|
|
48
|
+}
|
|
49
|
+
|
|
50
|
+@ To calculate the rate of temperature change we require at least two cached
|
|
51
|
+measurements. Using only the most recent two measurements will result in a
|
|
52
|
+highly volatile rate of change while using two data points that are more
|
|
53
|
+separated will smooth out random fluctuations but provide a less immediate
|
|
54
|
+response to a change in the rate of change. For this reason we provide two
|
|
55
|
+parameters that can be adjusted independently: the amount of time we allow a
|
|
56
|
+measurement to stay in the cache determines how far apart the measurements
|
|
57
|
+used to calculate the rate of change are while a separate scale time is used
|
|
58
|
+to determine how the calculated value is presented. We never allow fewer than
|
|
59
|
+two cached values, but we can force the most volatile calculation by setting
|
|
60
|
+the cache time to 0 seconds.
|
|
61
|
+
|
|
62
|
+@<Remove stale measurements from rate cache@>=
|
|
63
|
+if(cache.size() > 2)
|
|
64
|
+{
|
|
65
|
+ bool done = false;
|
|
66
|
+ while(!done)
|
|
67
|
+ {
|
|
68
|
+ if(cache.front().time().secsTo(cache.back().time()) > ct)
|
|
69
|
+ {
|
|
70
|
+ cache.removeFirst();
|
|
71
|
+ }
|
|
72
|
+ else
|
|
73
|
+ {
|
|
74
|
+ done = true;
|
|
75
|
+ }
|
|
76
|
+ if(cache.size() < 3)
|
|
77
|
+ {
|
|
78
|
+ done = true;
|
|
79
|
+ }
|
|
80
|
+ }
|
|
81
|
+}
|
|
82
|
+
|
|
83
|
+@ To calculate the rate of change, we compare both the time and the
|
|
84
|
+temperature of the first and last measurements in the cache and reduce this to
|
|
85
|
+a change per second value. This is then multiplied by the scale time and sent
|
|
86
|
+to whatever objects require the derived series data. Note that |tdiff| will
|
|
87
|
+generally be very close to the cache time except where this is set to zero, but
|
|
88
|
+it will almost never be exact. Basing the calculation on the data we have
|
|
89
|
+instead of on the data we wish we had should result in better stability in the
|
|
90
|
+derived series.
|
|
91
|
+
|
|
92
|
+@<Calculate rate of change@>=
|
|
93
|
+double mdiff = cache.back().temperature() - cache.front().temperature();
|
|
94
|
+double tdiff = cache.front().time().msecsTo(cache.back().time()) / 1000.0;
|
|
95
|
+double dps = mdiff / tdiff;
|
|
96
|
+double scale = dps * st;
|
|
97
|
+emit measurement(Measurement(scale, cache.back().time(), cache.back().scale()));
|
|
98
|
+
|
|
99
|
+@ The rest of the class implementation is trivial.
|
|
100
|
+
|
|
101
|
+@<RateOfChange implementation@>=
|
|
102
|
+RateOfChange::RateOfChange(int cachetime, int scaletime) : ct(cachetime), st(1)
|
|
103
|
+{
|
|
104
|
+ setScaleTime(scaletime);
|
|
105
|
+}
|
|
106
|
+
|
|
107
|
+void RateOfChange::setCacheTime(int seconds)
|
|
108
|
+{
|
|
109
|
+ ct = seconds;
|
|
110
|
+}
|
|
111
|
+
|
|
112
|
+void RateOfChange::setScaleTime(int seconds)
|
|
113
|
+{
|
|
114
|
+ st = (seconds > 0 ? seconds : 1);
|
|
115
|
+}
|
|
116
|
+
|
|
117
|
+@ This is exposed to the host environment in the usual way.
|
|
118
|
+
|
|
119
|
+@<Function prototypes for scripting@>=
|
|
120
|
+QScriptValue constructRateOfChange(QScriptContext *context, QScriptEngine *engine);
|
|
121
|
+void setRateOfChangeProperties(QScriptValue value, QScriptEngine *engine);
|
|
122
|
+
|
|
123
|
+@ The constructor is registered with the scripting engine.
|
|
124
|
+
|
|
125
|
+@<Set up the scripting engine@>=
|
|
126
|
+constructor = engine->newFunction(constructRateOfChange);
|
|
127
|
+value = engine->newQMetaObject(&RateOfChange::staticMetaObject, constructor);
|
|
128
|
+engine->globalObject().setProperty("RateOfChange", value);
|
|
129
|
+
|
|
130
|
+@ The constructor takes two arguments if they are provided.
|
|
131
|
+
|
|
132
|
+@<Functions for scripting@>=
|
|
133
|
+QScriptValue constructRateOfChange(QScriptContext *context, QScriptEngine *engine)
|
|
134
|
+{
|
|
135
|
+ int cachetime = 1;
|
|
136
|
+ int scaletime = 1;
|
|
137
|
+ if(context->argumentCount() > 0)
|
|
138
|
+ {
|
|
139
|
+ cachetime = argument<int>(0, context);
|
|
140
|
+ if(context->argumentCount() > 1)
|
|
141
|
+ {
|
|
142
|
+ scaletime = argument<int>(1, context);
|
|
143
|
+ }
|
|
144
|
+ }
|
|
145
|
+ QScriptValue object = engine->newQObject(new RateOfChange(cachetime, scaletime));
|
|
146
|
+ setRateOfChangeProperties(object, engine);
|
|
147
|
+ return object;
|
|
148
|
+}
|
|
149
|
+
|
|
150
|
+void setRateOfChangeProperties(QScriptValue value, QScriptEngine *engine)
|
|
151
|
+{
|
|
152
|
+ setQObjectProperties(value, engine);
|
|
153
|
+}
|
|
154
|
+
|