Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/Messages/Util/ModalityCombinationsUtil.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Messages\Util;

use WordPress\AiClient\Messages\Enums\ModalityEnum;

/**
* Utility class for building modality combinations.
*
* @since n.e.x.t
*/
class ModalityCombinationsUtil
{
/**
* Builds all modality combinations that always include the required modalities
* with every possible subset of the optional modalities.
*
* Uses binary representation to enumerate all 2^n subsets of the optional
* modalities. Each subset is merged with the required modalities to form one
* complete combination. Modality lists are unordered, so only unique
* combinations (not permutations) are produced.
*
* @since n.e.x.t
*
* @param list<ModalityEnum> $required Required modalities included in every combination.
* @param list<ModalityEnum> $optional Optional modalities to generate subsets from.
* @return list<list<ModalityEnum>> List of modality combinations, each containing
* all required modalities plus a unique subset of
* the optional modalities.
*/
public static function buildCombinations(array $required, array $optional): array
{
$combinations = [];
$count = count($optional);
$subsetCount = 1 << $count; // 2^count.

for ($i = 0; $i < $subsetCount; $i++) {
$combo = $required;

for ($j = 0; $j < $count; $j++) {
if ($i & (1 << $j)) {
$combo[] = $optional[$j];
}
}

$combinations[] = $combo;
}

return $combinations;
}
}
171 changes: 171 additions & 0 deletions tests/unit/Messages/Util/ModalityCombinationsUtilTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tests\unit\Messages\Util;

use PHPUnit\Framework\TestCase;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Messages\Util\ModalityCombinationsUtil;

/**
* @covers \WordPress\AiClient\Messages\Util\ModalityCombinationsUtil
*/
class ModalityCombinationsUtilTest extends TestCase
{
/**
* Tests that buildCombinations returns the correct number of combinations.
*
* @dataProvider combinationCountProvider
* @param list<ModalityEnum> $required The required modalities.
* @param list<ModalityEnum> $optional The optional modalities.
* @param int $expectedCount The expected number of combinations.
* @return void
*/
public function testBuildCombinationsReturnsCorrectCount(
array $required,
array $optional,
int $expectedCount
): void {
$combinations = ModalityCombinationsUtil::buildCombinations($required, $optional);

$this->assertCount($expectedCount, $combinations);
}

/**
* Provides data for combination count tests.
*
* @return array<string, array{list<ModalityEnum>, list<ModalityEnum>, int}>
*/
public function combinationCountProvider(): array
{
return [
'both empty' => [[], [], 1],
'only required' => [[ModalityEnum::text()], [], 1],
'only optional one' => [[], [ModalityEnum::text()], 2],
'only optional two' => [[], [ModalityEnum::text(), ModalityEnum::audio()], 4],
'required and one optional' => [[ModalityEnum::text()], [ModalityEnum::image()], 2],
'required and two optional' => [
[ModalityEnum::text()],
[ModalityEnum::image(), ModalityEnum::audio()],
4,
],
'required and four optional' => [
[ModalityEnum::text()],
[ModalityEnum::image(), ModalityEnum::audio(), ModalityEnum::document(), ModalityEnum::video()],
16,
],
];
}

/**
* Tests that every combination always contains all required modalities.
*
* @return void
*/
public function testBuildCombinationsAlwaysIncludesRequiredModalities(): void
{
$required = [ModalityEnum::text()];
$optional = [ModalityEnum::image(), ModalityEnum::audio()];

$combinations = ModalityCombinationsUtil::buildCombinations($required, $optional);

foreach ($combinations as $combo) {
$this->assertContains(ModalityEnum::text(), $combo);
}
}

/**
* Tests that buildCombinations produces no duplicate combinations.
*
* @return void
*/
public function testBuildCombinationsProducesNoDuplicates(): void
{
$required = [ModalityEnum::text()];
$optional = [ModalityEnum::image(), ModalityEnum::audio(), ModalityEnum::document()];

$combinations = ModalityCombinationsUtil::buildCombinations($required, $optional);

// Normalise each combo to a sorted value string for de-duplication comparison.
$normalised = array_map(
static function (array $combo): string {
$values = array_map(
static function (ModalityEnum $m): string {
return $m->value;
},
$combo
);
sort($values);
return implode(',', $values);
},
$combinations
);

$this->assertCount(count($normalised), array_unique($normalised));
}

/**
* Tests that the combination with no optional modalities is always present.
*
* @return void
*/
public function testBuildCombinationsIncludesRequiredOnlyCombination(): void
{
$required = [ModalityEnum::text()];
$optional = [ModalityEnum::image(), ModalityEnum::audio()];

$combinations = ModalityCombinationsUtil::buildCombinations($required, $optional);

// The first combination produced by the bitmask loop (i=0) is always required-only.
$firstCombo = $combinations[0];
$this->assertCount(count($required), $firstCombo);
$this->assertContains(ModalityEnum::text(), $firstCombo);
}

/**
* Tests that both required and optional empty returns a single empty combination.
*
* @return void
*/
public function testBuildCombinationsBothEmptyReturnsSingleEmptyCombo(): void
{
$combinations = ModalityCombinationsUtil::buildCombinations([], []);

$this->assertCount(1, $combinations);
$this->assertSame([], $combinations[0]);
}

/**
* Tests that optional modalities each appear in exactly half the combinations.
*
* With k optional modalities there are 2^k subsets, and each specific optional
* modality appears in exactly 2^(k-1) of them.
*
* @return void
*/
public function testBuildCombinationsEachOptionalAppearsInExactlyHalfTheCombinations(): void
{
$optional = [ModalityEnum::image(), ModalityEnum::audio(), ModalityEnum::document()];
$combinations = ModalityCombinationsUtil::buildCombinations([], $optional);

$expectedAppearances = count($combinations) / 2; // 2^(k-1) = 4

foreach ($optional as $modality) {
$appearances = count(
array_filter(
$combinations,
static function (array $combo) use ($modality): bool {
return in_array($modality, $combo, true);
}
)
);

$this->assertSame(
(int) $expectedAppearances,
$appearances,
sprintf('Modality "%s" should appear in exactly half the combinations.', $modality->value)
);
}
}
}
Loading