diff --git a/phpunit.xml b/phpunit.xml index 2b57a08..acb7da4 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,6 +3,9 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" + displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnPhpunitDeprecations="true" > diff --git a/src/Tecan/AdvancedCommands/Mix.php b/src/Tecan/AdvancedCommands/Mix.php new file mode 100644 index 0000000..92382bf --- /dev/null +++ b/src/Tecan/AdvancedCommands/Mix.php @@ -0,0 +1,69 @@ +liquidClass = $liquidClass; + $this->volumes = $volumes; + $this->grid = $grid; + $this->site = $site; + $this->spacing = $spacing; + $this->wellSelection = $wellSelection->toString(); + $this->cycles = $cycles; + $this->arm = $arm; + } + + public function toString(): string + { + if ($this->noOfLoopOptions !== 0) { + throw new TecanException('Loop options are not yet supported'); + } + + $mixParameters = implode(',', [ + $this->volumes->tipMask(), + Str::encloseWithDoubleQuotes($this->liquidClass), + $this->volumes->volumeString(), + $this->grid, + $this->site, + $this->spacing, + Str::encloseWithDoubleQuotes($this->wellSelection), + $this->cycles, + $this->noOfLoopOptions, + $this->arm, + ]); + + return "Mix({$mixParameters})"; + } +} diff --git a/src/Tecan/AdvancedCommands/Str.php b/src/Tecan/AdvancedCommands/Str.php new file mode 100644 index 0000000..ba4f17f --- /dev/null +++ b/src/Tecan/AdvancedCommands/Str.php @@ -0,0 +1,12 @@ +volumes = $volumes; + } + + public function volumeString(): string + { + $volumesArray = array_map(fn ($volume): string => $volume === 0.0 || $volume === 0 + ? (string) $volume + : Str::encloseWithDoubleQuotes($volume), $this->volumes); + + return implode(',', $volumesArray); + } + + /** + * Generate tip bitmask from volumes. + * + * @return int Bitmask representing the tip mask + */ + public function tipMask() + { + $bitmask = 0; + foreach ($this->volumes as $index => $volume) { + if ($volume > 0) { + $bitmask |= (1 << $index); + } + } + + return $bitmask; + } +} diff --git a/src/Tecan/AdvancedCommands/WellSelection.php b/src/Tecan/AdvancedCommands/WellSelection.php new file mode 100644 index 0000000..a6b3c7f --- /dev/null +++ b/src/Tecan/AdvancedCommands/WellSelection.php @@ -0,0 +1,84 @@ +> */ + private array $selFlag; + + /** @param array $positions */ + public function __construct(int $xWells, int $yWells, array $positions) + { + $this->xWells = $xWells; + $this->yWells = $yWells; + $this->selFlag = WellSelection::transformPositionsToSelFlag($positions, $xWells, $yWells); + } + + public function toString(): string + { + if ($this->xWells === 0 || $this->yWells === 0) { + return '0000'; + } + $selString = sprintf('%02X%02X', $this->xWells, $this->yWells); + + $bitCounter = 0; + $bitMask = 0; + + $selFlag = $this->selFlag; + + for ($x = 0; $x < $this->xWells; ++$x) { + for ($y = 0; $y < $this->yWells; ++$y) { + if (isset($selFlag[$x][$y]) && ($selFlag[$x][$y] & 1) !== 0) { + $bitMask |= (1 << $bitCounter); + } + if (++$bitCounter > 6) { + $selString .= chr(ord('0') + $bitMask); + $bitCounter = 0; + $bitMask = 0; + } + } + } + + if ($bitCounter > 0) { + $selString .= chr(ord('0') + $bitMask); + } + + return $selString; + } + + /** + * @param array $positions + * + * @return array> + */ + public static function transformPositionsToSelFlag(array $positions, int $xWells, int $yWells): array + { + // Calculate the total number of wells + $totalWells = $xWells * $yWells; + + // Calculate the number of sub-arrays needed, each with 7 elements + $numSubArrays = (int) ceil($totalWells / self::MAX_ITEMS_PER_SUBARRAY); + + // Initialize the selFlag array with zeros + $selFlag = array_fill(0, $numSubArrays, array_fill(0, self::MAX_ITEMS_PER_SUBARRAY, 0)); + + // Iterate over the positions and set the corresponding selFlag positions to 1 + foreach ($positions as $position) { + $index = $position - 1; + $subArrayIndex = intdiv($index, self::MAX_ITEMS_PER_SUBARRAY); + $bitIndex = $index % self::MAX_ITEMS_PER_SUBARRAY; + if ($subArrayIndex < $numSubArrays) { + $selFlag[$subArrayIndex][$bitIndex] = 1; + } + } + + return $selFlag; + } +} diff --git a/tests/Tecan/AdvancedCommands/MixTest.php b/tests/Tecan/AdvancedCommands/MixTest.php new file mode 100644 index 0000000..5d29123 --- /dev/null +++ b/tests/Tecan/AdvancedCommands/MixTest.php @@ -0,0 +1,43 @@ +toString()); + } + + public function testMixCommandWithNoVolumes(): void + { + $mix = new Mix( + 'Water', + new Volumes([0, 0.0, 0.00000, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 15, + 2, + 1, + new WellSelection(12, 8, [1]), + 1, + ); + + $expected = 'Mix(0,"Water",0,0,0,0,0,0,0,0,0,0,0,0,15,2,1,"0C0810000000000000",1,0,0)'; + self::assertEquals($expected, $mix->toString()); + } +} diff --git a/tests/Tecan/AdvancedCommands/VolumesTest.php b/tests/Tecan/AdvancedCommands/VolumesTest.php new file mode 100644 index 0000000..dd8cff3 --- /dev/null +++ b/tests/Tecan/AdvancedCommands/VolumesTest.php @@ -0,0 +1,47 @@ +, 1: string, 2: int}> */ + public static function validVolumesProvider(): iterable + { + yield [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], '0,0,0,0,0,0,0,0,0,0,0,0', 0]; + yield [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], '"1",0,0,0,0,0,0,0,0,0,0,0', 1]; + yield [[1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], '"1","2",0,0,0,0,0,0,0,0,0,0', 3]; + yield [[1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], '"1",0,"2",0,0,0,0,0,0,0,0,0', 5]; + yield [[1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0], '"1",0,0,"2",0,0,0,0,0,0,0,0', 9]; + + yield [[1, 0, 2.5, 0, 0, 3, 0, 4, 0, 0, 0, 0], '"1",0,"2.5",0,0,"3",0,"4",0,0,0,0', 165]; + yield [[1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0], '"1","2","3","4","5","6","7","8",0,0,0,0', 255]; + } + + /** @dataProvider validVolumesProvider + * @param array{ + * 0:float|int, + * 1:float|int, + * 2:float|int, + * 3:float|int, + * 4:float|int, + * 5:float|int, + * 6:float|int, + * 7:float|int, + * 8:float|int, + * 9:float|int, + * 10:float|int, + * 11:float|int, + * } $volumes + */ + #[DataProvider('validVolumesProvider')] + public function testVolumesStringAndTipMask(array $volumes, string $expectedString, int $expectedMask): void + { + $volumesObj = new Volumes($volumes); + self::assertSame($expectedString, $volumesObj->volumeString()); + self::assertSame($expectedMask, $volumesObj->tipMask()); + } +} diff --git a/tests/Tecan/AdvancedCommands/WellSelectionTest.php b/tests/Tecan/AdvancedCommands/WellSelectionTest.php new file mode 100644 index 0000000..9c29e74 --- /dev/null +++ b/tests/Tecan/AdvancedCommands/WellSelectionTest.php @@ -0,0 +1,109 @@ +, 3: string}> */ + public static function wellSelectionProvider(): iterable + { + yield 'returns0000WhenXWellsIsZero' => [0, 5, [], '0000']; + yield 'returns0000WhenYWellsIsZero' => [5, 0, [], '0000']; + yield 'A1 pos 1' => [12, 8, [1], '0C0810000000000000']; + yield 'B1 pos 2' => [12, 8, [2], '0C0820000000000000']; + yield 'C1 pos 3' => [12, 8, [3], '0C0840000000000000']; + yield 'D1 pos 4' => [12, 8, [4], '0C0880000000000000']; + yield 'E1 pos 5' => [12, 8, [5], '0C08@0000000000000']; + yield 'F1 pos 6' => [12, 8, [6], '0C08P0000000000000']; + yield 'G1 pos 7' => [12, 8, [7], '0C08p0000000000000']; + yield 'A1 + E1 pos 1 + 5' => [12, 8, [1, 5], '0C08A0000000000000']; + yield 'no wells selected' => [12, 8, [], '0C0800000000000000']; + } + + /** + * @dataProvider wellSelectionProvider + * + * @param array $positions + */ + #[DataProvider('wellSelectionProvider')] + public function testWellSelection(int $xWells, int $yWells, array $positions, string $expected): void + { + $wellSelection = new WellSelection($xWells, $yWells, $positions); + self::assertEquals($expected, $wellSelection->toString()); + } + + /** @return iterable, 1: int, 2: int, 3: array>}> */ + public static function transformPositionsToSelFlagProvider(): iterable + { + yield [ + [1, 5], 12, 8, [ + [1, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + ], + ]; + yield [ + [8, 96], 12, 8, [ + [0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0], + ], + ]; + yield [ + [15, 40], 12, 8, [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + ], + ]; + } + + /** + * @dataProvider transformPositionsToSelFlagProvider + * + * @param array $positions + * @param array> $expected + */ + #[DataProvider('transformPositionsToSelFlagProvider')] + public function testTransformPositionsToSelFlag(array $positions, int $xWells, int $yWells, array $expected): void + { + self::assertEquals($expected, WellSelection::transformPositionsToSelFlag($positions, $xWells, $yWells)); + } +}