Typica is a free program for professional coffee roasters. https://typica.us
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. @** A Rate of Change Indicator.
  2. \noindent A common metric used for understanding roast profiles is the rate of
  3. temperature change over a given amount of time. When roasters discuss roast
  4. profiles it is not uncommon to hear references to the change in temperature per
  5. 30 seconds or per minute, often with the sloppy shorthand $\Delta$ or with the
  6. term Rate of Rise (RoR). This is most commonly calculated from the secant line
  7. defined by two measurement points at the desired separation, however this may
  8. not be the most useful way to calculate this value.
  9. The rate of change can be considered as its own data series which happens to be
  10. derived from a primary measurement series. The interface for producing this
  11. series can sensibly match other classes which store, forward, or manipulate
  12. measurement data.
  13. @<Class declarations@>=
  14. class RateOfChange : public QObject
  15. {
  16. Q_OBJECT
  17. public:
  18. RateOfChange(int cachetime = 1, int scaletime = 1);
  19. public slots:
  20. void newMeasurement(Measurement measure);
  21. void setCacheTime(int seconds);
  22. void setScaleTime(int seconds);
  23. signals:
  24. void newData(Measurement measure);
  25. private:
  26. int ct;
  27. int st;
  28. QList<Measurement> cache;
  29. QMap<double,double> smoothCache;
  30. };
  31. @ The interesting part of this class is in the |newMeasurement()| method. This
  32. is a slot method that will be called for every new measurement in the primary
  33. series. We require at least two measurements before calculating a rate of
  34. temperature change.
  35. @<RateOfChange implementation@>=
  36. void RateOfChange::newMeasurement(Measurement measure)
  37. {
  38. cache.append(measure);
  39. @<Remove stale measurements from rate cache@>@;
  40. if(cache.size() >= 2)
  41. {
  42. @<Calculate rate of change@>@;
  43. }
  44. }
  45. @ To calculate the rate of temperature change we require at least two cached
  46. measurements. Using only the most recent two measurements will result in a
  47. highly volatile rate of change while using two data points that are more
  48. separated will smooth out random fluctuations but provide a less immediate
  49. response to a change in the rate of change. For this reason we provide two
  50. parameters that can be adjusted independently: the amount of time we allow a
  51. measurement to stay in the cache determines how far apart the measurements
  52. used to calculate the rate of change are while a separate scale time is used
  53. to determine how the calculated value is presented. We never allow fewer than
  54. two cached values, but we can force the most volatile calculation by setting
  55. the cache time to 0 seconds.
  56. Measurement handling is a little bit different on a date transition.
  57. @<Remove stale measurements from rate cache@>=
  58. if(cache.size() > 2)
  59. {
  60. bool done = false;
  61. while(!done)
  62. {
  63. if(cache.front().time().secsTo(cache.back().time()) > ct)
  64. {
  65. cache.removeFirst();
  66. }
  67. else if(cache.back().time() < cache.front().time())
  68. {
  69. cache.removeFirst();
  70. done = true;
  71. }
  72. else
  73. {
  74. done = true;
  75. }
  76. if(cache.size() < 3)
  77. {
  78. done = true;
  79. }
  80. }
  81. }
  82. @ The calculation method here is subject to change as this is still noisier
  83. than I would like. What we are doing here is calculating the rate of change
  84. between each pair of adjacent measurements in the cache and averaging them to
  85. produce something that is a little less noisy than just using the first and
  86. last measurements in the cache. Other techniques may be useful for reducing the
  87. noise further.
  88. The measurement will carry the fact that it is a relative measurement.
  89. @<Calculate rate of change@>=
  90. QList<double> rates;
  91. for(int i = 1; i < cache.size(); i++)
  92. {
  93. double mdiff = cache.at(i).temperature() - cache.at(i-1).temperature();
  94. double tdiff = (double)(cache.at(i-1).time().msecsTo(cache.at(i).time())) / 1000.0;
  95. rates.append(mdiff/tdiff);
  96. }
  97. double acc = 0.0;
  98. for(int i = 0; i < rates.size(); i++)
  99. {
  100. acc += rates.at(i);
  101. }
  102. double pavg = acc /= rates.size();
  103. double v2 = pavg * st;
  104. double refm = cache.back().temperature() - cache.front().temperature();
  105. double reft = (double)(cache.front().time().msecsTo(cache.back().time())) / 1000.0;
  106. double ref = refm/reft;
  107. Measurement value(v2, cache.back().time(), cache.back().scale());
  108. value.insert("relative", true);
  109. emit newData(value);
  110. double calcdiff = ref - pavg;
  111. if(calcdiff < 0)
  112. {
  113. calcdiff = -calcdiff;
  114. }
  115. if(pavg < 0)
  116. {
  117. pavg = -pavg;
  118. }
  119. if(calcdiff > (pavg * 0.2))
  120. {
  121. Measurement save = cache.back();
  122. cache.clear();
  123. cache.append(save);
  124. }
  125. @ The rest of the class implementation is trivial.
  126. @<RateOfChange implementation@>=
  127. RateOfChange::RateOfChange(int cachetime, int scaletime) : ct(cachetime), st(1)
  128. {
  129. setScaleTime(scaletime);
  130. }
  131. void RateOfChange::setCacheTime(int seconds)
  132. {
  133. ct = seconds;
  134. }
  135. void RateOfChange::setScaleTime(int seconds)
  136. {
  137. st = (seconds > 0 ? seconds : 1);
  138. }
  139. @ This is exposed to the host environment in the usual way.
  140. @<Function prototypes for scripting@>=
  141. QScriptValue constructRateOfChange(QScriptContext *context, QScriptEngine *engine);
  142. void setRateOfChangeProperties(QScriptValue value, QScriptEngine *engine);
  143. @ The constructor is registered with the scripting engine.
  144. @<Set up the scripting engine@>=
  145. constructor = engine->newFunction(constructRateOfChange);
  146. value = engine->newQMetaObject(&RateOfChange::staticMetaObject, constructor);
  147. engine->globalObject().setProperty("RateOfChange", value);
  148. @ The constructor takes two arguments if they are provided.
  149. @<Functions for scripting@>=
  150. QScriptValue constructRateOfChange(QScriptContext *context, QScriptEngine *engine)
  151. {
  152. int cachetime = 1;
  153. int scaletime = 1;
  154. if(context->argumentCount() > 0)
  155. {
  156. cachetime = argument<int>(0, context);
  157. if(context->argumentCount() > 1)
  158. {
  159. scaletime = argument<int>(1, context);
  160. }
  161. }
  162. QScriptValue object = engine->newQObject(new RateOfChange(cachetime, scaletime));
  163. setRateOfChangeProperties(object, engine);
  164. return object;
  165. }
  166. void setRateOfChangeProperties(QScriptValue value, QScriptEngine *engine)
  167. {
  168. setQObjectProperties(value, engine);
  169. }
  170. @ To make use of this feature conveniently, we must integrate this with the
  171. in-program configuration system by providing a configuration widget.
  172. @<Class declarations@>=
  173. class RateOfChangeConfWidget : public BasicDeviceConfigurationWidget
  174. {
  175. Q_OBJECT
  176. public:
  177. Q_INVOKABLE RateOfChangeConfWidget(DeviceTreeModel *model, const QModelIndex &index);
  178. private slots:
  179. void updateColumn(const QString &column);
  180. void updateCacheTime(const QString &seconds);
  181. void updateScaleTime(const QString &seconds);
  182. };
  183. @ The constructor sets up the user interface.
  184. @<RateOfChangeConfWidget implementation@>=
  185. RateOfChangeConfWidget::RateOfChangeConfWidget(DeviceTreeModel *model, const QModelIndex &index)
  186. : BasicDeviceConfigurationWidget(model, index)
  187. {
  188. QFormLayout *layout = new QFormLayout;
  189. QLineEdit *column = new QLineEdit;
  190. layout->addRow(tr("Primary series column name:"), column);
  191. QSpinBox *cacheTime = new QSpinBox;
  192. cacheTime->setMinimum(0);
  193. cacheTime->setMaximum(300);
  194. layout->addRow(tr("Cache time:"), cacheTime);
  195. QSpinBox *scaleTime = new QSpinBox;
  196. scaleTime->setMinimum(1);
  197. scaleTime->setMaximum(300);
  198. layout->addRow(tr("Scale time:"), scaleTime);
  199. @<Get device configuration data for current node@>@;
  200. for(int i = 0; i < configData.size(); i++)
  201. {
  202. node = configData.at(i).toElement();
  203. if(node.attribute("name") == "column")
  204. {
  205. column->setText(node.attribute("value"));
  206. }
  207. else if(node.attribute("name") == "cache")
  208. {
  209. cacheTime->setValue(node.attribute("value").toInt());
  210. }
  211. else if(node.attribute("name") == "scale")
  212. {
  213. scaleTime->setValue(node.attribute("value").toInt());
  214. }
  215. }
  216. updateColumn(column->text());
  217. updateCacheTime(cacheTime->text());
  218. updateScaleTime(scaleTime->text());
  219. connect(column, SIGNAL(textEdited(QString)), this, SLOT(updateColumn(QString)));
  220. connect(cacheTime, SIGNAL(valueChanged(QString)), this, SLOT(updateCacheTime(QString)));
  221. connect(scaleTime, SIGNAL(valueChanged(QString)), this, SLOT(updateScaleTime(QString)));
  222. setLayout(layout);
  223. }
  224. @ A set of update methods modify the device configuration to reflect changes in
  225. the configuration widget.
  226. @<RateOfChangeConfWidget implementation@>=
  227. void RateOfChangeConfWidget::updateColumn(const QString &column)
  228. {
  229. updateAttribute("column", column);
  230. }
  231. void RateOfChangeConfWidget::updateCacheTime(const QString &seconds)
  232. {
  233. updateAttribute("cache", seconds);
  234. }
  235. void RateOfChangeConfWidget::updateScaleTime(const QString &seconds)
  236. {
  237. updateAttribute("scale", seconds);
  238. }
  239. @ This is registered with the configuration system.
  240. @<Register device configuration widgets@>=
  241. app.registerDeviceConfigurationWidget("rate", RateOfChangeConfWidget::staticMetaObject);
  242. @ This is accessed through the advanced features menu.
  243. @<Add node inserters to advanced features menu@>=
  244. NodeInserter *rateOfChangeInserter = new NodeInserter(tr("Rate of Change"), tr("Rate of Change"), "rate");
  245. connect(rateOfChangeInserter, SIGNAL(triggered(QString, QString)), this, SLOT(insertChildNode(QString, QString)));
  246. advancedMenu->addAction(rateOfChangeInserter);
  247. @ Our class implementation is currently expanded into |"typica.cpp"|.
  248. @<Class implementations@>=
  249. @<RateOfChangeConfWidget implementation@>