/ tests / LaplaceTest.php
<?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");
    }
}
X