diff --git a/src/Flowcells/FlowcellLaneDoesNotExistException.php b/src/Flowcells/FlowcellLaneDoesNotExistException.php new file mode 100644 index 00000000..2300d71c --- /dev/null +++ b/src/Flowcells/FlowcellLaneDoesNotExistException.php @@ -0,0 +1,5 @@ + */ + public array $lanes; + + /** @param array|null $lanes */ + public function __construct(?array $lanes) + { + if ($lanes === null) { + $lanes = range(1, $this->totalLaneCount()); + } + $this->validate($lanes); + $this->lanes = $lanes; + } + + /** @param array $specificLanes */ + public function validate(array $specificLanes): void + { + $validLanes = range(1, $this->totalLaneCount()); + $invalidLanes = array_diff($specificLanes, $validLanes); + + if (count($invalidLanes) > 0) { + $label = count($invalidLanes) > 1 + ? 'lanes' + : 'lane'; + $invalidLanesAsString = implode(', ', $invalidLanes); + + throw new FlowcellLaneDoesNotExistException("The Flowcell-Type: '{$this->name()}' doesn't contain {$label}: {$invalidLanesAsString}"); + } + } +} diff --git a/src/Flowcells/Miseqi100_100M.php b/src/Flowcells/Miseqi100_100M.php new file mode 100644 index 00000000..b7e0dd47 --- /dev/null +++ b/src/Flowcells/Miseqi100_100M.php @@ -0,0 +1,16 @@ + $section->convertSectionToString(), + fn (Section $section): string => "[{$section->sectionName()}]" . PHP_EOL . $section->convertSectionToString(), $this->sections ); diff --git a/src/IlluminaSampleSheet/Section.php b/src/IlluminaSampleSheet/Section.php index c5711296..fa61108f 100644 --- a/src/IlluminaSampleSheet/Section.php +++ b/src/IlluminaSampleSheet/Section.php @@ -5,4 +5,6 @@ interface Section { public function convertSectionToString(): string; + + public function sectionName(): string; } diff --git a/src/IlluminaSampleSheet/V1/DataSection.php b/src/IlluminaSampleSheet/V1/DataSection.php index a432db5c..7fceb791 100644 --- a/src/IlluminaSampleSheet/V1/DataSection.php +++ b/src/IlluminaSampleSheet/V1/DataSection.php @@ -48,7 +48,7 @@ public function convertSectionToString(): string ->map(fn (Row $row): string => $row->toString()) ->implode("\n"); - return "[Data]\n{$firstRow->headerLine()}\n{$rowsData}\n"; + return "{$firstRow->headerLine()}\n{$rowsData}\n"; } protected function validateDuplicatedSampleIDs(): void @@ -65,4 +65,9 @@ protected function validateDuplicatedSampleIDs(): void throw new IlluminaSampleSheetException("Sample_ID values must be distinct. Duplicated SampleIDs: {$duplicateIDsAsString}"); } } + + public function sectionName(): string + { + return 'Data'; + } } diff --git a/src/IlluminaSampleSheet/V1/HeaderSection.php b/src/IlluminaSampleSheet/V1/HeaderSection.php index 208d8201..01d55ae3 100644 --- a/src/IlluminaSampleSheet/V1/HeaderSection.php +++ b/src/IlluminaSampleSheet/V1/HeaderSection.php @@ -49,7 +49,6 @@ public function __construct( public function convertSectionToString(): string { $headerLines = [ - '[Header]', "IEMFileVersion,{$this->iemFileVersion}", "Investigator Name,{$this->investigatorName}", "Experiment Name,{$this->experimentName}", @@ -63,4 +62,9 @@ public function convertSectionToString(): string return implode("\n", $headerLines); } + + public function sectionName(): string + { + return 'Header'; + } } diff --git a/src/IlluminaSampleSheet/V1/ReadsSection.php b/src/IlluminaSampleSheet/V1/ReadsSection.php index fcde2071..e8833e91 100644 --- a/src/IlluminaSampleSheet/V1/ReadsSection.php +++ b/src/IlluminaSampleSheet/V1/ReadsSection.php @@ -18,6 +18,11 @@ public function __construct(int $read1Cycles, int $read2Cycles) public function convertSectionToString(): string { - return "[Reads]\n" . $this->read1Cycles . "\n" . $this->read2Cycles; + return $this->read1Cycles . "\n" . $this->read2Cycles; + } + + public function sectionName(): string + { + return 'Reads'; } } diff --git a/src/IlluminaSampleSheet/V1/SettingsSection.php b/src/IlluminaSampleSheet/V1/SettingsSection.php index 6fc8b614..1970a807 100644 --- a/src/IlluminaSampleSheet/V1/SettingsSection.php +++ b/src/IlluminaSampleSheet/V1/SettingsSection.php @@ -18,7 +18,7 @@ public function __construct(?string $adapter = null, ?string $adapterRead2 = nul public function convertSectionToString(): string { - $settingsLines = ['[Settings]']; + $settingsLines = []; if ($this->adapter !== null) { $settingsLines[] = 'Adapter,' . $this->adapter; @@ -30,4 +30,9 @@ public function convertSectionToString(): string return implode("\n", $settingsLines); } + + public function sectionName(): string + { + return 'Settings'; + } } diff --git a/src/IlluminaSampleSheet/V2/BclConvert/BclConvertSection.php b/src/IlluminaSampleSheet/V2/BclConvert/BclConvertSection.php deleted file mode 100644 index e98757ab..00000000 --- a/src/IlluminaSampleSheet/V2/BclConvert/BclConvertSection.php +++ /dev/null @@ -1,34 +0,0 @@ -settingsSection = $settingsSection; - $this->dataSection = $dataSection; - } - - public function convertSectionToString(): string - { - $bclConvertLines = [ - $this->settingsSection->convertSectionToString(), - $this->dataSection->convertSectionToString(), - ]; - - return implode("\n", $bclConvertLines); - } - - public function makeReadsSection(): ReadsSection - { - return $this->dataSection->makeReadsSection(); - } -} diff --git a/src/IlluminaSampleSheet/V2/BclConvert/BclSample.php b/src/IlluminaSampleSheet/V2/BclConvert/BclSample.php index d9419ee9..a7b960cf 100644 --- a/src/IlluminaSampleSheet/V2/BclConvert/BclSample.php +++ b/src/IlluminaSampleSheet/V2/BclConvert/BclSample.php @@ -2,55 +2,67 @@ namespace MLL\Utils\IlluminaSampleSheet\V2\BclConvert; +use MLL\Utils\Flowcells\FlowcellType; + class BclSample { - public int $lane; + public FlowcellType $flowcellType; - /** Not using camelCase because the property names of this class must match the CSV file. */ - public string $sample_ID; + public string $sampleID; - public string $index; + public string $indexRead1; - public ?string $index2 = null; + public ?string $indexRead2; public OverrideCycles $overrideCycles; - public ?string $adapterRead1 = null; - - public ?string $adapterRead2 = null; + public string $adapterRead1; - public ?string $barcodeMismatchesIndex1 = null; + public string $adapterRead2; - public ?string $barcodeMismatchesIndex2 = null; + public string $barcodeMismatchesIndex1; - public ?string $project = null; + public ?string $barcodeMismatchesIndex2; public function __construct( - int $lane, - string $sample_ID, - string $index, - OverrideCycles $overrideCycles + FlowcellType $flowcellType, + string $sampleID, + string $indexRead1, + ?string $indexRead2, + OverrideCycles $overrideCycles, + string $adapterRead1, + string $adapterRead2, + string $barcodeMismatchesIndex1, + ?string $barcodeMismatchesIndex2 ) { - $this->lane = $lane; - $this->sample_ID = $sample_ID; - $this->index = $index; + $this->flowcellType = $flowcellType; + $this->sampleID = $sampleID; + $this->indexRead1 = $indexRead1; + $this->indexRead2 = $indexRead2; $this->overrideCycles = $overrideCycles; + $this->adapterRead1 = $adapterRead1; + $this->adapterRead2 = $adapterRead2; + $this->barcodeMismatchesIndex1 = $barcodeMismatchesIndex1; + $this->barcodeMismatchesIndex2 = $barcodeMismatchesIndex2; } - /** @return array */ - public function toArray(): array + public function toString(OverrideCycleCounter $overrideCycleCounter): string { - return array_filter([ // @phpstan-ignore arrayFilter.strict (we want truthy comparison) - $this->lane, - $this->sample_ID, - $this->index, - $this->index2, - $this->overrideCycles->toString(), - $this->adapterRead1, - $this->adapterRead2, - $this->barcodeMismatchesIndex1, - $this->barcodeMismatchesIndex2, - $this->project, - ]); + $lines = array_map( + fn (int $lane): string => implode(',', [ + $lane, + $this->sampleID, + $this->indexRead1, + $this->indexRead2 ?? '', + $this->overrideCycles->toString($overrideCycleCounter), + $this->adapterRead1, + $this->adapterRead2, + $this->barcodeMismatchesIndex1, + $this->barcodeMismatchesIndex2 ?? '', + ]), + $this->flowcellType->lanes + ); + + return implode(PHP_EOL, $lines); } } diff --git a/src/IlluminaSampleSheet/V2/BclConvert/CycleType.php b/src/IlluminaSampleSheet/V2/BclConvert/CycleType.php index f26983dd..636e9ba8 100644 --- a/src/IlluminaSampleSheet/V2/BclConvert/CycleType.php +++ b/src/IlluminaSampleSheet/V2/BclConvert/CycleType.php @@ -15,24 +15,4 @@ public function __construct(string $value) { $this->value = $value; } - - public static function READ_CYCLE(): self - { - return new static(self::READ_CYCLE); - } - - public static function TRIMMED_CYCLE(): self - { - return new static(self::TRIMMED_CYCLE); - } - - public static function UMI_CYCLE(): self - { - return new static(self::UMI_CYCLE); - } - - public static function INDEX_CYCLE(): self - { - return new static(self::INDEX_CYCLE); - } } diff --git a/src/IlluminaSampleSheet/V2/BclConvert/CycleTypeWithCount.php b/src/IlluminaSampleSheet/V2/BclConvert/CycleTypeWithCount.php index 039da119..2556e5cf 100644 --- a/src/IlluminaSampleSheet/V2/BclConvert/CycleTypeWithCount.php +++ b/src/IlluminaSampleSheet/V2/BclConvert/CycleTypeWithCount.php @@ -4,7 +4,7 @@ class CycleTypeWithCount { - protected CycleType $cycleType; + public CycleType $cycleType; public int $count; diff --git a/src/IlluminaSampleSheet/V2/BclConvert/DataSection.php b/src/IlluminaSampleSheet/V2/BclConvert/DataSection.php deleted file mode 100644 index db3fc4f3..00000000 --- a/src/IlluminaSampleSheet/V2/BclConvert/DataSection.php +++ /dev/null @@ -1,131 +0,0 @@ - */ - public Collection $dataRows; - - public function __construct() - { - $this->dataRows = new Collection(); - } - - public function addSample(BclSample $bclSample): void - { - $this->dataRows->add($bclSample); - } - - public function convertSectionToString(): string - { - $this->assertNotEmpty(); - - $object = $this->dataRows[0]; - if (! is_object($object)) { - throw new IlluminaSampleSheetException('Trying to convert empty data section to string.'); - } - /** @var array $samplePropertiesOfFirstSample */ - $samplePropertiesOfFirstSample = array_keys(get_object_vars($object)); - foreach ($this->dataRows as $sample) { - $actualProperties = array_keys(get_object_vars($sample)); - if ($samplePropertiesOfFirstSample !== $actualProperties) { - throw new IlluminaSampleSheetException('All samples must have the same properties. Expected: ' . \Safe\json_encode($samplePropertiesOfFirstSample) . ', Actual: ' . \Safe\json_encode($actualProperties)); - } - } - - $bclConvertDataHeaderLines = $this->generateDataHeaderByProperties($samplePropertiesOfFirstSample); - - $bclConvertDataLines = [ - '[BCLConvert_Data]', - $bclConvertDataHeaderLines, - ]; - - foreach ($this->dataRows as $dataRow) { - $bclConvertDataLines[] = implode(',', $dataRow->toArray()); - } - - return implode("\n", $bclConvertDataLines) . "\n"; - } - - /** @param array $samplePropertiesOfFirstSample */ - protected function generateDataHeaderByProperties(array $samplePropertiesOfFirstSample): string - { - $samplePropertiesOfFirstSample = array_filter($samplePropertiesOfFirstSample, fn (string $value): bool // @phpstan-ignore-next-line Variable property access on a non-object required here - => $this->dataRows[0]->$value !== null); - - $samplePropertiesOfFirstSample = array_map(fn (string $value): string => ucfirst($value), $samplePropertiesOfFirstSample); - - return implode(',', $samplePropertiesOfFirstSample); - } - - public function makeReadsSection(): ReadsSection - { - return new ReadsSection( - $this->maxRead1Cycles(), - $this->maxIndex1Cycles(), - $this->maxRead2Cycles(), - $this->maxIndex2Cycles() - ); - } - - public function maxRead1Cycles(): int - { - $max = $this->dataRows - ->max(fn (BclSample $dataRow): int => $dataRow - ->overrideCycles - ->read1 - ->sumCountOfAllCycles()); - assert(is_int($max)); - - return $max; - } - - public function maxRead2Cycles(): ?int - { - $max = $this->dataRows->max( - fn (BclSample $dataRow): ?int => $dataRow->overrideCycles->read2 instanceof OverrideCycle - ? $dataRow->overrideCycles->read2->sumCountOfAllCycles() - : null - ); - assert(is_int($max) || is_null($max)); - - return $max; - } - - public function maxIndex1Cycles(): int - { - $index1Cycles = $this->dataRows - ->max(fn (BclSample $dataRow): int => $dataRow - ->overrideCycles - ->index1 - ->sumCountOfAllCycles()); - assert(is_int($index1Cycles)); - - return $index1Cycles; - } - - public function maxIndex2Cycles(): ?int - { - $index2Cycles = $this->dataRows->max( - fn (BclSample $dataRow): ?int => $dataRow->overrideCycles->index2 instanceof OverrideCycle - ? $dataRow->overrideCycles->index2->sumCountOfAllCycles() - : null - ); - assert(is_int($index2Cycles) || is_null($index2Cycles)); - - return $index2Cycles; - } - - public function assertNotEmpty(): void - { - if ($this->dataRows->isEmpty()) { - throw new IlluminaSampleSheetException('At least one sample must be added to the DataSection.'); - } - } -} diff --git a/src/IlluminaSampleSheet/V2/BclConvert/NucleotideType.php b/src/IlluminaSampleSheet/V2/BclConvert/NucleotideType.php new file mode 100644 index 00000000..44c6adf8 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/BclConvert/NucleotideType.php @@ -0,0 +1,18 @@ +value = $value; + } +} diff --git a/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycle.php b/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycle.php index 54c035b7..032db3b3 100644 --- a/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycle.php +++ b/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycle.php @@ -3,39 +3,70 @@ namespace MLL\Utils\IlluminaSampleSheet\V2\BclConvert; use MLL\Utils\IlluminaSampleSheet\IlluminaSampleSheetException; +use MLL\Utils\IlluminaSampleSheet\V2\IndexOrientation; class OverrideCycle { - /** @var array */ - public array $cycles; + /** @var array */ + public array $cycleTypeWithCountList; - /** @param array $cycles */ - public function __construct(array $cycles) + public IndexOrientation $indexOrientation; + + /** @param array $cycleTypeWithCountList */ + public function __construct(array $cycleTypeWithCountList, IndexOrientation $indexOrientation) { - $this->cycles = $cycles; + $this->cycleTypeWithCountList = $cycleTypeWithCountList; + $this->indexOrientation = $indexOrientation; } - /** @param bool|null $isSecondIndexAndForwardDirection null if it is not the secondIndex, true if it is the secondIndex and forwardDirection, false if it is the secondIndex and reverseDirection */ - public function toString(int $fillUpToMax, ?bool $isSecondIndexAndForwardDirection): string + public static function fromString( + string $cycleString, + IndexOrientation $indexOrientation + ): self { + \Safe\preg_match_all('/([YNUI])(\d+)/', $cycleString, $matches, PREG_SET_ORDER); + + if (count($matches) > 4) { + throw new IlluminaSampleSheetException("Invalid Override Cycle Part. Should have at most 4 parts: {$cycleString}."); + } + + if (count($matches) === 0) { + throw new IlluminaSampleSheetException("Invalid Override Cycle Part. Should have at least 1 part: {$cycleString}."); + } + + return new self( + array_map( + fn (array $match): CycleTypeWithCount => new CycleTypeWithCount(new CycleType($match[1]), (int) $match[2]), + $matches + ), + $indexOrientation + ); + } + + public function fillUpTo(int $fillUpToMaxNucleotideCount, NucleotideType $nucleotideType): self { $countOfAllCycleTypes = $this->sumCountOfAllCycles(); - if ($countOfAllCycleTypes > $fillUpToMax) { - throw new IlluminaSampleSheetException("The sum of all cycle types must be less than or equal to the fill up to max value. \$countOfAllCycleTypes: {$countOfAllCycleTypes} > \$fillUpToMax: {$fillUpToMax}"); + if ($countOfAllCycleTypes > $fillUpToMaxNucleotideCount) { + throw new IlluminaSampleSheetException("The sum of all cycle types must be less than or equal to the fill up to max value. \$countOfAllCycleTypes: {$countOfAllCycleTypes} > \$fillUpToMax: {$fillUpToMaxNucleotideCount}"); } - $rawOverrideCycle = implode('', array_map( - fn (CycleTypeWithCount $cycle): string => $cycle->toString(), - $this->cycles - )); - if ($countOfAllCycleTypes === $fillUpToMax) { - return $rawOverrideCycle; + if ($countOfAllCycleTypes === $fillUpToMaxNucleotideCount) { + return $this; } - $remainingCycles = 'N' . ($fillUpToMax - $countOfAllCycleTypes); + $trimmedCycle = new CycleTypeWithCount( + new CycleType(CycleType::TRIMMED_CYCLE), + $fillUpToMaxNucleotideCount - $countOfAllCycleTypes + ); + + $newCycleTypeWithCountList = $this->cycleTypeWithCountList; + + if ($nucleotideType->value === NucleotideType::I2 && $this->indexOrientation->value === IndexOrientation::FORWARD) { + array_unshift($newCycleTypeWithCountList, $trimmedCycle); + } else { + $newCycleTypeWithCountList[] = $trimmedCycle; + } - return (bool) $isSecondIndexAndForwardDirection - ? $remainingCycles . $rawOverrideCycle - : $rawOverrideCycle . $remainingCycles; + return new self($newCycleTypeWithCountList, $this->indexOrientation); } public function sumCountOfAllCycles(): int @@ -43,8 +74,16 @@ public function sumCountOfAllCycles(): int return array_sum( array_map( fn (CycleTypeWithCount $cycleTypeWithCount): int => $cycleTypeWithCount->count, - $this->cycles + $this->cycleTypeWithCountList ) ); } + + public function toString(): string + { + return implode('', array_map( + fn (CycleTypeWithCount $cycle): string => $cycle->toString(), + $this->cycleTypeWithCountList + )); + } } diff --git a/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycleCounter.php b/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycleCounter.php new file mode 100644 index 00000000..af11e255 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycleCounter.php @@ -0,0 +1,65 @@ + */ + public Collection $overrideCyclesList; + + /** @param Collection $overrideCyclesList */ + public function __construct(Collection $overrideCyclesList) + { + $this->overrideCyclesList = $overrideCyclesList; + } + + public function maxRead1CycleCount(): int + { + $max = $this->overrideCyclesList + ->max(fn (OverrideCycles $overrideCycles): int => $overrideCycles + ->overrideCycleRead1 + ->sumCountOfAllCycles()); + assert(is_int($max)); + + return $max; + } + + public function maxIndex1CycleCount(): int + { + $max = $this->overrideCyclesList + ->max(fn (OverrideCycles $overrideCycles): int => $overrideCycles + ->overrideCycleIndex1 + ->sumCountOfAllCycles()); + assert(is_int($max)); + + return $max; + } + + public function maxIndex2CycleCount(): int + { + $max = $this->overrideCyclesList + ->max( + fn (OverrideCycles $overrideCycles): int => $overrideCycles->overrideCycleIndex2 !== null + ? $overrideCycles->overrideCycleIndex2->sumCountOfAllCycles() + : 0 + ); + assert(is_int($max)); + + return $max; + } + + public function maxRead2CycleCount(): int + { + $max = $this->overrideCyclesList + ->max( + fn (OverrideCycles $overrideCycles): int => $overrideCycles->overrideCycleRead2 !== null + ? $overrideCycles->overrideCycleRead2->sumCountOfAllCycles() + : 0 + ); + assert(is_int($max)); + + return $max; + } +} diff --git a/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycles.php b/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycles.php index 03715cfd..5822ae5d 100644 --- a/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycles.php +++ b/src/IlluminaSampleSheet/V2/BclConvert/OverrideCycles.php @@ -3,87 +3,71 @@ namespace MLL\Utils\IlluminaSampleSheet\V2\BclConvert; use MLL\Utils\IlluminaSampleSheet\IlluminaSampleSheetException; -use MLL\Utils\IlluminaSampleSheet\V2\HeaderSection; +use MLL\Utils\IlluminaSampleSheet\V2\IndexOrientation; class OverrideCycles { - public OverrideCycle $read1; + public OverrideCycle $overrideCycleRead1; - public OverrideCycle $index1; + public OverrideCycle $overrideCycleIndex1; - public ?OverrideCycle $index2; + public ?OverrideCycle $overrideCycleIndex2; - public ?OverrideCycle $read2; + public ?OverrideCycle $overrideCycleRead2; - private DataSection $dataSection; - - public function __construct(DataSection $dataSection, string $read1, string $index1, ?string $index2, ?string $read2) - { - $this->read1 = $this->makeOverrideCycle($read1); - $this->index1 = $this->makeOverrideCycle($index1); - $this->index2 = $index2 !== null ? $this->makeOverrideCycle($index2) : null; - $this->read2 = $read2 !== null ? $this->makeOverrideCycle($read2) : null; - $this->dataSection = $dataSection; - } - - public function toString(): string - { - $dataSection = $this->dataSection; - $dataSection->assertNotEmpty(); - - $filledParts = array_filter([ // @phpstan-ignore arrayFilter.strict (we want truthy comparison) - $this->read1->toString($dataSection->maxRead1Cycles(), null), - $this->index1->toString($dataSection->maxIndex1Cycles(), null), - $this->index2(), - $this->read2(), - ]); - - return implode(';', $filledParts); + public function __construct( + OverrideCycle $overrideCycleRead1, + OverrideCycle $overrideCycleIndex1, + ?OverrideCycle $overrideCycleIndex2, + ?OverrideCycle $overrideCycleRead2 + ) { + $this->overrideCycleRead1 = $overrideCycleRead1; + $this->overrideCycleIndex1 = $overrideCycleIndex1; + $this->overrideCycleIndex2 = $overrideCycleIndex2; + $this->overrideCycleRead2 = $overrideCycleRead2; } - public function makeOverrideCycle(string $cycleString): OverrideCycle + public static function fromString(string $overrideCyclesAsString, IndexOrientation $indexOrientation): self { - \Safe\preg_match_all('/([YNUI]+)(\d+)/', $cycleString, $matches, PREG_SET_ORDER); - - if (count($matches) > 3) { - throw new IlluminaSampleSheetException("Invalid Override Cycle Part. Should have less than 4 parts: {$cycleString}."); - } + $overrideCyclesAsArray = explode(';', $overrideCyclesAsString); - if (count($matches) === 0) { - throw new IlluminaSampleSheetException("Invalid Override Cycle Part. Should have at least 1 part: {$cycleString}."); + if (count($overrideCyclesAsArray) < 2) { + throw new IlluminaSampleSheetException("Invalid OverrideCycles string. Must contain at least 2 semicolon-separated parts (Read1 and Index1): {$overrideCyclesAsString}"); } - return new OverrideCycle( - array_map( - fn (array $match): CycleTypeWithCount => new CycleTypeWithCount(new CycleType($match[1]), (int) $match[2]), - $matches - ) + return new self( + OverrideCycle::fromString($overrideCyclesAsArray[0], $indexOrientation), + OverrideCycle::fromString($overrideCyclesAsArray[1], $indexOrientation), + isset($overrideCyclesAsArray[2]) + ? OverrideCycle::fromString($overrideCyclesAsArray[2], $indexOrientation) + : null, + isset($overrideCyclesAsArray[3]) + ? OverrideCycle::fromString($overrideCyclesAsArray[3], $indexOrientation) + : null ); } - private function index2(): ?string + public function toString(OverrideCycleCounter $overrideCycleCounter): string { - if (! $this->index2 instanceof OverrideCycle) { - return null; - } - $maxIndex2Cycles = $this->dataSection->maxIndex2Cycles(); - if ($maxIndex2Cycles === null) { - throw new IlluminaSampleSheetException('MaxIndex2Cycles is required when Index2 is set.'); - } - - return $this->index2->toString($maxIndex2Cycles, HeaderSection::isForwardIndexOrientation()); - } - - private function read2(): ?string - { - if (! $this->read2 instanceof OverrideCycle) { - return null; - } - $maxIndex2Cycles = $this->dataSection->maxRead2Cycles(); - if ($maxIndex2Cycles === null) { - throw new IlluminaSampleSheetException('MaxRead2Cycles is required when Read2 is set.'); - } + $filledParts = array_filter([ // @phpstan-ignore arrayFilter.strict (we want truthy comparison) + $this->overrideCycleRead1 + ->fillUpTo($overrideCycleCounter->maxRead1CycleCount(), new NucleotideType(NucleotideType::R1)) + ->toString(), + $this->overrideCycleIndex1 + ->fillUpTo($overrideCycleCounter->maxIndex1CycleCount(), new NucleotideType(NucleotideType::I1)) + ->toString(), + $this->overrideCycleIndex2 !== null + ? $this->overrideCycleIndex2 + ->fillUpTo($overrideCycleCounter->maxIndex2CycleCount(), new NucleotideType(NucleotideType::I2)) + ->toString() + : null, + $this->overrideCycleRead2 !== null + ? $this->overrideCycleRead2 + ->fillUpTo($overrideCycleCounter->maxRead2CycleCount(), new NucleotideType(NucleotideType::R2)) + ->toString() + : null, + ]); - return $this->read2->toString($maxIndex2Cycles, null); + return implode(';', $filledParts); } } diff --git a/src/IlluminaSampleSheet/V2/BclConvert/SettingsSection.php b/src/IlluminaSampleSheet/V2/BclConvert/SettingsSection.php deleted file mode 100644 index 7279c24f..00000000 --- a/src/IlluminaSampleSheet/V2/BclConvert/SettingsSection.php +++ /dev/null @@ -1,42 +0,0 @@ -softwareVersion = $softwareVersion; - $this->fastqCompressionFormat = $fastqCompressionFormat; - } - - public function convertSectionToString(): string - { - $bclConvertSettingsLines = [ - '[BCLConvert_Settings]', - "SoftwareVersion,{$this->softwareVersion}", - "FastqCompressionFormat,{$this->fastqCompressionFormat->value}", - ]; - - if (! is_null($this->trimUMI)) { - $trimUMIAsString = $this->trimUMI - ? '1' - : '0'; - - $bclConvertSettingsLines[] = "TrimUMI,{$trimUMIAsString}"; - } - - return implode("\n", $bclConvertSettingsLines) . "\n"; - } -} diff --git a/src/IlluminaSampleSheet/V2/BclConvertSoftwareVersion.php b/src/IlluminaSampleSheet/V2/BclConvertSoftwareVersion.php new file mode 100644 index 00000000..67a3dc34 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/BclConvertSoftwareVersion.php @@ -0,0 +1,20 @@ +value = $value; + } + + public static function V4_1_23(): self + { + return new self(self::V4_1_23); + } +} diff --git a/src/IlluminaSampleSheet/V2/Enums/FastQCompressionFormat.php b/src/IlluminaSampleSheet/V2/Enums/FastQCompressionFormat.php index d8138203..42b69b82 100644 --- a/src/IlluminaSampleSheet/V2/Enums/FastQCompressionFormat.php +++ b/src/IlluminaSampleSheet/V2/Enums/FastQCompressionFormat.php @@ -13,14 +13,4 @@ public function __construct(string $value) { $this->value = $value; } - - public static function GZIP(): self - { - return new static(self::GZIP); - } - - public static function DRAGEN(): self - { - return new static(self::DRAGEN); - } } diff --git a/src/IlluminaSampleSheet/V2/HeaderSection.php b/src/IlluminaSampleSheet/V2/HeaderSection.php deleted file mode 100644 index 8731f43c..00000000 --- a/src/IlluminaSampleSheet/V2/HeaderSection.php +++ /dev/null @@ -1,71 +0,0 @@ - */ - protected array $customParams = []; - - public function __construct(string $runName) - { - $this->runName = $runName; - } - - public static function isForwardIndexOrientation(): bool - { - // Added this static method to explicitly display that this flag influences Index2 in OverrideCycles. - return HeaderSection::indexOrientation() === HeaderSection::INDEX_ORIENTATION_FORWARD; - } - - public function setCustomParam(string $paramName, string $paramValue): void - { - $this->customParams['Custom_' . $paramName] = $paramValue; - } - - public function convertSectionToString(): string - { - $indexOrientation = self::indexOrientation(); - $headerLines = [ - '[Header]', - "FileFormatVersion,{$this->fileFormatVersion}", - "RunName,{$this->runName}", - "IndexOrientation,{$indexOrientation}", - ]; - if (! is_null($this->runDescription)) { - $headerLines[] = "RunDescription,{$this->runDescription}"; - } - if (! is_null($this->instrumentType)) { - $headerLines[] = "InstrumentType,{$this->instrumentType}"; - } - if (! is_null($this->instrumentPlatform)) { - $headerLines[] = "InstrumentPlatform,{$this->instrumentPlatform}"; - } - foreach ($this->customParams as $paramName => $paramValue) { - $headerLines[] = "{$paramName},{$paramValue}"; - } - - return implode("\n", $headerLines) . "\n"; - } - - public static function indexOrientation(): string - { - // Only support the default IndexOrientation (Forward) for now. - return self::INDEX_ORIENTATION_FORWARD; - } -} diff --git a/src/IlluminaSampleSheet/V2/IlluminaSampleSheetVersion2.php b/src/IlluminaSampleSheet/V2/IlluminaSampleSheetVersion2.php new file mode 100644 index 00000000..26091824 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/IlluminaSampleSheetVersion2.php @@ -0,0 +1,46 @@ +performAnalysisOn(AnalysisLocation::CLOUD()); + } else { + $headerSection->performAnalysisOnLocalMachine(); + $bclConvertSettingsSection->performAnalysisOn(AnalysisLocation::LOCAL_MACHINE()); + } + + $this->addSection($headerSection); + $this->addSection($readsSection); + $this->addSection($bclConvertSettingsSection); + $this->addSection($bclConvertDataSection); + if ($cloudSettingsSection instanceof CloudSettingsSection) { + $this->addSection($cloudSettingsSection); + } + if ($cloudDataSection instanceof CloudDataSection) { + $this->addSection($cloudDataSection); + } + } +} diff --git a/src/IlluminaSampleSheet/V2/IndexOrientation.php b/src/IlluminaSampleSheet/V2/IndexOrientation.php new file mode 100644 index 00000000..d421c838 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/IndexOrientation.php @@ -0,0 +1,26 @@ +value = $value; + } + + public static function FORWARD(): self + { + return new self(self::FORWARD); + } + + public static function REVERSE(): self + { + return new self(self::REVERSE); + } +} diff --git a/src/IlluminaSampleSheet/V2/InstrumentPlatform.php b/src/IlluminaSampleSheet/V2/InstrumentPlatform.php new file mode 100644 index 00000000..0d40ce43 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/InstrumentPlatform.php @@ -0,0 +1,26 @@ +value = $value; + } + + public static function NOVASEQ_X_SERIES(): self + { + return new self(self::NOVASEQ_X_SERIES); + } + + public static function MISEQ_I100_SERIES(): self + { + return new self(self::MISEQ_I100_SERIES); + } +} diff --git a/src/IlluminaSampleSheet/V2/NovaSeqXSampleSheet.php b/src/IlluminaSampleSheet/V2/NovaSeqXSampleSheet.php deleted file mode 100644 index 430ed28c..00000000 --- a/src/IlluminaSampleSheet/V2/NovaSeqXSampleSheet.php +++ /dev/null @@ -1,21 +0,0 @@ -addSection($header); - - if (! is_null($bclConvertSection)) { - $this->addSection($bclConvertSection->makeReadsSection()); - $this->addSection($bclConvertSection); - } - } -} diff --git a/src/IlluminaSampleSheet/V2/ReadsSection.php b/src/IlluminaSampleSheet/V2/ReadsSection.php deleted file mode 100644 index 6a28e50b..00000000 --- a/src/IlluminaSampleSheet/V2/ReadsSection.php +++ /dev/null @@ -1,55 +0,0 @@ -read1Cycles = $read1Cycles; - $this->read2Cycles = $read2Cycles; - $this->index1Cycles = $index1Cycles; - $this->index2Cycles = $index2Cycles; - } - - public function convertSectionToString(): string - { - $readsLines = ['[Reads]']; - $readsLines[] = "Read1Cycles,{$this->read1Cycles}"; - - if ($this->read2Cycles !== null) { - $readsLines[] = "Read2Cycles,{$this->read2Cycles}"; - } - - $readsLines[] = "Index1Cycles,{$this->index1Cycles}"; - - if ($this->index2Cycles !== null) { - $readsLines[] = "Index2Cycles,{$this->index2Cycles}"; - } - - return implode("\n", $readsLines) . "\n"; - } -} diff --git a/src/IlluminaSampleSheet/V2/Sections/AnalysisLocation.php b/src/IlluminaSampleSheet/V2/Sections/AnalysisLocation.php new file mode 100644 index 00000000..e1d8dd61 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/AnalysisLocation.php @@ -0,0 +1,26 @@ +value = $value; + } + + public static function LOCAL_MACHINE(): self + { + return new self(self::LOCAL_MACHINE); + } + + public static function CLOUD(): self + { + return new self(self::CLOUD); + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/BclConvertDataSection.php b/src/IlluminaSampleSheet/V2/Sections/BclConvertDataSection.php new file mode 100644 index 00000000..eff4080f --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/BclConvertDataSection.php @@ -0,0 +1,53 @@ + */ + public Collection $bclSampleList; + + public OverrideCycleCounter $overrideCycleCounter; + + /** @param Collection $bclSampleList */ + public function __construct(Collection $bclSampleList) + { + $this->bclSampleList = $bclSampleList; + $this->overrideCycleCounter = new OverrideCycleCounter( + $this->bclSampleList->map(fn (BclSample $bclSample): OverrideCycles => $bclSample->overrideCycles) + ); + } + + public function convertSectionToString(): string + { + $this->assertNotEmpty(); + + return + self::HEADER_ROW . PHP_EOL + . $this->bclSampleList + ->map(fn (BclSample $bclSample): string => $bclSample->toString($this->overrideCycleCounter)) + ->join(PHP_EOL) . PHP_EOL; + } + + public function assertNotEmpty(): void + { + if ($this->bclSampleList->isEmpty()) { + throw new IlluminaSampleSheetException('At least one sample must be added to the DataSection.'); + } + } + + public function sectionName(): string + { + return 'BCLConvert_Data'; + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/BclConvertSettingsSection.php b/src/IlluminaSampleSheet/V2/Sections/BclConvertSettingsSection.php new file mode 100644 index 00000000..ed862474 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/BclConvertSettingsSection.php @@ -0,0 +1,46 @@ + FastQCompressionFormat::GZIP, + ]); + + parent::__construct($fields); + } + + public function performAnalysisOn(AnalysisLocation $analysisLocation): void + { + $this->analysisLocation = $analysisLocation; + $analysisLocation->value === AnalysisLocation::LOCAL_MACHINE + ? $this->performAnalysisOnLocalMachine() + : $this->performAnalysisOnCloud(); + } + + private function performAnalysisOnCloud(): void + { + $this->keyValues['SoftwareVersion'] = BclConvertSoftwareVersion::V4_1_23; + } + + private function performAnalysisOnLocalMachine(): void + { + $this->keyValues['GenerateFastqcMetrics'] = 'true'; + } + + public function sectionName(): string + { + $this->checkIfAnalysisLocationIsSet(); + + return 'BCLConvert_Settings'; + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/CloudDataItem.php b/src/IlluminaSampleSheet/V2/Sections/CloudDataItem.php new file mode 100644 index 00000000..574de175 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/CloudDataItem.php @@ -0,0 +1,31 @@ +bioSampleName = $bioSampleName; + $this->projectName = $projectName; + $this->libraryName = $libraryName; + } + + public function toString(): string + { + return implode(',', [ + $this->bioSampleName, + $this->projectName, + $this->libraryName, + ]); + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/CloudDataSection.php b/src/IlluminaSampleSheet/V2/Sections/CloudDataSection.php new file mode 100644 index 00000000..61dfffd6 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/CloudDataSection.php @@ -0,0 +1,35 @@ + */ + private Collection $cloudDataItems; + + /** @param Collection $cloudDataItems */ + public function __construct(Collection $cloudDataItems) + { + $this->cloudDataItems = $cloudDataItems; + } + + public function convertSectionToString(): string + { + return + self::HEADER_ROW . PHP_EOL + . $this->cloudDataItems + ->map(fn (CloudDataItem $cloudDataItem): string => $cloudDataItem->toString()) + ->join(PHP_EOL) . PHP_EOL; + } + + public function sectionName(): string + { + return 'Cloud_Data'; + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/CloudSettingsSection.php b/src/IlluminaSampleSheet/V2/Sections/CloudSettingsSection.php new file mode 100644 index 00000000..69e7017e --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/CloudSettingsSection.php @@ -0,0 +1,27 @@ + self::GENERATED_VERSION, + 'Cloud_Workflow' => self::CLOUD_WORKFLOW, + 'BCLConvert_Pipeline' => self::BCL_CONVERT_PIPELINE, + ])); + } + + public function sectionName(): string + { + return 'Cloud_Settings'; + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/HeaderSection.php b/src/IlluminaSampleSheet/V2/Sections/HeaderSection.php new file mode 100644 index 00000000..7b512fb2 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/HeaderSection.php @@ -0,0 +1,55 @@ +runName = $runName; + $this->indexOrientation = $indexOrientation; + $this->instrumentPlatform = $instrumentPlatform; + $this->runDescription = $runDescription; + + $fields = new Collection([ + 'FileFormatVersion' => self::FILE_FORMAT_VERSION, + 'RunName' => $this->runName, + 'IndexOrientation' => $this->indexOrientation->value, + 'InstrumentPlatform' => $this->instrumentPlatform->value, + ]); + + if (! is_null($this->runDescription)) { + $fields->put('RunDescription', $this->runDescription); + } + + parent::__construct($fields); + } + + public function sectionName(): string + { + return 'Header'; + } + + public function performAnalysisOnLocalMachine(): void + { + $this->keyValues['AnalysisLocation'] = 'Local'; + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/MissingAnalysisLocationSelectedException.php b/src/IlluminaSampleSheet/V2/Sections/MissingAnalysisLocationSelectedException.php new file mode 100644 index 00000000..fcaf4d93 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/MissingAnalysisLocationSelectedException.php @@ -0,0 +1,11 @@ + 0 && $maximumCycleCountForRead2 < 6) + ) { + throw new IlluminaSampleSheetException('Read2Cycles must be a positive integer.'); + } + + if ($maximumCycleCountForIndex1 < 6) { + throw new IlluminaSampleSheetException('Index1Cycles must be at least 6.'); + } + /** + * Maximum cycle count for index2 can be 0 (Single Indexing), but it the maximum cycle count for index 2 + * exists (Dual indexing) it has to be at least 6. + */ + if ( + $maximumCycleCountForIndex2 < 0 + || ($maximumCycleCountForIndex2 > 0 && $maximumCycleCountForIndex2 < 6) + ) { + throw new IlluminaSampleSheetException('Index2Cycles must be at least 6.'); + } + + $fields = new Collection(); + + $fields->put('Read1Cycles', $maximumCycleCountForRead1); + if ($maximumCycleCountForRead2 > 0) { + $fields->put('Read2Cycles', $maximumCycleCountForRead2); + } + + $fields->put('Index1Cycles', $maximumCycleCountForIndex1); + if ($maximumCycleCountForIndex2 > 0) { + $fields->put('Index2Cycles', $maximumCycleCountForIndex2); + } + + parent::__construct($fields); + } + + public static function fromOverrideCycleCounter(OverrideCycleCounter $overrideCycleCounter): self + { + return new self( + $overrideCycleCounter->maxRead1CycleCount(), + $overrideCycleCounter->maxIndex1CycleCount(), + $overrideCycleCounter->maxRead2CycleCount(), + $overrideCycleCounter->maxIndex2CycleCount() + ); + } + + public function sectionName(): string + { + return 'Reads'; + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/RequiresAnalysisLocationToBeSet.php b/src/IlluminaSampleSheet/V2/Sections/RequiresAnalysisLocationToBeSet.php new file mode 100644 index 00000000..dbf3c989 --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/RequiresAnalysisLocationToBeSet.php @@ -0,0 +1,15 @@ +analysisLocation === null) { + throw new MissingAnalysisLocationSelectedException(); + } + } +} diff --git a/src/IlluminaSampleSheet/V2/Sections/SimpleKeyValueSection.php b/src/IlluminaSampleSheet/V2/Sections/SimpleKeyValueSection.php new file mode 100644 index 00000000..5c81d53c --- /dev/null +++ b/src/IlluminaSampleSheet/V2/Sections/SimpleKeyValueSection.php @@ -0,0 +1,27 @@ + */ + public Collection $keyValues; + + /** @param Collection $keyValues */ + public function __construct(Collection $keyValues) + { + $this->keyValues = $keyValues; + } + + public function convertSectionToString(): string + { + return + $this->keyValues + ->map(fn (string $value, string $key): string => "{$key},{$value}") + ->join(PHP_EOL) + . PHP_EOL; + } +} diff --git a/src/Qiaxcel/QiaxcelImport.php b/src/Qiaxcel/QiaxcelImport.php index 2bd5cb9e..b9c0a303 100644 --- a/src/Qiaxcel/QiaxcelImport.php +++ b/src/Qiaxcel/QiaxcelImport.php @@ -27,7 +27,7 @@ public function __construct( $this->valueForEmptyCell = $valueForEmptyCell; } - public function generate(): SpreadSheet + public function generate(): Spreadsheet { $sampleSheetData = []; foreach (array_chunk($this->entries, 12) as $entryChunks) { diff --git a/tests/FluidXPlate/FluidXScannerTest.php b/tests/FluidXPlate/FluidXScannerTest.php index b4a5b25a..29ebd9dd 100644 --- a/tests/FluidXPlate/FluidXScannerTest.php +++ b/tests/FluidXPlate/FluidXScannerTest.php @@ -1,6 +1,6 @@ createMock(Section::class); $sectionMock1->method('convertSectionToString')->willReturn('section1'); + $sectionMock1->method('sectionName')->willReturn('section1'); $sectionMock2 = $this->createMock(Section::class); $sectionMock2->method('convertSectionToString')->willReturn('section2'); + $sectionMock2->method('sectionName')->willReturn('section2'); $sampleSheet = $this->createPartialMock(BaseSampleSheet::class, []); $sampleSheet->addSection($sectionMock1); $sampleSheet->addSection($sectionMock2); - self::assertSame("section1\nsection2", $sampleSheet->toString()); + self::assertSame("[section1]\nsection1\n[section2]\nsection2", $sampleSheet->toString()); } } diff --git a/tests/IlluminaSampleSheet/V2/BclConvertSettingsSectionTest.php b/tests/IlluminaSampleSheet/V2/BclConvertSettingsSectionTest.php new file mode 100644 index 00000000..8a7e73d4 --- /dev/null +++ b/tests/IlluminaSampleSheet/V2/BclConvertSettingsSectionTest.php @@ -0,0 +1,35 @@ +performAnalysisOn(AnalysisLocation::CLOUD()); + $expected = <<<'CSV' +FastqCompressionFormat,gzip +SoftwareVersion,4.1.23 + +CSV; + self::assertSame($expected, $bclConvertSettingsSection->convertSectionToString()); + } + + public function testToStringLocal(): void + { + $bclConvertSettingsSection = new BclConvertSettingsSection(); + $bclConvertSettingsSection->performAnalysisOn(AnalysisLocation::LOCAL_MACHINE()); + + $expected = <<<'CSV' +FastqCompressionFormat,gzip +GenerateFastqcMetrics,true + +CSV; + self::assertSame($expected, $bclConvertSettingsSection->convertSectionToString()); + } +} diff --git a/tests/IlluminaSampleSheet/V2/DataSectionTest.php b/tests/IlluminaSampleSheet/V2/DataSectionTest.php deleted file mode 100644 index af44ba2e..00000000 --- a/tests/IlluminaSampleSheet/V2/DataSectionTest.php +++ /dev/null @@ -1,58 +0,0 @@ -addSample(new BclSample(100, 'Sample1', 'Index1', $overrideCycles)); - - $overrideCycles = new OverrideCycles($dataSection, 'Y100', 'I11', 'I7', 'Y151'); - $dataSection->addSample(new BclSample(101, 'Sample2', 'Index3', $overrideCycles)); - - $expected = <<<'CSV' -[BCLConvert_Data] -Lane,Sample_ID,Index,OverrideCycles -100,Sample1,Index1,Y130;I8N3;I10;Y100N51 -101,Sample2,Index3,Y100N30;I11;N3I7;Y151 - -CSV; - self::assertSame($expected, $dataSection->convertSectionToString()); - } - - public function testThrowsExceptionIfDataSectionIsEmpty(): void - { - $dataSection = new DataSection(); - - $this->expectException(IlluminaSampleSheetException::class); - $this->expectExceptionMessage('At least one sample must be added to the DataSection.'); - $dataSection->convertSectionToString(); - } - - public function testToStringWithProject(): void - { - $dataSection = new DataSection(); - $overrideCycles = new OverrideCycles($dataSection, 'Y130', 'I8', 'I10', 'Y100'); - $bclSample = new BclSample(100, 'Sample1', 'Index1', $overrideCycles); - $bclSample->project = 'foo'; - $dataSection->addSample($bclSample); - - $expected = <<<'CSV' -[BCLConvert_Data] -Lane,Sample_ID,Index,OverrideCycles,Project -100,Sample1,Index1,Y130;I8;I10;Y100,foo - -CSV; - - self::assertSame($expected, $dataSection->convertSectionToString()); - } -} diff --git a/tests/IlluminaSampleSheet/V2/HeaderSectionTest.php b/tests/IlluminaSampleSheet/V2/HeaderSectionTest.php new file mode 100644 index 00000000..a3da9dc4 --- /dev/null +++ b/tests/IlluminaSampleSheet/V2/HeaderSectionTest.php @@ -0,0 +1,51 @@ +convertSectionToString()); + } + + public function testToStringLocal(): void + { + $headerSection = new HeaderSection( + 'Test1234', + IndexOrientation::FORWARD(), + InstrumentPlatform::NOVASEQ_X_SERIES(), + null + ); + $headerSection->performAnalysisOnLocalMachine(); + + $expected = <<<'CSV' +FileFormatVersion,2 +RunName,Test1234 +IndexOrientation,Forward +InstrumentPlatform,NovaSeqXSeries +AnalysisLocation,Local + +CSV; + self::assertSame($expected, $headerSection->convertSectionToString()); + } +} diff --git a/tests/IlluminaSampleSheet/V2/IlluminaSampleSheetVersion2Test.php b/tests/IlluminaSampleSheet/V2/IlluminaSampleSheetVersion2Test.php new file mode 100644 index 00000000..6c41cd5e --- /dev/null +++ b/tests/IlluminaSampleSheet/V2/IlluminaSampleSheetVersion2Test.php @@ -0,0 +1,153 @@ +overrideCycleCounter); + + $sampleSheet = new IlluminaSampleSheetVersion2( + $headerSection, + $readsSection, + $bclConvertSettingsSection, + $bclConvertDataSection, + new CloudSettingsSection(), + new CloudDataSection(new Collection([ + new CloudDataItem($bclSample0->sampleID, 'test', 'foo'), + new CloudDataItem($bclSample1->sampleID, 'test', 'foo'), + new CloudDataItem($bclSample2->sampleID, 'test', 'foo'), + new CloudDataItem($bclSample3->sampleID, 'test', 'foo'), + ])) + ); + + $expected = '[Header] +FileFormatVersion,2 +RunName,Run1 +IndexOrientation,Forward +InstrumentPlatform,NovaSeqXSeries + +[Reads] +Read1Cycles,151 +Read2Cycles,151 +Index1Cycles,8 +Index2Cycles,10 + +[BCLConvert_Settings] +FastqCompressionFormat,gzip +SoftwareVersion,4.1.23 + +[BCLConvert_Data] +Lane,Sample_ID,Index,Index2,OverrideCycles,AdapterRead1,AdapterRead2,BarcodeMismatchesIndex1,BarcodeMismatchesIndex2 +1,Sample1,Index1,Index2,U7N1Y143;I8;N2I8;U7N1Y143,Adapter1,Adapter2,0,0 +2,Sample2,Index3,Index4,Y151;I8;I10;Y151,Adapter3,Adapter4,0,0 +1,Sample3,Index5,Index6,Y151;I8;N2I8;U10N12Y127N2,Adapter5,Adapter6,0,0 +2,Sample3,Index5,Index6,Y151;I8;N2I8;U10N12Y127N2,Adapter5,Adapter6,0,0 +1,Sample4,Index5,Index6,Y101N50;I8;N2I8;Y101N50,Adapter5,Adapter6,1,1 +2,Sample4,Index5,Index6,Y101N50;I8;N2I8;Y101N50,Adapter5,Adapter6,1,1 + +[Cloud_Settings] +GeneratedVersion,2.6.0.202308300002 +Cloud_Workflow,ica_workflow_1 +BCLConvert_Pipeline,urn:ilmn:ica:pipeline:d5c7e407-d439-48c8-bce5-b7aec225f6a7#BclConvert_v4_1_23_patch1 + +[Cloud_Data] +Sample_ID,ProjectName,LibraryName +Sample1,test,foo +Sample2,test,foo +Sample3,test,foo +Sample4,test,foo +'; + self::assertSame($expected, $sampleSheet->toString()); + } +} diff --git a/tests/IlluminaSampleSheet/V2/NovaSeqXCloudSampleSheetTest.php b/tests/IlluminaSampleSheet/V2/NovaSeqXCloudSampleSheetTest.php deleted file mode 100644 index 774cd125..00000000 --- a/tests/IlluminaSampleSheet/V2/NovaSeqXCloudSampleSheetTest.php +++ /dev/null @@ -1,105 +0,0 @@ -instrumentPlatform = 'NovaSeqXSeries'; - $headerSection->setCustomParam('TestKey', 'TestValue'); - - $bclConvertSettingsSection = new SettingsSection('1.0.0', FastQCompressionFormat::GZIP()); - $bclConvertSettingsSection->trimUMI = false; - - $bclConvertDataSection = new DataSection(); - - $overrideCycles = new OverrideCycles($bclConvertDataSection, 'U7N1Y143', 'I8', 'I8', 'U7N1Y143'); - $bclSample = new BclSample(1, 'Sample1', 'Index1', $overrideCycles); - $bclSample->index2 = 'Index2'; - - $bclSample->adapterRead1 = 'Adapter1'; - $bclSample->adapterRead2 = 'Adapter2'; - - $overrideCycles1 = new OverrideCycles($bclConvertDataSection, 'Y151', 'I8', 'U10', 'Y151'); - $bclSample1 = new BclSample(2, 'Sample2', 'Index3', $overrideCycles1); - $bclSample1->index2 = 'Index4'; - - $bclSample1->adapterRead1 = 'Adapter3'; - $bclSample1->adapterRead2 = 'Adapter4'; - - $overrideCycles2 = new OverrideCycles($bclConvertDataSection, 'Y151', 'I8', 'I8', 'U10N12Y127'); - $bclSample2 = new BclSample(3, 'Sample3', 'Index5', $overrideCycles2); - $bclSample2->index2 = 'Index6'; - - $bclSample2->adapterRead1 = 'Adapter5'; - $bclSample2->adapterRead2 = 'Adapter6'; - - $overrideCycles3 = new OverrideCycles($bclConvertDataSection, 'Y101', 'I8', 'I8', 'Y101'); - $bclSample3 = new BclSample(8, 'Sample4', 'Index5', $overrideCycles3); - $bclSample3->index2 = 'Index6'; - - $bclSample3->adapterRead1 = 'Adapter5'; - $bclSample3->adapterRead2 = 'Adapter6'; - - $overrideCycles4 = new OverrideCycles($bclConvertDataSection, 'U5N2Y94', 'I6', 'I8', 'U5N2Y94'); - $bclSample4 = new BclSample(6, 'Sample5', 'Index5', $overrideCycles4); - $bclSample4->index2 = 'Index6'; - - $bclSample4->adapterRead1 = 'Adapter5'; - $bclSample4->adapterRead2 = 'Adapter6'; - - $bclConvertDataSection->addSample($bclSample); - $bclConvertDataSection->addSample($bclSample1); - $bclConvertDataSection->addSample($bclSample2); - $bclConvertDataSection->addSample($bclSample3); - $bclConvertDataSection->addSample($bclSample4); - - $bclConvertSection = new BclConvertSection($bclConvertSettingsSection, $bclConvertDataSection); - - $novaSeqXCloudSampleSheet = new NovaSeqXSampleSheet( - $headerSection, - $bclConvertSection, - ); - - $expected = '[Header] -FileFormatVersion,2 -RunName,Run1 -IndexOrientation,Forward -InstrumentPlatform,NovaSeqXSeries -Custom_TestKey,TestValue - -[Reads] -Read1Cycles,151 -Read2Cycles,151 -Index1Cycles,8 -Index2Cycles,10 - -[BCLConvert_Settings] -SoftwareVersion,1.0.0 -FastqCompressionFormat,gzip -TrimUMI,0 - -[BCLConvert_Data] -Lane,Sample_ID,Index,Index2,OverrideCycles,AdapterRead1,AdapterRead2 -1,Sample1,Index1,Index2,U7N1Y143;I8;N2I8;U7N1Y143,Adapter1,Adapter2 -2,Sample2,Index3,Index4,Y151;I8;U10;Y151,Adapter3,Adapter4 -3,Sample3,Index5,Index6,Y151;I8;N2I8;U10N12Y127N2,Adapter5,Adapter6 -8,Sample4,Index5,Index6,Y101N50;I8;N2I8;Y101N50,Adapter5,Adapter6 -6,Sample5,Index5,Index6,U5N2Y94N50;I6N2;N2I8;U5N2Y94N50,Adapter5,Adapter6 -'; - - self::assertSame($expected, $novaSeqXCloudSampleSheet->toString()); - } -} diff --git a/tests/IlluminaSampleSheet/V2/OverrideCycleTest.php b/tests/IlluminaSampleSheet/V2/OverrideCycleTest.php new file mode 100644 index 00000000..a29a79d7 --- /dev/null +++ b/tests/IlluminaSampleSheet/V2/OverrideCycleTest.php @@ -0,0 +1,83 @@ +cycleTypeWithCountList); + self::assertSame(251, $overrideCycle->cycleTypeWithCountList[0]->count); + + $overrideCycle = OverrideCycle::fromString('I6', IndexOrientation::FORWARD()); + self::assertCount(1, $overrideCycle->cycleTypeWithCountList); + self::assertSame(6, $overrideCycle->cycleTypeWithCountList[0]->count); + + $overrideCycle = OverrideCycle::fromString('U5N2Y94', IndexOrientation::FORWARD()); + self::assertCount(3, $overrideCycle->cycleTypeWithCountList); + self::assertSame(5, $overrideCycle->cycleTypeWithCountList[0]->count); + self::assertSame(2, $overrideCycle->cycleTypeWithCountList[1]->count); + self::assertSame(94, $overrideCycle->cycleTypeWithCountList[2]->count); + } + + /** + * @param array{string, NucleotideType} $cycleStringAndNucleotideType + * + * @dataProvider provideCasesForFillUpTest + */ + #[DataProvider('provideCasesForFillUpTest')] + public function testFillUp(array $cycleStringAndNucleotideType, int $diff, IndexOrientation $indexOrientation, string $expected): void + { + [$cycleString, $nucleotideType] = $cycleStringAndNucleotideType; + $overrideCycle = OverrideCycle::fromString($cycleString, $indexOrientation); + $total = $overrideCycle->sumCountOfAllCycles() + $diff; + + self::assertSame( + $expected, + $overrideCycle + ->fillUpTo($total, $nucleotideType) + ->toString() + ); + } + + public function testFillUpDoesNotMutateOriginal(): void + { + $overrideCycle = OverrideCycle::fromString('U5N2Y94', IndexOrientation::FORWARD()); + $originalSum = $overrideCycle->sumCountOfAllCycles(); + + $overrideCycle->fillUpTo($originalSum + 4, new NucleotideType(NucleotideType::R1)); + + self::assertSame($originalSum, $overrideCycle->sumCountOfAllCycles()); + self::assertCount(3, $overrideCycle->cycleTypeWithCountList); + } + + /** @return iterable */ + public static function provideCasesForFillUpTest(): iterable + { + yield 'R1 diff in length' => [ + ['U5N2Y94', new NucleotideType(NucleotideType::R1)], 4, IndexOrientation::FORWARD(), 'U5N2Y94N4', + ]; + yield 'I1 diff in length' => [ + ['I6', new NucleotideType(NucleotideType::I1)], 2, IndexOrientation::FORWARD(), 'I6N2', + ]; + yield 'R1 UMI diff in length' => [ + ['U4N2Y98', new NucleotideType(NucleotideType::R1)], 1, IndexOrientation::FORWARD(), 'U4N2Y98N1', + ]; + yield 'R2 diff in length' => [ + ['Y241', new NucleotideType(NucleotideType::R2)], 10, IndexOrientation::FORWARD(), 'Y241N10', + ]; + yield 'I2 diff in length - IndexOrientation Forward' => [ + ['I6', new NucleotideType(NucleotideType::I2)], 2, IndexOrientation::FORWARD(), 'N2I6', + ]; + yield 'I2 diff in length - IndexOrientation Reverse' => [ + ['I6', new NucleotideType(NucleotideType::I2)], 2, IndexOrientation::REVERSE(), 'I6N2', + ]; + } +} diff --git a/tests/IlluminaSampleSheet/V2/OverrideCyclesTest.php b/tests/IlluminaSampleSheet/V2/OverrideCyclesTest.php new file mode 100644 index 00000000..f0b97ad4 --- /dev/null +++ b/tests/IlluminaSampleSheet/V2/OverrideCyclesTest.php @@ -0,0 +1,81 @@ + $overrideCyclesList + * + * @dataProvider provideCasesForFromStringToString + */ + #[DataProvider('provideCasesForFromStringToString')] + public function testFromStringToString(string $overrideCyclesAsString, Collection $overrideCyclesList, IndexOrientation $indexOrientation, string $expected): void + { + $overrideCycleCounter = new OverrideCycleCounter( + $overrideCyclesList->map(fn (string $overrideCycleAsString): OverrideCycles => OverrideCycles::fromString($overrideCycleAsString, $indexOrientation)) + ); + $overrideCycles = OverrideCycles::fromString($overrideCyclesAsString, $indexOrientation); + self::assertSame($expected, $overrideCycles->toString($overrideCycleCounter)); + } + + /** @return iterable, IndexOrientation, string}> */ + public static function provideCasesForFromStringToString(): iterable + { + yield 'L1 diff in length' => [ + 'U5N2Y94;I6;I8;Y251', + new Collection(['U5N2Y94;I6;I8;Y251', 'U5N2Y94;I8;I8;Y251']), + IndexOrientation::FORWARD(), + 'U5N2Y94;I6N2;I8;Y251', + ]; + + yield 'R1 read diff in length' => [ + 'U5N2Y94;I8;I8;Y251', + new Collection(['U5N2Y94;I8;I8;Y251', 'U5N2Y98;I8;I8;Y251']), + IndexOrientation::FORWARD(), + 'U5N2Y94N4;I8;I8;Y251', + ]; + + yield 'R1 UMI diff in length' => [ + 'U4N2Y98;I8;I8;Y251', + new Collection(['U4N2Y98;I8;I8;Y251', 'U5N2Y98;I6;I8;Y251']), + IndexOrientation::FORWARD(), + 'U4N2Y98N1;I8;I8;Y251', + ]; + + yield 'R2 read diff in length' => [ + 'U5N2Y98;I8;I8;Y241', + new Collection(['U5N2Y98;I8;I8;Y241', 'U5N2Y98;I8;I8;Y251']), + IndexOrientation::FORWARD(), + 'U5N2Y98;I8;I8;Y241N10', + ]; + + yield 'I2 Changed - Index Forward' => [ + 'U5N2Y98;I8;I6;Y251', + new Collection(['U5N2Y98;I8;I6;Y251', 'U5N2Y98;I8;I8;Y251']), + IndexOrientation::FORWARD(), + 'U5N2Y98;I8;N2I6;Y251', + ]; + + yield 'I2 Changed - Index Reverse' => [ + 'U5N2Y98;I8;I6;Y251', + new Collection(['U5N2Y98;I8;I6;Y251', 'U5N2Y98;I8;I8;Y251']), + IndexOrientation::REVERSE(), + 'U5N2Y98;I8;I6N2;Y251', + ]; + + yield 'R1 changed, I1 Changed, I2 Changed, R2 Changed' => [ + 'U4N2Y98;I6;I6;Y251', + new Collection(['U4N2Y98;I6;I8;Y251', 'U5N2Y100;I8;I6;Y241']), + IndexOrientation::REVERSE(), + 'U4N2Y98N3;I6N2;I6N2;Y251', + ]; + } +} diff --git a/tests/Tecan/BasicCommands/CommentTest.php b/tests/Tecan/BasicCommands/CommentTest.php index 78d6d171..e3c7e49a 100644 --- a/tests/Tecan/BasicCommands/CommentTest.php +++ b/tests/Tecan/BasicCommands/CommentTest.php @@ -1,6 +1,6 @@