Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #6 from mll-lab/add_sectioned_microplate
Browse files Browse the repository at this point in the history
Add SectionedMicroplate
  • Loading branch information
simbig authored Nov 25, 2021
2 parents 0925cbd + add7982 commit aaa6c4f
Show file tree
Hide file tree
Showing 16 changed files with 645 additions and 168 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## v3.1.0

### Added

- Add ability to have sectioned microplates to group wells

## v3.0.0

### Added
Expand Down
107 changes: 107 additions & 0 deletions src/AbstractMicroplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php declare(strict_types=1);

namespace Mll\Microplate;

use Illuminate\Support\Collection;
use Mll\Microplate\Enums\FlowDirection;
use Mll\Microplate\Exceptions\UnexpectedFlowDirection;

/**
* @template TWell
* @template TCoordinateSystem of CoordinateSystem
*
* @phpstan-type WellsCollection Collection<string, TWell|null>
*/
abstract class AbstractMicroplate
{
public const EMPTY_WELL = null;

/**
* @var TCoordinateSystem
*/
public CoordinateSystem $coordinateSystem;

/**
* @param TCoordinateSystem $coordinateSystem
*/
public function __construct(CoordinateSystem $coordinateSystem)
{
$this->coordinateSystem = $coordinateSystem;
}

/**
* @return WellsCollection
*/
abstract public function wells(): Collection;

/**
* @param Coordinate<TCoordinateSystem> $coordinate
*
* @return TWell|null
*/
public function well(Coordinate $coordinate)
{
return $this->wells()[$coordinate->toString()];
}

/**
* @param Coordinate<TCoordinateSystem> $coordinate
*/
public function isWellEmpty(Coordinate $coordinate): bool
{
return self::EMPTY_WELL === $this->well($coordinate);
}

/**
* @return WellsCollection
*/
public function sortedWells(FlowDirection $flowDirection): Collection
{
return $this->wells()->sortBy(
/**
* @param TWell $value
*/
function ($value, string $key) use ($flowDirection): string {
switch ($flowDirection->getValue()) {
case FlowDirection::ROW:
return $key;
case FlowDirection::COLUMN:
$coordinate = Coordinate::fromString($key, $this->coordinateSystem);

return $coordinate->column . $coordinate->row;
// @codeCoverageIgnoreStart all Enums are listed and this should never happen
default:
throw new UnexpectedFlowDirection($flowDirection);
// @codeCoverageIgnoreEnd
}
},
SORT_NATURAL
);
}

/**
* @return Collection<string, null>
*/
public function freeWells(): Collection
{
return $this->wells()->filter(
/**
* @param TWell $value
*/
static fn ($value): bool => self::EMPTY_WELL === $value
);
}

/**
* @return Collection<string, TWell>
*/
public function filledWells(): Collection
{
return $this->wells()->filter(
/**
* @param TWell $value
*/
static fn ($value): bool => self::EMPTY_WELL !== $value
);
}
}
35 changes: 35 additions & 0 deletions src/AbstractSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types=1);

namespace Mll\Microplate;

use Illuminate\Support\Collection;

/**
* @template TSectionWell
*/
abstract class AbstractSection
{
/**
* @var SectionedMicroplate<TSectionWell, CoordinateSystem, static>
*/
public SectionedMicroplate $sectionedMicroplate;

/**
* @var Collection<TSectionWell|null>
*/
public Collection $sectionItems;

/**
* @param SectionedMicroplate<TSectionWell, CoordinateSystem, static> $sectionedMicroplate
*/
public function __construct(SectionedMicroplate $sectionedMicroplate)
{
$this->sectionedMicroplate = $sectionedMicroplate;
$this->sectionItems = new Collection();
}

/**
* @param TSectionWell $content
*/
abstract public function addWell($content): void;
}
5 changes: 2 additions & 3 deletions src/Coordinate.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,8 @@ public function position(FlowDirection $direction): int

private static function assertPositionInRange(CoordinateSystem $coordinateSystem, int $position): void
{
$maxPosition = count($coordinateSystem->columns()) * count($coordinateSystem->rows());
if (! in_array($position, range(self::MIN_POSITION, $maxPosition), true)) {
throw new InvalidArgumentException("Expected a position between 1-{$maxPosition}, got: {$position}.");
if (! in_array($position, range(self::MIN_POSITION, $coordinateSystem->positionsCount()), true)) {
throw new InvalidArgumentException("Expected a position between 1-{$coordinateSystem->positionsCount()}, got: {$position}.");
}
}
}
35 changes: 31 additions & 4 deletions src/CoordinateSystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,48 @@ abstract public function columns(): array;

public function rowForRowFlowPosition(int $position): string
{
return $this->rows()[floor(($position - 1) / count($this->columns()))];
return $this->rows()[floor(($position - 1) / $this->columnsCount())];
}

public function rowForColumnFlowPosition(int $position): string
{
return $this->rows()[($position - 1) % count($this->rows())];
return $this->rows()[($position - 1) % $this->rowsCount()];
}

public function columnForRowFlowPosition(int $position): int
{
return $this->columns()[($position - 1) % count($this->columns())];
return $this->columns()[($position - 1) % $this->columnsCount()];
}

public function columnForColumnFlowPosition(int $position): int
{
return $this->columns()[floor(($position - 1) / count($this->rows()))];
return $this->columns()[floor(($position - 1) / $this->rowsCount())];
}

public function positionsCount(): int
{
return $this->columnsCount() * $this->rowsCount();
}

/**
* @return iterable<int, Coordinate>
*/
public function all(): iterable
{
foreach ($this->columns() as $column) {
foreach ($this->rows() as $row) {
yield new Coordinate($row, $column, $this);
}
}
}

public function rowsCount(): int
{
return count($this->rows());
}

public function columnsCount(): int
{
return count($this->columns());
}
}
6 changes: 5 additions & 1 deletion src/Exceptions/MicroplateIsFullException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

namespace Mll\Microplate\Exceptions;

class MicroplateIsFullException extends \Exception
class MicroplateIsFullException extends \UnexpectedValueException
{
public function __construct()
{
parent::__construct('No free spots left on plate');
}
}
7 changes: 7 additions & 0 deletions src/Exceptions/SectionDoesNotExistException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types=1);

namespace Mll\Microplate\Exceptions;

class SectionDoesNotExistException extends \Exception
{
}
11 changes: 11 additions & 0 deletions src/Exceptions/SectionIsFullException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace Mll\Microplate\Exceptions;

class SectionIsFullException extends \UnexpectedValueException
{
public function __construct()
{
parent::__construct('No free spots left on section');
}
}
84 changes: 84 additions & 0 deletions src/FullColumnSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php declare(strict_types=1);

namespace Mll\Microplate;

use Mll\Microplate\Exceptions\MicroplateIsFullException;
use Mll\Microplate\Exceptions\SectionIsFullException;

/**
* A Section that occupies all wells of a column if one sample exists in this column. Samples of other sections are
* not allowed in this occupied wells. Occupied wells can still be filled with samples of the same type.
*
* @template TSectionWell
*/
class FullColumnSection extends AbstractSection
{
public function __construct(SectionedMicroplate $sectionedMicroplate)
{
parent::__construct($sectionedMicroplate);
$this->growSection();
}

/**
* @param TSectionWell $content
*
* @throws MicroplateIsFullException
* @throws SectionIsFullException
*/
public function addWell($content): void
{
if ($this->sectionedMicroplate->freeWells()->isEmpty()) {
throw new MicroplateIsFullException();
}

$nextReservedWell = $this->nextReservedWell();
if (false !== $nextReservedWell) {
$this->sectionItems[$nextReservedWell] = $content;

return;
}

$this->growSection();

/** @var int $nextReservedWell Guaranteed to be found after we grew the section */
$nextReservedWell = $this->nextReservedWell();
$this->sectionItems[$nextReservedWell] = $content;
}

/**
* Grows the section by initializing a new column with empty wells.
*
* @throws SectionIsFullException
*/
private function growSection(): void
{
if (! $this->sectionCanGrow()) {
throw new SectionIsFullException();
}

foreach ($this->sectionedMicroplate->coordinateSystem->rows() as $row) {
$this->sectionItems->push(AbstractMicroplate::EMPTY_WELL);
}
}

/**
* @return false|int
*/
private function nextReservedWell()
{
return $this->sectionItems->search(AbstractMicroplate::EMPTY_WELL);
}

private function sectionCanGrow(): bool
{
$totalReservedColumns = $this->sectionedMicroplate->sections->sum(fn (self $section) => $section->reservedColumns());
$availableColumns = $this->sectionedMicroplate->coordinateSystem->columnsCount();

return $totalReservedColumns < $availableColumns;
}

private function reservedColumns(): int
{
return (int) ceil($this->sectionItems->count() / $this->sectionedMicroplate->coordinateSystem->rowsCount());
}
}
Loading

0 comments on commit aaa6c4f

Please sign in to comment.