<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\tests;
use seekquarry\yioop\library\av_processing\Laplace;
use seekquarry\yioop\library\av_processing\RangeDecoder;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UnitTest;
/**
* Checks the reader for numbers that were likely to be small and near
* zero, which is how band loudnesses are stored.
*
* The awkward part is that the shape describing how likely each value
* was only runs so far out. Past that point every remaining value is
* treated alike, and the join between the two has to fall in the same
* place for the reader as it did for the writer. A number just either
* side of that join is where a fault would show, so the cases push out
* to it on purpose rather than only trying small numbers.
*
* @author Chris Pollett
*/
class LaplaceTest extends UnitTest
{
/**
* How much room to give a written piece in these cases
*/
const ROOM = 4000;
/**
* A chance of being zero in the middle of the range the real
* loudness models use
*/
const MIDDLE_ZERO_CHANCE = 12800;
/**
* A rate of falling away in the middle of that range
*/
const MIDDLE_FALL = 6400;
/**
* A starting point for the made up numbers
*/
const SEED = 20260803;
/**
* Brings in the writer these cases check against
*/
public function setUp()
{
if (!class_exists("seekquarry\\yioop\\tests\\RangeEncoder")) {
require_once C\PARENT_DIR . "/tests/test_files/RangeEncoder.php";
}
if (!class_exists("seekquarry\\yioop\\tests\\LaplaceWriter")) {
require_once C\PARENT_DIR . "/tests/test_files/LaplaceWriter.php";
}
mt_srand(self::SEED);
}
/**
* Nothing needs clearing away after these cases
*/
public function tearDown()
{
}
/**
* Writes a run of numbers and reads them back, saying how many
* came back as the writer stored them
*
* @param array $values the numbers to store
* @param int $zero_chance how much of the whole zero takes
* @param int $fall how fast the chance falls away
* @return array how many matched, how many were tried, and how
* many bytes the run took
*/
public function thereAndBack($values, $zero_chance, $fall)
{
$writer = new RangeEncoder(self::ROOM);
$stored = [];
foreach ($values as $value) {
$stored[] = LaplaceWriter::writeTo($writer, $value,
$zero_chance, $fall);
}
$piece = $writer->finish();
$reader = new RangeDecoder($piece);
$matched = 0;
foreach ($stored as $want) {
if (Laplace::readFrom($reader, $zero_chance, $fall) == $want) {
$matched++;
}
}
return [$matched, count($stored),
strlen(rtrim($piece, "\0"))];
}
/**
* Small numbers, which is what nearly every band loudness is,
* should come back exactly
*/
public function smallNumbersTestCase()
{
$values = [];
for ($i = 0; $i < 300; $i++) {
$values[] = mt_rand(-4, 4);
}
list($matched, $tried, $size) = $this->thereAndBack($values,
self::MIDDLE_ZERO_CHANCE, self::MIDDLE_FALL);
$this->assertEqual($matched, $tried,
"every small number came back as it went in");
}
/**
* Zero should come back as zero and should cost very little room,
* since it is the value the shape is built around
*/
public function zeroIsCheapTestCase()
{
$values = array_fill(0, 300, 0);
list($matched, $tried, $size) = $this->thereAndBack($values,
self::MIDDLE_ZERO_CHANCE, self::MIDDLE_FALL);
$this->assertEqual($matched, $tried, "every zero came back");
$spread = [];
for ($i = 0; $i < 300; $i++) {
$spread[] = mt_rand(-4, 4);
}
list($other, $tried, $spread_size) = $this->thereAndBack($spread,
self::MIDDLE_ZERO_CHANCE, self::MIDDLE_FALL);
$this->assertTrue($size < $spread_size,
"a run of zeroes takes less room than a spread of values");
}
/**
* Numbers far enough out that the shape has run out of room should
* still come back as whatever the writer settled on
*/
public function numbersPastTheShapeTestCase()
{
$values = [];
for ($i = 0; $i < 40; $i++) {
$values[] = mt_rand(-60, 60);
}
list($matched, $tried, $size) = $this->thereAndBack($values,
self::MIDDLE_ZERO_CHANCE, self::MIDDLE_FALL);
$this->assertEqual($matched, $tried,
"every far out number came back as the writer stored it");
}
/**
* The join between the part that follows the shape and the part
* beyond it should fall in the same place for reader and writer,
* so numbers marching outward one at a time should all come back
*/
public function marchingOutwardTestCase()
{
$values = [];
for ($size = 0; $size <= 40; $size++) {
$values[] = $size;
if ($size > 0) {
$values[] = -$size;
}
}
list($matched, $tried, $room) = $this->thereAndBack($values,
self::MIDDLE_ZERO_CHANCE, self::MIDDLE_FALL);
$this->assertEqual($matched, $tried,
"every step outward came back, $tried tried");
}
/**
* The shape is described by two numbers, and the reader has to
* follow whichever pair it is given, so a spread of pairs is tried
*/
public function manyShapesTestCase()
{
$wrong = 0;
$tried = 0;
foreach ([1000, 5000, 12800, 22000, 30000] as $zero_chance) {
foreach ([0, 2000, 6400, 12000, 16000] as $fall) {
$values = [];
for ($i = 0; $i < 30; $i++) {
$values[] = mt_rand(-12, 12);
}
list($matched, $count, $room) = $this->thereAndBack($values,
$zero_chance, $fall);
$wrong += $count - $matched;
$tried += $count;
}
}
$this->assertEqual($wrong, 0,
"every shape read back what it wrote, $tried numbers tried");
}
/**
* A shape that falls away faster should spend less room on small
* numbers and more on large ones, which is the whole point of
* being able to describe the shape
*/
public function fasterFallCostsLessOnSmallNumbersTestCase()
{
$values = array_fill(0, 200, 1);
list($matched, $tried, $slow_size) = $this->thereAndBack($values,
self::MIDDLE_ZERO_CHANCE, 2000);
list($matched, $tried, $fast_size) = $this->thereAndBack($values,
self::MIDDLE_ZERO_CHANCE, 14000);
$this->assertTrue($slow_size != $fast_size,
"the rate of falling away changes how much room is spent");
}
}