0.3.0 PCR

This commit is contained in:
Rob Tillaart 2024-06-01 13:47:06 +02:00
parent 62230e22d8
commit 4794e6adaf
13 changed files with 266 additions and 140 deletions

View File

@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.3.0] - 2024-06-01
- breaking change
- duration in configuration function to **float seconds** as this is more convenient.
- update examples to use seconds.
- fix **getHoldTemp()** bug.
- fix signature of functions in 0.2.1
- clean up code (remove commented sections).
- add **PCR_demo_aquarium.ino** example (for fun).
- update readme.md
- update unit tests
----
## [0.2.1] - 2024-05-31
- add **setHeatPulseLength(uint16_t len)** to modify the pulse length of the heater / cooler.
- add example **PCR_demo_DS18B20.ino** DS18B20 for PCR.
@ -16,11 +29,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- update readme.md
- minor edits
## [0.2.0] - 2024-05-30
- initial class version (published)
----
## [0.1.3] - 2024-05-30
- initial class version (not published)

View File

@ -3,7 +3,7 @@
// FILE: PCR.h
// AUTHOR: Rob Tillaart
// DATE: 2015-06-10
// VERSION: 0.2.1
// VERSION: 0.3.0
// PURPOSE: Arduino library for PCR process control.
// URL: https://github.com/RobTillaart/PCR
// https://forum.arduino.cc/t/problem-with-arduino-pcr-amplifies-of-dna/314808
@ -12,7 +12,7 @@
#include "Arduino.h"
#define PCR_LIB_VERSION (F("0.2.1"))
#define PCR_LIB_VERSION (F("0.3.0"))
enum PCRSTATE {
PCR_STATE_IDLE = 0,
@ -40,52 +40,52 @@ public:
// PARAMETERS
void setInitial(float Celsius, uint32_t ms)
void setInitial(float Celsius, float seconds)
{
_initialTemp = Celsius;
_initialTime = ms;
_initialTime = seconds * 1000;
}
float getInitialTemp() { return _initialTemp; }
uint32_t getInitialTime() { return _initialTime; }
float getInitialTime() { return _initialTime * 0.001; }
void setDenature(float Celsius, uint32_t ms)
void setDenature(float Celsius, float seconds)
{
_denatureTemp = Celsius;
_denatureTime = ms;
_denatureTime = seconds * 1000;
}
float getDenatureTemp() { return _denatureTemp; }
uint32_t getDenatureTime() { return _denatureTime; }
float getDenatureTime() { return _denatureTime * 0.001; }
void setAnnealing(float Celsius, uint32_t ms)
void setAnnealing(float Celsius, float seconds)
{
_annealingTemp = Celsius;
_annealingTime = ms;
_annealingTime = seconds * 1000;
}
float getAnnealingTemp() { return _annealingTemp; }
uint32_t getAnnealingTime() { return _annealingTime; }
float getAnnealingTime() { return _annealingTime * 0.001; }
void setExtension(float Celsius, uint32_t ms)
void setExtension(float Celsius, float seconds)
{
_extensionTemp = Celsius;
_extensionTime = ms;
_extensionTime = seconds * 1000;
}
float getExtensionTemp() { return _extensionTemp; }
float getExtensionTime() { return _extensionTime; }
float getExtensionTime() { return _extensionTime * 0.001; }
void setElongation(float Celsius, uint32_t ms)
void setElongation(float Celsius, float seconds)
{
_elongationTemp = Celsius;
_elongationTime = ms;
_elongationTime = seconds * 1000;
}
float getElongationTemp() { return _elongationTemp; }
float getElongationTime() { return _elongationTime; }
float getElongationTime() { return _elongationTime * 0.001; }
void setHold(float Celsius) { _holdTemp = Celsius; }
float getHoldTemp() { return _extensionTemp; }
float getHoldTemp() { return _holdTemp; }
// PROCESS CONTROL
void reset(int iterations)
void reset(uint16_t iterations)
{
_startTime = millis();
_cycles = iterations;
@ -94,7 +94,7 @@ public:
debug();
}
int iterationsLeft()
uint16_t iterationsLeft()
{
return _cycles;
}
@ -104,11 +104,6 @@ public:
uint8_t process(float temperature)
{
_temperature = temperature;
/* 0.3.0
if (_temperature < temp[_state]) heat();
else if (_temperature > temp[_state]) cool();
else off();
*/
switch(_state)
{
case PCR_STATE_IDLE:
@ -190,18 +185,19 @@ public:
// HEATER / COOLER CONTROL
// ms = timing in milliseconds
void setHeatPulseLength(uint16_t ms = 10)
{
if (ms > 1000) ms = 1000;
_heatPulseLength = ms;
}
// returns milliseconds.
uint16_t getHeatPulseLength()
{
return _heatPulseLength;
}
void heat()
{
digitalWrite(_heatPin, HIGH);
@ -222,26 +218,9 @@ public:
digitalWrite(_coolPin, LOW);
}
// blocking version of single step.
// to be tested what to do with it
// could be a separate class / stand alone version.
/*
void keepTempTime(float temperature, uint32_t ms, float (*getTemp)())
{
_startTime = millis();
_temperature = temperature;
while (millis() - _startTime < ms)
{
if (getTemp() < _temperature ) heat();
else if (getTemp() > _temperature) cool();
else off();
}
}
*/
// estimator timeLeft, assumes process is not stopped.
uint32_t timeLeft()
// returns value in seconds
float timeLeft()
{
uint32_t sum = 0;
if (_state < PCR_STATE_DENATURE) sum += _initialTime;
@ -249,7 +228,7 @@ public:
sum += _annealingTime * _cycles;
sum += _extensionTime * _cycles;
if (_state <= PCR_STATE_ELONGATION) sum += _elongationTime;
return sum;
return sum * 0.001;
}
@ -257,12 +236,6 @@ protected:
// development.
void debug()
{
// log for plotting temperature
//
// Serial.print(_cycles);
// Serial.print("\t");
// Serial.println(_temperature);
// log for seeing state transitions.
Serial.print(_startTime);
Serial.print("\t");
@ -278,16 +251,8 @@ protected:
else if (_state == PCR_STATE_HOLD) Serial.println("\tHold");
}
/*
// simplify the code. 0.3.0
// maybe temperature only?
float _temp[6] = { 94, 94, 54, 76, 76, 14 };
float _time[6] = { 0, 1, 1, 1, 1, -1 };
*/
float _initialTemp = 94;
uint32_t _initialTime = 0;
float _initialTemp = 94; // °Celsius
uint32_t _initialTime = 0; // milliseconds
float _denatureTemp = 94;
uint32_t _denatureTime = 1000;
float _annealingTemp = 54;
@ -303,7 +268,7 @@ protected:
int _heatPin = 0;
int _coolPin = 0;
PCRSTATE _state = PCR_STATE_IDLE;
int _cycles = 0;
uint16_t _cycles = 0;
uint32_t _startTime = 0;
uint16_t _heatPulseLength = 10; // milliseconds.
};

View File

@ -19,7 +19,7 @@ Arduino library for PCR process control.
**Experimental**
From wikipedia:
From Wikipedia:
_The polymerase chain reaction (PCR) is a method widely used to make millions to
billions of copies of a specific DNA sample rapidly, allowing scientists to amplify
@ -103,10 +103,21 @@ Some examples:
- control an ice making machine.
#### Breaking change 0.3.0
Since 0.3.0 the timing of the 6 steps is done in seconds instead of milliseconds.
As the steps take up to 15 minutes of more, defining the time in seconds is a more
natural order of magnitude than milliseconds.
Note however that the internal math still is done in milliseconds so one can define
a step as taking 15.75 seconds = 15750 milliseconds.
Pre 0.3.0 versions are now obsolete.
#### Related
- https://en.wikipedia.org/wiki/Polymerase_chain_reaction
- https://github.com/RobTillaart/PCR
- https://github.com/RobTillaart/Temperature scale conversions.
- https://forum.arduino.cc/t/problem-with-arduino-pcr-amplifies-of-dna/314808
- https://www.scientificamerican.com/article/the-unusual-origin-of-the-polymeras/ (paid site)
@ -121,15 +132,17 @@ Some examples:
- **PCR(uint8_t heatPin, uint8_t coolPin)** constructor defines the hardware pins to which
the heater and cooler are connected.
- **void reset(int iterations)** full stop of the process, also stops heating and cooling,
- **void reset(uint16_t iterations)** full stop of the process, also stops heating and cooling,
resets the state to IDLE and defines the number of iterations for the next run.
The parameter iterations must be >= 0 so it changed to unsigned int in 0.3.0.
- **uint8_t process(float temperature)** The worker core. This function runs the main process
and iterates over the DENATURE, ANNEALING and EXTENSION phase. Returns the current state.
The user **MUST** provide the actual temperature of the sample so process can heat and cool
the sample on a need to basis.
The user **MUST** call this function as often as possible in a tight loop.
- **int iterationsLeft()** returns the number of iterations left.
- **uint32_t timeLeft()** estimator of the time left to reach the HOLD state.
- **float timeLeft()** estimator of the time left to reach the HOLD state.
Since 0.3.0 returns its value in seconds.
This function assumes that the duration per phase does not change runtime,
however it will adapt its estimate.
Returns the value in milliseconds.
@ -137,29 +150,36 @@ Returns the value in milliseconds.
#### About phases
Temperatures are in °Celsius, timing is in milliseconds (for 0.2.x version).
Temperatures are in **°Celsius**, timing is in **seconds** (since 0.3.0 version).
The timing is the time that the process will be in this state, so it includes
the time to heat / cool to reach the temperature defined.
The timing parameter is a float so you can use e.g. 10.5 seconds, or even use
scientific notation like 1.2e2.
In theory the maximum time is 4294967 seconds which is 49.7 days,
In practice the phases are much shorter.
- 1 hour = 3600 seconds, 1 day = 86400 seconds
Note that the parameters of the phases can change while the process is running,
e.g. one can increase the duration of the extension phase per cycle to give
that part of the PCR process more time.
that part of the PCR process more time (adjust to concentration?).
#### 1 Initial phase
This step used in **hot-start PCR** (Wikipedia) to bring the system to starting temperature.
- **void setInitial(float Celsius, uint32_t ms)** Sets temperature and duration.
- **void setInitial(float Celsius, float seconds)** Sets temperature and duration.
- **float getInitialTemp()** returns set value.
- **uint32_t getInitialTime()** returns set value.
- **float getInitialTime()** returns set value.
#### 2 Denature phase
This step breaks the double DNA helix into two single strands.
- **void setDenature(float Celsius, uint32_t ms)** Sets temperature and duration.
- **void setDenature(float Celsius, float seconds)** Sets temperature and duration.
- **float getDenatureTemp()** returns set value.
- **uint32_t getDenatureTime()** returns set value.
- **float getDenatureTime()** returns set value.
#### 3 Annealing phase
@ -167,16 +187,16 @@ This step let **primers** (Wikipedia) connect to the single strands.
The primers create a starting point for the replication.
The temperature and duration depends on many factors, so very specific for the reaction.
- **void setAnnealing(float Celsius, uint32_t ms)** Sets temperature and duration.
- **void setAnnealing(float Celsius, float seconds)** Sets temperature and duration.
- **float getAnnealingTemp()** returns set value.
- **uint32_t getAnnealingTime()** returns set value.
- **float getAnnealingTime()** returns set value.
#### 4 Extension phase
This step extends the primers with **dNTP's** nucleotides (Wikipedia) to complete
the duplication process.
- **void setExtension(float Celsius, uint32_t ms)** Sets temperature and duration.
- **void setExtension(float Celsius, float seconds)** Sets temperature and duration.
- **float getExtensionTemp()** returns set value.
- **float getExtensionTime()** returns set value.
@ -185,7 +205,7 @@ the duplication process.
This step is used to finalize the remaining DNA strands that are not fully extended
in step 4 Extension phase.
- **void setElongation(float Celsius, uint32_t ms)** Sets temperature and duration.
- **void setElongation(float Celsius, float seconds)** Sets temperature and duration.
- **float getElongationTemp()** returns set value.
- **float getElongationTime()** returns set value.
@ -204,8 +224,9 @@ from their own code.
In 0.2.x version the heater / cooler are switched on/off for a short period.
This prevent excessive heating or cooling due to not switching of the heater / cooler in time.
This pulsed heating cooling makes the process safer and a bit slower to heat up / cool down.
The length of the period can be adjusted between 0 and 1000 milliseconds to increase
This pulsed heating / cooling makes the process safer as after the call it is switched off.
Drawback is that the pulsed behaviour makes the process a bit slower to heat up / cool down.
Therefore the length of the period can be adjusted between 0 and 1000 milliseconds to increase
the efficiency of the process. Be aware that the heat() and cool() will block longer.
- **void heat()** Switches off cooler first, and then switches the heater for (default)
@ -213,9 +234,10 @@ the efficiency of the process. Be aware that the heat() and cool() will block lo
- **void cool()** switch on the cooler for (default) 10 milliseconds. Switches off heater first.
- **void off()** switch off both heater and cooler.
- **void setHeatPulseLength(uint16_t ms = 10)** adjust the timing for heat() and cool().
- The maximum value is 1000 milliseconds == 1 second.
- The maximum value is 1000 milliseconds == 1 second (this limit is to prevent overheating).
- The minimum value is 0 milliseconds but it would slow down the heating / cooling.
- warning the heat() and cool() will block for the set period.
- Assumption: optimal time == time to increase 0.1°C. This depends on the specific heat.
- Warning: the heat() and cool() will block for the set period.
- **uint16_t getHeatPulseLength()** returns set value.
@ -235,8 +257,6 @@ Users can patch this function when needed, or make it empty.
#### Should
- time of phases should be in seconds ==> breaking change
- **void setAnnealing(float Celsius, float seconds)** Sets temperature and duration.
- investigate the blocking version
- void keepTempTime(temp, time, getTemperature());
@ -244,17 +264,17 @@ Users can patch this function when needed, or make it empty.
- PCR scripting language, simple example?
- add examples
- optimize code
- have an array of times and temperatures to go through.
- add continuous heating (unsafe mode) versus the current pulsed heating(safe mode).
- add stir pin, to control the stirring of the PCR device.
- add signalling pin to indicate ready by a buzzer.
- add unit tests
#### Wont
- add callback function when ready (user can check state)
- Fahrenheit interface (C = (F-32) x 5/9.0; F = C x 9/5.0 + 32;
- Kelvin or other temperature scale.
- optimize code
- have an array of times and temperatures to go through.
- add continuous heating (unsafe mode) versus the current pulsed heating(safe mode).
## Support

View File

@ -51,16 +51,16 @@ void setup()
sensor.begin();
sensor.setResolution(10); // 10 bit ==> 0.25 degrees precision
sensor.requestTemperatures();
delay(1000); // wait a second to get a first reading.
delay(1000); // wait a second to get a first measurement.
// configure PCR process
// adjust timing and temperature to your needs.
pcr.setInitial(98, 10000); // temp, ms
pcr.setDenature(94.5, 5000); // temp, ms
pcr.setAnnealing(54.2, 2000); // temp, ms
pcr.setExtension(75.0, 3000); // temp, ms
pcr.setElongation(75.0, 3000); // temp, ms
pcr.setInitial(98, 10); // temp, seconds
pcr.setDenature(94.5, 5); // temp, seconds
pcr.setAnnealing(54.2, 2); // temp, seconds
pcr.setExtension(75.0, 3); // temp, seconds
pcr.setElongation(75.0, 5); // temp, seconds
pcr.setHold(8.0); // temp only
pcr.reset(15); // iterations.

View File

@ -46,11 +46,11 @@ void setup()
// configure PCR process
// adjust timing and temperature to your needs.
pcr.setInitial(98, 10000); // temp, ms
pcr.setDenature(94.5, 5000); // temp, ms
pcr.setAnnealing(54.2, 2000); // temp, ms
pcr.setExtension(75.0, 3000); // temp, ms
pcr.setElongation(75.0, 3000); // temp, ms
pcr.setInitial(98, 10); // temp, seconds
pcr.setDenature(94.5, 5); // temp, seconds
pcr.setAnnealing(54.2, 2); // temp, seconds
pcr.setExtension(75.0, 3); // temp, seconds
pcr.setElongation(75.0, 3); // temp, seconds
pcr.setHold(8.0); // temp only
pcr.reset(15); // iterations.

View File

@ -0,0 +1,54 @@
//
// FILE: PCR_demo_aquarium.ino
// AUTHOR: Rob Tillaart
// PURPOSE: using PCR class to control temperature in a tropic aquarium
// URL: https://github.com/RobTillaart/PCR
//
// Warning: example takes a day to do one cycle. Adjust timing to see effect.
#include "PCR.h"
PCR pcr(8, 9); // heatPin, coolPin
float getTemperature()
{
return 30; // dummy, to be elaborated.
}
void setup()
{
Serial.begin(115200);
Serial.println();
Serial.println(__FILE__);
Serial.print("PCR_LIB_VERSION: ");
Serial.println(PCR_LIB_VERSION);
Serial.println();
// split 86400 seconds over three temperature levels
pcr.setDenature(32, 23200); // temp, seconds
pcr.setAnnealing(29, 43200); // temp, seconds
pcr.setExtension(27, 20000); // temp, seconds
// just one cycle a day.
pcr.reset(1);
}
void loop()
{
while (true)
{
float temp = getTemperature();
pcr.process(temp);
// break after a full cycle / day.
if (pcr.iterationsLeft() == 0) break;
}
// do next cycle / day.
pcr.reset(1);
}
// -- END OF FILE --

View File

@ -28,25 +28,25 @@ void setup()
Serial.println();
// configure all phases
pcr[0].setInitial(98, 10000); // temp, ms
pcr[0].setDenature(94.5, 5000); // temp, ms
pcr[0].setAnnealing(54.2, 2000); // temp, ms
pcr[0].setExtension(75.0, 3000); // temp, ms
pcr[0].setElongation(75.0, 3000); // temp, ms
pcr[0].setInitial(98, 10); // temp, seconds
pcr[0].setDenature(94.5, 5); // temp, seconds
pcr[0].setAnnealing(54.2, 2); // temp, seconds
pcr[0].setExtension(75.0, 3); // temp, seconds
pcr[0].setElongation(75.0, 3); // temp, seconds
pcr[0].setHold(8.0); // temp only
pcr[1].setInitial(97, 8000);
pcr[1].setDenature(94, 4000);
pcr[1].setAnnealing(54, 2000);
pcr[1].setExtension(70.0, 2000);
pcr[1].setElongation(70.0, 2000);
pcr[1].setInitial(97, 8);
pcr[1].setDenature(94, 4);
pcr[1].setAnnealing(54, 2);
pcr[1].setExtension(70.0, 2);
pcr[1].setElongation(70.0, 2);
pcr[1].setHold(8.0);
pcr[2].setInitial(96, 15000);
pcr[2].setDenature(93, 8000);
pcr[2].setAnnealing(53, 4000);
pcr[2].setExtension(75.0, 4000);
pcr[2].setElongation(75.0, 4000);
pcr[2].setInitial(96, 15);
pcr[2].setDenature(93, 8);
pcr[2].setAnnealing(53, 4);
pcr[2].setExtension(75.0, 4);
pcr[2].setElongation(75.0, 4);
pcr[2].setHold(8.0);
pcr[0].reset(15); // iterations.

View File

@ -25,9 +25,9 @@ void setup()
Serial.println(PCR_LIB_VERSION);
Serial.println();
pcr.setDenature(94.5, 1000); // temp, ms
pcr.setAnnealing(54.2, 1000); // temp, ms
pcr.setExtension(75.0, 1000); // temp, ms
pcr.setDenature(94.5, 1); // temp, seconds
pcr.setAnnealing(54.2, 2); // temp, seconds
pcr.setExtension(75.0, 3); // temp, seconds
pcr.reset(5); // iterations.

View File

@ -27,11 +27,11 @@ void setup()
Serial.println();
// configure all phases
pcr.setInitial(98, 10000); // temp, ms
pcr.setDenature(94.5, 5000); // temp, ms
pcr.setAnnealing(54.2, 2000); // temp, ms
pcr.setExtension(75.0, 3000); // temp, ms
pcr.setElongation(75.0, 3000); // temp, ms
pcr.setInitial(98, 10); // temp, seconds
pcr.setDenature(94.5, 5); // temp, seconds
pcr.setAnnealing(54.2, 2); // temp, seconds
pcr.setExtension(75.0, 3); // temp, seconds
pcr.setElongation(75.0, 5); // temp, seconds
pcr.setHold(8.0); // temp only
pcr.reset(15); // iterations.

View File

@ -27,15 +27,15 @@ void setup()
Serial.println();
// configure all phases
pcr.setInitial(98, 10000); // temp, ms
pcr.setDenature(94.5, 5000); // temp, ms
pcr.setAnnealing(54.2, 2000); // temp, ms
pcr.setExtension(75.0, 3000); // temp, ms
pcr.setElongation(75.0, 3000); // temp, ms
pcr.setInitial(98, 10); // temp, seconds
pcr.setDenature(94.5, 5); // temp, seconds
pcr.setAnnealing(54.2, 2); // temp, seconds
pcr.setExtension(75.0, 3); // temp, seconds
pcr.setElongation(75.0, 3); // temp, seconds
pcr.setHold(8.0); // temp only
pcr.reset(10); // iterations.
Serial.print("Estimated time (ms): ");
Serial.print("Estimated time (seconds): ");
Serial.println(pcr.timeLeft());
bool flagFive = false;
@ -48,10 +48,10 @@ void setup()
if ((pcr.iterationsLeft() == 5) && (flagFive == false))
{
flagFive = true;
pcr.setDenature(94.5, 7500); // temp, ms
pcr.setAnnealing(54.2, 4000); // temp, ms
pcr.setExtension(75.0, 5000); // temp, ms
Serial.print("Estimated time (ms): ");
pcr.setDenature(94.5, 7.5); // temp, seconds
pcr.setAnnealing(54.2, 4.25); // temp, seconds
pcr.setExtension(75.0, 5.75); // temp, seconds
Serial.print("Estimated time (seconds): ");
Serial.println(pcr.timeLeft());
}
}

View File

@ -15,7 +15,7 @@
"type": "git",
"url": "https://github.com/RobTillaart/PCR.git"
},
"version": "0.2.1",
"version": "0.3.0",
"license": "MIT",
"frameworks": "*",
"platforms": "*",

View File

@ -1,5 +1,5 @@
name=PCR
version=0.2.1
version=0.3.0
author=Rob Tillaart <rob.tillaart@gmail.com>
maintainer=Rob Tillaart <rob.tillaart@gmail.com>
sentence=Arduino library for PCR process control.

View File

@ -54,8 +54,82 @@ unittest(test_constructor_parameters)
{
PCR pcr(8,9);
assertEqual(1, 1);
// to elaborate configuration.
pcr.setInitial(94, 50);
assertEqualFloat(94, pcr.getInitialTemp(), 0.01);
assertEqualFloat(50, pcr.getInitialTime(), 0.01);
pcr.setDenature(93, 30);
assertEqualFloat(93, pcr.getDenatureTemp(), 0.01);
assertEqualFloat(30, pcr.getDenatureTime(), 0.01);
pcr.setAnnealing(54, 40);
assertEqualFloat(54, pcr.getAnnealingTemp(), 0.01);
assertEqualFloat(40, pcr.getAnnealingTime(), 0.01);
pcr.setExtension(75, 60);
assertEqualFloat(75, pcr.getExtensionTemp(), 0.01);
assertEqualFloat(60, pcr.getExtensionTime(), 0.01);
pcr.setElongation(74, 90);
assertEqualFloat(74, pcr.getElongationTemp(), 0.01);
assertEqualFloat(90, pcr.getElongationTime(), 0.01);
pcr.setHold(17);
assertEqualFloat(17, pcr.getHoldTemp(), 0.01);
}
unittest(test_constructor_iterations)
{
PCR pcr(8,9);
assertEqual(0, pcr.iterationsLeft());
for (int i = 1; i < 10; i++)
{
pcr.reset(i);
assertEqual(i, pcr.iterationsLeft());
}
}
unittest(test_constructor_heat_pulse_length)
{
PCR pcr(8,9);
assertEqual(10, pcr.getHeatPulseLength());
for (int i = 10; i <= 100; i+= 10)
{
pcr.setHeatPulseLength(i);
assertEqual(i, pcr.getHeatPulseLength());
}
// test constrain
pcr.setHeatPulseLength(5000);
assertEqual(1000, pcr.getHeatPulseLength());
pcr.setHeatPulseLength();
assertEqual(10, pcr.getHeatPulseLength());
}
unittest(test_constructor_timeLeft)
{
PCR pcr(8,9);
assertEqualFloat(1, pcr.timeLeft(), 0.01);
pcr.setInitial(94, 50);
pcr.setDenature(93, 30);
pcr.setAnnealing(54, 40);
pcr.setExtension(75, 60);
pcr.setElongation(74, 90);
pcr.setHold(17);
// note: zero cycles
assertEqualFloat(140, pcr.timeLeft(), 0.1);
pcr.reset(10);
assertEqualFloat(1440, pcr.timeLeft(), 0.1);
}