Browse Source

Rate of Temperature Change calculation. Fixes #78

Neal Wilson 11 years ago
parent
commit
1fa4bb5b35
2 changed files with 159 additions and 2 deletions
  1. 154
    0
      src/rate.w
  2. 5
    2
      src/typica.w

+ 154
- 0
src/rate.w View File

@@ -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
+

+ 5
- 2
src/typica.w View File

@@ -834,6 +834,7 @@ generated file empty.
834 834
 @<LinearSplineInterpolationConfWidget implementation@>@/
835 835
 @<TranslationConfWidget implementation@>@/
836 836
 @<FreeAnnotationConfWidget implementation@>@/
837
+@<RateOfChange implementation@>@/
837 838
 
838 839
 @ A few headers are required for various parts of \pn{}. These allow the use of
839 840
 various Qt modules.
@@ -6743,7 +6744,7 @@ QScriptValue FakeDAQ_newChannel(QScriptContext *context, QScriptEngine *engine)
6743 6744
 @* The Channel class.
6744 6745
 
6745 6746
 \noindent |Channel| is a simple class. It is a subclass of |QObject| so it can
6746
-use Qt's signals and slots mechanism. Any object that is interested in
6747
+use Qt'@q'@>s signals and slots mechanism. Any object that is interested in
6747 6748
 measurements from a channel can connect to the |newData| signal the channel
6748 6749
 emits. Any number of objects can make this connection and each will receive a
6749 6750
 copy of the measurement.
@@ -7553,7 +7554,7 @@ constructor = engine->newFunction(constructThresholdDetector);
7553 7554
 value = engine->newQMetaObject(&ThresholdDetector::staticMetaObject, constructor);
7554 7555
 engine->globalObject().setProperty("ThresholdDetector", value);
7555 7556
 
7556
-@ Implementation. At present I'm not bothering to implement constructor
7557
+@ Implementation. At present I'@q'@>m not bothering to implement constructor
7557 7558
 arguments here and am aligning on a fixed point. Another slot method was added
7558 7559
 to restore adjustability.
7559 7560
 
@@ -18232,6 +18233,8 @@ void TranslationConfWidget::updateMatchingColumn(const QString &column)
18232 18233
 @<Register device configuration widgets@>=
18233 18234
 app.registerDeviceConfigurationWidget("translation", TranslationConfWidget::staticMetaObject);
18234 18235
 
18236
+@i rate.w
18237
+
18235 18238
 @** Local changes.
18236 18239
 
18237 18240
 \noindent This is the end of \pn{} as distributed by its author. It is expected

Loading…
Cancel
Save