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.

rate.w 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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. };
  30. @ The interesting part of this class is in the |newMeasurement()| method. This
  31. is a slot method that will be called for every new measurement in the primary
  32. series. We require at least two measurements before calculating a rate of
  33. temperature change.
  34. @<RateOfChange implementation@>=
  35. void RateOfChange::newMeasurement(Measurement measure)
  36. {
  37. cache.append(measure);
  38. @<Remove stale measurements from rate cache@>@;
  39. if(cache.size() >= 2)
  40. {
  41. @<Calculate rate of change@>@;
  42. }
  43. }
  44. @ To calculate the rate of temperature change we require at least two cached
  45. measurements. Using only the most recent two measurements will result in a
  46. highly volatile rate of change while using two data points that are more
  47. separated will smooth out random fluctuations but provide a less immediate
  48. response to a change in the rate of change. For this reason we provide two
  49. parameters that can be adjusted independently: the amount of time we allow a
  50. measurement to stay in the cache determines how far apart the measurements
  51. used to calculate the rate of change are while a separate scale time is used
  52. to determine how the calculated value is presented. We never allow fewer than
  53. two cached values, but we can force the most volatile calculation by setting
  54. the cache time to 0 seconds.
  55. Measurement handling is a little bit different on a date transition.
  56. @<Remove stale measurements from rate cache@>=
  57. if(cache.size() > 2)
  58. {
  59. bool done = false;
  60. while(!done)
  61. {
  62. if(cache.front().time().secsTo(cache.back().time()) > ct)
  63. {
  64. cache.removeFirst();
  65. }
  66. else if(cache.back().time() < cache.front().time())
  67. {
  68. cache.removeFirst();
  69. done = true;
  70. }
  71. else
  72. {
  73. done = true;
  74. }
  75. if(cache.size() < 3)
  76. {
  77. done = true;
  78. }
  79. }
  80. }
  81. @ Rather than work directly with changes from one measurement to the next and
  82. attempting to filter out the noise, we instead calculate the slope of a simple
  83. linear regression on the current window of data.
  84. The measurement will carry the fact that it is a relative measurement.
  85. @<Calculate rate of change@>=
  86. int N = cache.size();
  87. double SXY = 0;
  88. double SX = 0;
  89. double SXX = 0;
  90. double SY = 0;
  91. double y;
  92. double x;
  93. for(int i = 0; i < N; i++)
  94. {
  95. y = cache.at(i).temperature();
  96. SY += y;
  97. x = cache.at(0).time().msecsTo(cache.at(i).time()) / 1000.0;
  98. SX += x;
  99. SXX += (x*x);
  100. SXY += (x*y);
  101. }
  102. double M = ((N * SXY) - (SX * SY)) / ((N * SXX) - (SX * SX));
  103. Measurement value(M * st, cache.back().time(), cache.back().scale());
  104. value.insert("relative", true);
  105. emit newData(value);
  106. @ The rest of the class implementation is trivial.
  107. @<RateOfChange implementation@>=
  108. RateOfChange::RateOfChange(int cachetime, int scaletime) : ct(cachetime), st(1)
  109. {
  110. setScaleTime(scaletime);
  111. }
  112. void RateOfChange::setCacheTime(int seconds)
  113. {
  114. ct = seconds;
  115. }
  116. void RateOfChange::setScaleTime(int seconds)
  117. {
  118. st = (seconds > 0 ? seconds : 1);
  119. }
  120. @ This is exposed to the host environment in the usual way.
  121. @<Function prototypes for scripting@>=
  122. QScriptValue constructRateOfChange(QScriptContext *context, QScriptEngine *engine);
  123. void setRateOfChangeProperties(QScriptValue value, QScriptEngine *engine);
  124. @ The constructor is registered with the scripting engine.
  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. @ The constructor takes two arguments if they are provided.
  130. @<Functions for scripting@>=
  131. QScriptValue constructRateOfChange(QScriptContext *context, QScriptEngine *engine)
  132. {
  133. int cachetime = 1;
  134. int scaletime = 1;
  135. if(context->argumentCount() > 0)
  136. {
  137. cachetime = argument<int>(0, context);
  138. if(context->argumentCount() > 1)
  139. {
  140. scaletime = argument<int>(1, context);
  141. }
  142. }
  143. QScriptValue object = engine->newQObject(new RateOfChange(cachetime, scaletime));
  144. setRateOfChangeProperties(object, engine);
  145. return object;
  146. }
  147. void setRateOfChangeProperties(QScriptValue value, QScriptEngine *engine)
  148. {
  149. setQObjectProperties(value, engine);
  150. }
  151. @ To make use of this feature conveniently, we must integrate this with the
  152. in-program configuration system by providing a configuration widget.
  153. @<Class declarations@>=
  154. class RateOfChangeConfWidget : public BasicDeviceConfigurationWidget
  155. {
  156. Q_OBJECT
  157. public:
  158. Q_INVOKABLE RateOfChangeConfWidget(DeviceTreeModel *model, const QModelIndex &index);
  159. private slots:
  160. void updateColumn(const QString &column);
  161. void updateCacheTime(const QString &seconds);
  162. void updateScaleTime(const QString &seconds);
  163. };
  164. @ The constructor sets up the user interface.
  165. @<RateOfChangeConfWidget implementation@>=
  166. RateOfChangeConfWidget::RateOfChangeConfWidget(DeviceTreeModel *model, const QModelIndex &index)
  167. : BasicDeviceConfigurationWidget(model, index)
  168. {
  169. QFormLayout *layout = new QFormLayout;
  170. QLineEdit *column = new QLineEdit;
  171. layout->addRow(tr("Primary series column name:"), column);
  172. QSpinBox *cacheTime = new QSpinBox;
  173. cacheTime->setMinimum(0);
  174. cacheTime->setMaximum(300);
  175. layout->addRow(tr("Cache time:"), cacheTime);
  176. QSpinBox *scaleTime = new QSpinBox;
  177. scaleTime->setMinimum(1);
  178. scaleTime->setMaximum(300);
  179. layout->addRow(tr("Scale time:"), scaleTime);
  180. @<Get device configuration data for current node@>@;
  181. for(int i = 0; i < configData.size(); i++)
  182. {
  183. node = configData.at(i).toElement();
  184. if(node.attribute("name") == "column")
  185. {
  186. column->setText(node.attribute("value"));
  187. }
  188. else if(node.attribute("name") == "cache")
  189. {
  190. cacheTime->setValue(node.attribute("value").toInt());
  191. }
  192. else if(node.attribute("name") == "scale")
  193. {
  194. scaleTime->setValue(node.attribute("value").toInt());
  195. }
  196. }
  197. updateColumn(column->text());
  198. updateCacheTime(cacheTime->text());
  199. updateScaleTime(scaleTime->text());
  200. connect(column, SIGNAL(textEdited(QString)), this, SLOT(updateColumn(QString)));
  201. connect(cacheTime, SIGNAL(valueChanged(QString)), this, SLOT(updateCacheTime(QString)));
  202. connect(scaleTime, SIGNAL(valueChanged(QString)), this, SLOT(updateScaleTime(QString)));
  203. setLayout(layout);
  204. }
  205. @ A set of update methods modify the device configuration to reflect changes in
  206. the configuration widget.
  207. @<RateOfChangeConfWidget implementation@>=
  208. void RateOfChangeConfWidget::updateColumn(const QString &column)
  209. {
  210. updateAttribute("column", column);
  211. }
  212. void RateOfChangeConfWidget::updateCacheTime(const QString &seconds)
  213. {
  214. updateAttribute("cache", seconds);
  215. }
  216. void RateOfChangeConfWidget::updateScaleTime(const QString &seconds)
  217. {
  218. updateAttribute("scale", seconds);
  219. }
  220. @ This is registered with the configuration system.
  221. @<Register device configuration widgets@>=
  222. app.registerDeviceConfigurationWidget("rate", RateOfChangeConfWidget::staticMetaObject);
  223. @ This is accessed through the advanced features menu.
  224. @<Add node inserters to advanced features menu@>=
  225. NodeInserter *rateOfChangeInserter = new NodeInserter(tr("Rate of Change"), tr("Rate of Change"), "rate");
  226. connect(rateOfChangeInserter, SIGNAL(triggered(QString, QString)), this, SLOT(insertChildNode(QString, QString)));
  227. advancedMenu->addAction(rateOfChangeInserter);
  228. @ Our class implementation is currently expanded into |"typica.cpp"|.
  229. @<Class implementations@>=
  230. @<RateOfChangeConfWidget implementation@>