Skip to content

Commit 8eb7bc7

Browse files
authored
feat: Support creating Lightcycler Sample Sheets for Absolute Quantification (#55)
1 parent c42181c commit 8eb7bc7

19 files changed

+249
-29
lines changed

.php-cs-fixer.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@
99
->ignoreDotFiles(true)
1010
->ignoreVCS(true);
1111

12-
return risky($finder);
12+
$config = risky($finder);
13+
$config->setCacheFile(__DIR__ . '/.build/php-cs-fixer/cache');
14+
15+
return $config;

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases).
99

1010
## Unreleased
1111

12+
## v5.17.0
13+
14+
### Added
15+
16+
- Support creating Lightcycler Sample Sheets for Absolute Quantification https://github.com/mll-lab/php-utils/pull/55
17+
- Accept `iterable $data` in `CSVArray::toCSV` https://github.com/mll-lab/php-utils/pull/55
18+
1219
## v5.16.0
1320

1421
### Added

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ it: fix stan test ## Run the commonly used targets
33

44
.PHONY: help
55
help: ## Displays this list of targets with descriptions
6-
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}'
6+
@grep --extended-regexp '^[a-zA-Z0-9_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}'
77

88
setup: vendor ## Set up the repository
99

@@ -23,7 +23,7 @@ rector: vendor
2323
.PHONY: php-cs-fixer
2424
php-cs-fixer:
2525
mkdir --parents .build/php-cs-fixer
26-
vendor/bin/php-cs-fixer fix --cache-file=.build/php-cs-fixer/cache
26+
vendor/bin/php-cs-fixer fix
2727

2828
.PHONY: stan
2929
stan: vendor ## Runs a static analysis with phpstan
@@ -37,5 +37,5 @@ test: vendor ## Runs auto-review, unit, and integration tests with phpunit
3737

3838
vendor: composer.json
3939
composer validate --strict
40-
composer install
40+
composer update
4141
composer normalize

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ parameters:
1818
- '#Unsafe usage of new static.*#'
1919
# Not in older PHPUnit versions
2020
- '#Attribute class PHPUnit\\Framework\\Attributes\\DataProvider does not exist\.#'
21+
# Allows assert() calls to assist IDE autocompletion
22+
- message: '#Call to function assert\(\) with true.* will always evaluate to true\.#'

rector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitSelfCallRector::class,
2525
])
2626
->withSkip([
27-
Rector\PHPUnit\CodeQuality\Rector\MethodCall\AssertCountWithZeroToAssertEmptyRector::class, // sloppy
27+
Rector\PHPUnit\CodeQuality\Rector\Class_\RemoveDataProviderParamKeysRector::class, // breaks tests
2828
Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector::class, // breaks tests
2929
Rector\CodeQuality\Rector\Concat\JoinStringConcatRector::class => [
3030
__DIR__ . '/tests/CSVArrayTest.php', // keep `\r\n` for readability

src/CSVArray.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,18 @@ public static function toArray(string $csv, string $delimiter = ';', string $enc
4646
return $result;
4747
}
4848

49-
/** @param array<int, array<string, CSVPrimitive>> $data */
50-
public static function toCSV(array $data, string $delimiter = ';', string $lineSeparator = "\r\n"): string
49+
/** @param iterable<array<string, CSVPrimitive>> $data */
50+
public static function toCSV(iterable $data, string $delimiter = ';', string $lineSeparator = StringUtil::WINDOWS_NEWLINE): string
5151
{
52-
if ($data === []) {
53-
throw new \Exception('Array is empty');
54-
}
55-
5652
// Use the keys of the array as the headers of the CSV
5753
$headerItem = Arr::first($data);
58-
if (! is_array($headerItem)) {
59-
throw new \Exception('Missing column headers.');
54+
if ($headerItem === null) {
55+
throw new \Exception('Expected $data to contain at least one item.');
6056
}
57+
assert(
58+
is_array($headerItem), // @phpstan-ignore function.alreadyNarrowedType (necessary for older PHPStan/Illuminate versions)
59+
'Expected $data to contain arrays.'
60+
);
6161
$headerKeys = array_keys($headerItem);
6262

6363
$content = str_putcsv($headerKeys, $delimiter) . $lineSeparator;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\Utils\LightcyclerSampleSheet;
4+
5+
use MLL\Utils\Microplate\Coordinates;
6+
use MLL\Utils\Microplate\CoordinateSystem12x8;
7+
8+
class AbsoluteQuantificationSample
9+
{
10+
public string $sampleName;
11+
12+
public string $filterCombination;
13+
14+
public string $hexColor;
15+
16+
public string $sampleType;
17+
18+
/** Key used to determine replication grouping - samples with the same key will replicate to the first occurrence */
19+
public string $replicationOfKey;
20+
21+
public ?int $concentration;
22+
23+
public function __construct(
24+
string $sampleName,
25+
string $filterCombination,
26+
string $hexColor,
27+
string $sampleType,
28+
string $replicationOfKey,
29+
?int $concentration
30+
) {
31+
$this->sampleName = $sampleName;
32+
$this->filterCombination = $filterCombination;
33+
$this->hexColor = $hexColor;
34+
$this->sampleType = $sampleType;
35+
$this->replicationOfKey = $replicationOfKey;
36+
$this->concentration = $concentration;
37+
}
38+
39+
public static function formatConcentration(?int $concentration): ?string
40+
{
41+
if ($concentration === null) {
42+
return null;
43+
}
44+
45+
$exponent = (int) floor(log10(abs($concentration)));
46+
$mantissa = $concentration / (10 ** $exponent);
47+
48+
return number_format($mantissa, 2) . 'E' . $exponent;
49+
}
50+
51+
/** @return array<string, string|null> */
52+
public function toSerializableArray(string $coordinatesString, string $replicationOfCoordinate): array
53+
{
54+
return [
55+
'General:Pos' => Coordinates::fromString($coordinatesString, new CoordinateSystem12x8())->toString(),
56+
'General:Sample Name' => $this->sampleName,
57+
'General:Repl. Of' => $replicationOfCoordinate,
58+
'General:Filt. Comb.' => $this->filterCombination,
59+
'Sample Preferences:Color' => RandomHexGenerator::LIGHTCYCLER_COLOR_PREFIX . $this->hexColor,
60+
'Abs Quant:Sample Type' => $this->sampleType,
61+
'Abs Quant:Concentration' => self::formatConcentration($this->concentration),
62+
];
63+
}
64+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\Utils\LightcyclerSampleSheet;
4+
5+
use Illuminate\Support\Collection;
6+
use MLL\Utils\CSVArray;
7+
8+
class AbsoluteQuantificationSheet
9+
{
10+
/** @param Collection<string, AbsoluteQuantificationSample> $samples */
11+
public function generate(Collection $samples): string
12+
{
13+
$replicationMapping = $this->calculateReplicationMapping($samples);
14+
15+
$data = $samples->map(fn (AbsoluteQuantificationSample $well, string $coordinateFromKey): array => $well->toSerializableArray(
16+
$coordinateFromKey,
17+
$replicationMapping[$coordinateFromKey]
18+
));
19+
20+
return CSVArray::toCSV($data, "\t");
21+
}
22+
23+
/**
24+
* Calculate replication mapping based on replicationOfKey.
25+
*
26+
* @param Collection<string, AbsoluteQuantificationSample> $samples
27+
*
28+
* @return array<string, string> a map of coordinate -> replicationOfCoordinate
29+
*/
30+
private function calculateReplicationMapping(Collection $samples): array
31+
{
32+
$replicationKeyMap = [];
33+
$mapping = [];
34+
35+
foreach ($samples as $coordinate => $sample) {
36+
if (! isset($replicationKeyMap[$sample->replicationOfKey])) {
37+
// The First occurrence replicates to itself
38+
$replicationKeyMap[$sample->replicationOfKey] = $coordinate;
39+
$mapping[$coordinate] = $coordinate;
40+
} else {
41+
// Later occurrences replicate to the first occurrence
42+
$firstOccurrenceCoordinate = $replicationKeyMap[$sample->replicationOfKey];
43+
$mapping[$coordinate] = $firstOccurrenceCoordinate;
44+
}
45+
}
46+
47+
return $mapping;
48+
}
49+
}

src/LightcyclerSampleSheet/RandomHexGenerator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
class RandomHexGenerator
66
{
7+
public const LIGHTCYCLER_COLOR_PREFIX = '$00';
8+
79
/** @var list<string> */
810
private array $generatedHexCodes = [];
911

src/LightcyclerSampleSheet/RelativeQuantificationSample.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function toSerializableArray(string $coordinatesString): array
4141
"\"{$this->sampleName}\"",
4242
$replicationOf,
4343
$this->filterCombination,
44-
"$00{$this->hexColor}",
44+
RandomHexGenerator::LIGHTCYCLER_COLOR_PREFIX . $this->hexColor,
4545
];
4646
}
4747
}

0 commit comments

Comments
 (0)