[ADD] engine: end-to-end runs on the demonstrations, measured thresholds
The whole chain — search, both verifiers, measure, ceilings, ranking — runs end to end on the demonstrations. At 500 000 moves the large one reaches its proven floor: largest gap 1, no collision, no repeat. Its quality thresholds were set after measurement, with the measurement in the comment, never guessed. Checked: small at 8 for all and certified; conflict variant spreads its 4 collisions over 4 pairs; large at gap 1 with 99 imposed returns; the ranking holds when its order differs from id order. --- FR --- [ADD] moteur : la chaîne de bout en bout, seuils posés après mesure Toute la chaîne — recherche, les deux vérificateurs, mesure, plafonds, classement — tourne de bout en bout sur les démonstrations. À 500 000 mouvements, la grande atteint son plancher prouvé : écart maximal 1, aucune collision, aucune répétition. Ses seuils de qualité sont posés après mesure, la mesure en commentaire, jamais de tête. Vérifié : petite à 8 pour tous et certifiée ; variante en conflit à 4 collisions sur 4 paires ; grande à l'écart 1 avec 99 retours imposés ; le classement tient quand son ordre diffère de celui des identifiants. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
f8b3e8b0a9
commit
c7cd5f9142
2 changed files with 419 additions and 0 deletions
202
src/moteur/integration.long.test.js
Normal file
202
src/moteur/integration.long.test.js
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Épreuve d'intégration lourde du moteur, sur la grande démonstration
|
||||
// (§ 5.5, § 12.10.4, § 15.1). Trois propositions traversent la chaîne
|
||||
// d'integration.test.js, de rechercher au certificat. La configuration est
|
||||
// exactement tendue et impose ses planchers, que le diagnostic chiffre avant
|
||||
// la recherche : 99 retours imposés par les 33 ancrages ; un écart
|
||||
// d'itinéraire maximal d'au moins 1, porté par au moins 24 mobiles ; un
|
||||
// certificat « minimum atteint » hors d'atteinte. Les seuils de qualité sont
|
||||
// posés après mesure, la mesure en commentaire (§ 14.1).
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { CATALOGUE } from '../demo/catalogue.js';
|
||||
import { indexerPlan, normaliser } from './configuration.js';
|
||||
import { diagnostiquer } from './diagnostic.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
import {
|
||||
ecartsAuPlafondAPriori,
|
||||
ecartsItineraire,
|
||||
minimumAtteint,
|
||||
troisChiffres,
|
||||
} from './manque.js';
|
||||
import { plafondsAPriori, plafondsRealises } from './plafond.js';
|
||||
import { rechercher } from './recherche.js';
|
||||
import { verifierIndicateurs, verifierInvariants } from './verification.js';
|
||||
|
||||
// Statut d'un ancré dans instance.statut (§ 4.2) ; les autres sont mobiles.
|
||||
const ANCRE = 2;
|
||||
|
||||
// Compte d'arrêt choisi par la mesure. À 1 000 000 mouvements, chacune des
|
||||
// trois propositions atteint, sur chaque seuil ci-dessous, la meilleure
|
||||
// valeur que la configuration permette, sans rencontre répétée ni retour
|
||||
// choisi. Avec cette graine, chaque seuil tient à partir de son propre
|
||||
// compte :
|
||||
// - rencontres minimales 24, sur tous comme sur les ancrés : déjà à
|
||||
// 100 000 ;
|
||||
// - écart au plafond a priori maximal 1, et rencontres minimales 27 des
|
||||
// mobiles : dès 250 000 ; à 200 000, ils valent 2 et 26 ;
|
||||
// - manque maximal 0, aucun retour choisi : dès 350 000 ; à 300 000, une
|
||||
// proposition garde un manque de 1 et un retour choisi ;
|
||||
// - aucune collision : dès 450 000 ; à 400 000, les trois propositions en
|
||||
// portent 8, 1 et 2.
|
||||
// 1 000 000 vaut ainsi 4 fois le compte où tiennent l'écart au plafond a
|
||||
// priori et les mobiles, 2,9 fois celui du manque et des retours choisis,
|
||||
// 2,2 fois celui des collisions. Sur les graines 1 à 8, les vingt-quatre
|
||||
// propositions tiennent l'écart au plafond a priori et les mobiles dès
|
||||
// 250 000, le manque et les retours choisis dès 350 000, les collisions dès
|
||||
// 500 000 ; à 450 000, deux en portent encore 1 et 5.
|
||||
const REGLAGES = Object.freeze({ graine: 314_159, arret: 1_000_000, nombre: 3 });
|
||||
|
||||
// Seuils de qualité, posés après mesure ; chacun est la meilleure valeur
|
||||
// que la configuration permette.
|
||||
//
|
||||
// Rencontres minimales : 24 mesuré à chacune des trois propositions. C'est
|
||||
// le plafond a priori des quatre animateurs des tables de 7, qu'aucune
|
||||
// proposition ne dépasse (§ 15.1) : le minimum global ne peut pas le
|
||||
// dépasser, et l'épreuve exige qu'il l'atteigne. Les deux lignes
|
||||
// secondaires du § 5.4 se lisent à part : 24 chez les ancrés, que ces
|
||||
// quatre animateurs gouvernent ; 27 chez les mobiles. Un mobile assis une
|
||||
// fois à une table de 7 rencontre 27 personnes au plus, une de moins que
|
||||
// son plafond a priori de 28, et au moins 24 mobiles y passent (§ 15.1) ;
|
||||
// l'écart au plafond a priori maximal de 1 interdit à chacun d'en
|
||||
// rencontrer moins. Un agrégat « tous » qui écarterait les ancrés vaudrait
|
||||
// 27 (§ 5.4).
|
||||
const RENCONTRES_MIN = 24;
|
||||
const RENCONTRES_MIN_ANCRES = 24;
|
||||
const RENCONTRES_MIN_MOBILES = 27;
|
||||
// Écart au plafond a priori maximal : 1 mesuré à chacune des trois
|
||||
// propositions. Il ne descend pas sous l'écart d'itinéraire maximal, dont le
|
||||
// plancher vaut 1 (§ 12.10.4) : ce seuil aussi est la meilleure valeur
|
||||
// possible.
|
||||
const ECART_AU_PLAFOND_A_PRIORI_MAX = 1;
|
||||
// Collisions cumulées : 0 mesuré à chacune des trois propositions, la
|
||||
// meilleure valeur possible ; le diagnostic n'impose à cette configuration
|
||||
// aucun plancher de collisions. Une recherche qui ne sépare pas les
|
||||
// appartenances en laisse de 14 à 20 à chaque proposition.
|
||||
const COLLISIONS_CUMULEES_MAX = 0;
|
||||
|
||||
// Plus grand élément d'une liste non vide.
|
||||
const plusGrand = (valeurs) => valeurs.reduce((plus, valeur) => Math.max(plus, valeur));
|
||||
|
||||
// La génération et ses mesures, calculées au premier appel puis partagées
|
||||
// par les épreuves du fichier : la recherche en est le coût. Chaque
|
||||
// proposition porte la liste de ses violations ; ses mesures, ses plafonds
|
||||
// réalisés, ses trois chiffres et ses écarts d'itinéraire ne sont calculés
|
||||
// que pour un plan sans violation. indexerPlan n'examine ni capacités ni
|
||||
// réservations et lève à la première faute de forme, là où le vérificateur
|
||||
// les nomme toutes.
|
||||
let generation = null;
|
||||
function genererGrande() {
|
||||
if (generation !== null) return generation;
|
||||
const configuration = CATALOGUE.find(({ cle }) => cle === 'grande').construire();
|
||||
const instance = normaliser(configuration);
|
||||
const aPriori = plafondsAPriori(instance);
|
||||
const propositions = rechercher(configuration, REGLAGES).map(({ id, plan }) => {
|
||||
const violations = verifierInvariants(instance, plan);
|
||||
if (violations.length > 0) return { id, violations };
|
||||
const tableDe = indexerPlan(instance, plan);
|
||||
const mesures = mesurer(instance, tableDe);
|
||||
const realises = plafondsRealises(instance, tableDe);
|
||||
return {
|
||||
id,
|
||||
violations,
|
||||
mesures,
|
||||
realises,
|
||||
chiffres: troisChiffres(instance, mesures, aPriori, realises),
|
||||
ecartsItineraire: ecartsItineraire(aPriori, realises),
|
||||
};
|
||||
});
|
||||
generation = { instance, aPriori, diagnostic: diagnostiquer(configuration), propositions };
|
||||
return generation;
|
||||
}
|
||||
|
||||
// Les propositions de la génération, chacune jugée par verifierInvariants
|
||||
// avant toute lecture de ses mesures. Un parcours vide ne prouverait rien
|
||||
// (§ 14.2).
|
||||
function propositionsValides() {
|
||||
const { propositions } = genererGrande();
|
||||
assert.equal(propositions.length, REGLAGES.nombre);
|
||||
for (const { id, violations } of propositions) {
|
||||
assert.deepEqual(violations, [], `proposition ${id}`);
|
||||
}
|
||||
return propositions;
|
||||
}
|
||||
|
||||
describe('intégration : la grande démonstration de bout en bout (§ 5.5, § 12.10.4, § 15.1)', () => {
|
||||
test('chaque proposition tient invariants et indicateurs, porte les 99 retours imposés du diagnostic et aucun retour choisi', () => {
|
||||
const { aPriori, diagnostic } = genererGrande();
|
||||
// 33 ancrages de 4 tours, 3 retours chacun : attribués aux réservations,
|
||||
// les mêmes pour toute proposition (§ 5.4).
|
||||
assert.equal(diagnostic.retoursImposes, 99);
|
||||
for (const { id, mesures, realises } of propositionsValides()) {
|
||||
assert.deepEqual(verifierIndicateurs(mesures, aPriori, realises), [], `proposition ${id}`);
|
||||
assert.equal(mesures.totalRetoursImposes, diagnostic.retoursImposes, `proposition ${id}`);
|
||||
// Retours choisis : 0 mesuré à chacune des trois propositions, la
|
||||
// meilleure valeur possible ; avec cette graine, dès 350 000
|
||||
// mouvements (REGLAGES). Les deux comptes restent séparés, chacun à sa
|
||||
// valeur : leur somme afficherait 99 retours choisis à chaque
|
||||
// proposition, une constante où la différence entre propositions ne se
|
||||
// lit plus (§ 5.4, § 14.10).
|
||||
assert.equal(mesures.totalRetoursChoisis, 0, `proposition ${id}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("le plancher du diagnostic tient : écart d'itinéraire maximal ≥ 1, au moins 24 mobiles sous leur plafond a priori", () => {
|
||||
const { instance, diagnostic } = genererGrande();
|
||||
// Les tables de 7 offrent 96 sièges-tours mobiles, qu'un mobile n'occupe
|
||||
// que 4 fois au plus : au moins 24 mobiles y passent, et chacun y perd au
|
||||
// moins 1 sur son plafond a priori de 28 avant toute rencontre, son écart
|
||||
// d'itinéraire (§ 12.10.4). Sans ce plancher écrit, les comparaisons
|
||||
// ci-dessous n'éprouveraient rien.
|
||||
assert.deepEqual(diagnostic.ecartItineraire, { plancher: 1, mobilesAuMoins: 24 });
|
||||
const { plancher, mobilesAuMoins } = diagnostic.ecartItineraire;
|
||||
for (const { id, chiffres, ecartsItineraire: ecarts } of propositionsValides()) {
|
||||
assert.ok(
|
||||
chiffres.ecartItineraireMax.tous >= plancher,
|
||||
`proposition ${id} : écart d'itinéraire maximal ${chiffres.ecartItineraireMax.tous}, plancher ${plancher}`,
|
||||
);
|
||||
let mobilesSous = 0;
|
||||
for (let p = 0; p < instance.N; p += 1) {
|
||||
if (instance.statut[p] !== ANCRE && ecarts[p] >= plancher) mobilesSous += 1;
|
||||
}
|
||||
assert.ok(
|
||||
mobilesSous >= mobilesAuMoins,
|
||||
`proposition ${id} : ${mobilesSous} mobiles sous leur plafond a priori, au moins ${mobilesAuMoins} attendus`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('jamais « minimum atteint » (§ 15.1), même quand chacun atteint son plafond réalisé', () => {
|
||||
const { aPriori } = genererGrande();
|
||||
for (const { id, mesures, chiffres } of propositionsValides()) {
|
||||
// Manque maximal, seuil posé après mesure : 0 à chacune des trois
|
||||
// propositions, sur les 260 personnes. Chacun rencontre tout ce que son
|
||||
// itinéraire permet ; un certificat lu sur le seul manque serait
|
||||
// décerné, et il ne l'est pas : il se lit contre le plafond a priori
|
||||
// (§ 5.5).
|
||||
assert.equal(chiffres.manqueMax.tous, 0, `proposition ${id}`);
|
||||
assert.equal(minimumAtteint(mesures, aPriori), false, `proposition ${id}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('seuils de qualité posés après mesure : rencontres minimales et leurs lignes secondaires, écart au plafond a priori maximal, collisions cumulées', () => {
|
||||
const { aPriori } = genererGrande();
|
||||
for (const { id, mesures } of propositionsValides()) {
|
||||
const { tous, ancres, mobiles } = mesures.aggRencontres;
|
||||
assert.equal(tous.min, RENCONTRES_MIN, `proposition ${id} : rencontres minimales`);
|
||||
assert.equal(ancres.min, RENCONTRES_MIN_ANCRES, `proposition ${id} : rencontres minimales des ancrés`);
|
||||
assert.equal(mobiles.min, RENCONTRES_MIN_MOBILES, `proposition ${id} : rencontres minimales des mobiles`);
|
||||
const ecartMax = plusGrand(ecartsAuPlafondAPriori(mesures, aPriori));
|
||||
assert.ok(
|
||||
ecartMax <= ECART_AU_PLAFOND_A_PRIORI_MAX,
|
||||
`proposition ${id} : écart au plafond a priori maximal ${ecartMax}, seuil ${ECART_AU_PLAFOND_A_PRIORI_MAX}`,
|
||||
);
|
||||
assert.ok(
|
||||
mesures.collisionsCumulees <= COLLISIONS_CUMULEES_MAX,
|
||||
`proposition ${id} : ${mesures.collisionsCumulees} collisions cumulées, seuil ${COLLISIONS_CUMULEES_MAX}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
217
src/moteur/integration.test.js
Normal file
217
src/moteur/integration.test.js
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Épreuve d'intégration du moteur, de la configuration au classement, sur la
|
||||
// petite démonstration et sa variante conflit (§ 5.5, § 5.6, § 5.7,
|
||||
// § 15.3). Chaque proposition de rechercher traverse la chaîne entière :
|
||||
// verifierInvariants juge son plan ; mesurer et plafondsRealises le
|
||||
// mesurent ; verifierIndicateurs éprouve la ligne de chacun contre le
|
||||
// plafond a priori ; classer ordonne les propositions, et minimumAtteint lit
|
||||
// le certificat de la première ; diagnostiquer fournit les planchers que la
|
||||
// meilleure respecte. Chaque quantité vient de son module (§ 13.2) :
|
||||
// l'épreuve n'en recalcule aucune.
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { CATALOGUE, PLAN_PARFAIT_PETITE } from '../demo/catalogue.js';
|
||||
import { CRITERES, classer } from './classement.js';
|
||||
import { indexerPlan, normaliser } from './configuration.js';
|
||||
import { diagnostiquer } from './diagnostic.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
import { minimumAtteint } from './manque.js';
|
||||
import { plafondsAPriori, plafondsRealises } from './plafond.js';
|
||||
import { rechercher } from './recherche.js';
|
||||
import { verifierIndicateurs, verifierInvariants } from './verification.js';
|
||||
|
||||
// Réglages de la génération : une graine fixée, écrite ici (§ 15.5, point 2).
|
||||
// À 200 000 mouvements, avec cette graine, les trois propositions de chaque
|
||||
// démonstration rencontrent 8 pour les douze, sans rencontre répétée ni
|
||||
// retour choisi, et ne portent que les collisions que le diagnostic impose :
|
||||
// aucune dans la petite démonstration, 4 dans sa variante. Les assertions
|
||||
// des épreuves tiennent avec cette graine dès 80 000 mouvements : 200 000
|
||||
// leur laisse une marge de 2,5. Sur les graines 1 à 64, elles tiennent
|
||||
// toutes à 200 000 ; à 150 000, deux graines en font échouer.
|
||||
const REGLAGES = Object.freeze({ graine: 314_159, arret: 200_000, nombre: 3 });
|
||||
|
||||
// La même génération, arrêtée au premier mouvement. Aucune de ses
|
||||
// propositions n'atteint son plafond a priori : avec cette graine, leurs
|
||||
// écarts au plafond a priori maximaux valent 2, 3 et 3, et sur les graines
|
||||
// 1 à 64, aucune proposition brève ne l'atteint non plus.
|
||||
const REGLAGES_BREVES = Object.freeze({ ...REGLAGES, arret: 1 });
|
||||
|
||||
// Configuration neuve d'une démonstration du catalogue.
|
||||
const demo = (cle) => CATALOGUE.find((entree) => entree.cle === cle).construire();
|
||||
|
||||
// Génère les propositions d'une configuration et les fait traverser la
|
||||
// chaîne. Chaque plan est jugé par verifierInvariants avant d'être indexé :
|
||||
// indexerPlan n'examine ni capacités ni réservations et lève à la première
|
||||
// faute de forme, là où le vérificateur les nomme toutes. Vient ensuite la
|
||||
// ligne N − 1 ≥ a priori ≥ réalisé ≥ rencontres de chacun. decalage
|
||||
// s'ajoute aux identifiants que rend rechercher, comme l'appelant qui
|
||||
// accumule des générations donne à chaque proposition un identifiant unique
|
||||
// (§ 5.7). Rend le plafond a priori de chacun, et les propositions dans
|
||||
// l'ordre de leurs identifiants, avec leurs mesures.
|
||||
function generer(configuration, etiquette, reglages, decalage = 0) {
|
||||
const instance = normaliser(configuration);
|
||||
const aPriori = plafondsAPriori(instance);
|
||||
const propositions = rechercher(configuration, reglages).map(({ id, plan }) => {
|
||||
const nom = `${etiquette}, proposition ${id + decalage}`;
|
||||
assert.deepEqual(verifierInvariants(instance, plan), [], nom);
|
||||
const tableDe = indexerPlan(instance, plan);
|
||||
const mesures = mesurer(instance, tableDe);
|
||||
const realises = plafondsRealises(instance, tableDe);
|
||||
assert.deepEqual(verifierIndicateurs(mesures, aPriori, realises), [], nom);
|
||||
return { id: id + decalage, mesures };
|
||||
});
|
||||
// Un parcours vide ne prouverait rien (§ 14.2).
|
||||
assert.equal(propositions.length, reglages.nombre, etiquette);
|
||||
return { aPriori, propositions };
|
||||
}
|
||||
|
||||
// Le classement de propositions mesurées sur une même configuration, et
|
||||
// celle qu'il met en tête.
|
||||
function classerPropositions({ aPriori, propositions }) {
|
||||
const classement = classer(
|
||||
propositions.map(({ id, mesures }) => ({ id, mesures, plafondsAPriori: aPriori })),
|
||||
);
|
||||
const premiere = propositions.find(({ id }) => id === classement.ordre[0]);
|
||||
return { classement, premiere };
|
||||
}
|
||||
|
||||
// La génération de la variante conflit aux réglages de l'épreuve, calculée
|
||||
// au premier appel puis partagée par les deux épreuves qui la lisent : la
|
||||
// recherche en est le coût.
|
||||
let variante = null;
|
||||
function genererVariante() {
|
||||
variante ??= generer(demo('petite-conflit'), 'petite-conflit', REGLAGES);
|
||||
return variante;
|
||||
}
|
||||
|
||||
// Le plan écrit du § 15.3 fait visiter les quatre tables à chacun : aucun
|
||||
// retour choisi. Le classement ne lit pas les retours (§ 5.7) : entre
|
||||
// propositions égales sur ses cinq critères, l'identifiant désigne la
|
||||
// première, qu'elle garde des retours ou non. L'épreuve exige donc le zéro
|
||||
// d'une proposition au moins, la preuve que la recherche tient compte de
|
||||
// nouvelleTable.
|
||||
function exigerUneSansRetourChoisi(etiquette, propositions) {
|
||||
const retours = propositions.map(({ mesures }) => mesures.totalRetoursChoisis);
|
||||
assert.ok(retours.includes(0), `${etiquette} : retours choisis par proposition ${retours.join(', ')}`);
|
||||
}
|
||||
|
||||
describe('intégration : de la configuration au classement (§ 5.5, § 5.7, § 15.3)', () => {
|
||||
test('petite : chaque proposition tient invariants et indicateurs et ne porte aucune collision, une au moins aucun retour choisi ; la première du classement porte « minimum atteint »', () => {
|
||||
const generation = generer(demo('petite'), 'petite', REGLAGES);
|
||||
const { classement, premiere } = classerPropositions(generation);
|
||||
// Le plafond a priori est connu : aucun critère n'est sauté, et le
|
||||
// certificat se lit contre lui (§ 5.5).
|
||||
assert.deepEqual(classement.criteresAppliques, [...CRITERES]);
|
||||
assert.equal(classement.critereSaute, null);
|
||||
assert.equal(minimumAtteint(premiere.mesures, generation.aPriori), true);
|
||||
// Le certificat ne lit que les rencontres : il se décerne aussi à un plan
|
||||
// qui réunit des collègues. Chaque appartenance de quatre membres peut
|
||||
// se répartir sur les quatre tables à chaque tour, comme dans le plan
|
||||
// écrit du § 15.3 : aucune proposition ne réunit de collègues, et le
|
||||
// compteur de collisions se mesure à zéro (§ 15.3).
|
||||
for (const { id, mesures } of generation.propositions) {
|
||||
assert.equal(mesures.collisionsCumulees, 0, `proposition ${id}`);
|
||||
}
|
||||
exigerUneSansRetourChoisi('petite', generation.propositions);
|
||||
});
|
||||
|
||||
test('petite-conflit : chaque proposition porte les 4 collisions du plancher du diagnostic, une au moins aucun retour choisi ; la meilleure respecte les planchers, rencontre 8 pour les douze, sur 4 paires, excédent 0', () => {
|
||||
const configuration = demo('petite-conflit');
|
||||
// Le groupe de cinq ne se répartit pas sur quatre tables : au moins une
|
||||
// collision par tour, 4 cumulées sur les 4 tours, portées par une paire
|
||||
// distincte au moins (§ 15.3). Le diagnostic les chiffre ; une liste vide
|
||||
// laisserait la boucle ci-dessous sans rien éprouver.
|
||||
const { collisions } = diagnostiquer(configuration);
|
||||
assert.deepEqual(
|
||||
collisions.map(({ plancherCumulees, plancherPairesDistinctes }) => [
|
||||
plancherCumulees,
|
||||
plancherPairesDistinctes,
|
||||
]),
|
||||
[[4, 1]],
|
||||
);
|
||||
const generation = genererVariante();
|
||||
const { premiere } = classerPropositions(generation);
|
||||
// Tout plan valide respecte ces planchers : ils éprouvent l'accord du
|
||||
// diagnostic et de la mesure, non la recherche.
|
||||
for (const plancher of collisions) {
|
||||
const mesure = premiere.mesures.parGroupe.find(({ groupe }) => groupe === plancher.groupe);
|
||||
assert.ok(
|
||||
mesure.collisionsCumulees >= plancher.plancherCumulees,
|
||||
`${plancher.groupe} : ${mesure.collisionsCumulees} collisions cumulées, plancher ${plancher.plancherCumulees}`,
|
||||
);
|
||||
assert.ok(
|
||||
mesure.pairesDistinctes >= plancher.plancherPairesDistinctes,
|
||||
`${plancher.groupe} : ${mesure.pairesDistinctes} paires distinctes, plancher ${plancher.plancherPairesDistinctes}`,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(premiere.mesures.rencontres, Array(12).fill(8));
|
||||
// La recherche se juge à ses collisions : chaque proposition atteint le
|
||||
// plancher des collisions cumulées sans le dépasser. La meilleure les
|
||||
// porte sur 4 paires distinctes, comme le plan écrit du § 15.3 (épreuve
|
||||
// suivante).
|
||||
for (const { id, mesures } of generation.propositions) {
|
||||
assert.equal(mesures.collisionsCumulees, collisions[0].plancherCumulees, `proposition ${id}`);
|
||||
}
|
||||
assert.equal(premiere.mesures.pairesDistinctes, 4);
|
||||
// Deux voisins par tour, quatre tours : qui rencontre 8 personnes n'en
|
||||
// revoit aucune. Chaque paire se réunit donc une fois au plus, et
|
||||
// l'excédent 0 découle de 8 pour les douze. Il éprouve mesurer, non la
|
||||
// recherche.
|
||||
assert.equal(premiere.mesures.excedentCollisions, 0);
|
||||
exigerUneSansRetourChoisi('petite-conflit', generation.propositions);
|
||||
});
|
||||
|
||||
test('le plan écrit du § 15.3, mesuré sur la variante conflit : 8 pour les douze, 4 collisions sur 4 paires, excédent 0', () => {
|
||||
// Le témoin de l'épreuve précédente : la personne passée de C à A
|
||||
// retrouve un membre de A à chacun des quatre tours, chaque fois un
|
||||
// autre. Ce que l'épreuve exige de la recherche est donc atteignable.
|
||||
const instance = normaliser(demo('petite-conflit'));
|
||||
const mesures = mesurer(instance, indexerPlan(instance, PLAN_PARFAIT_PETITE));
|
||||
assert.deepEqual(mesures.rencontres, Array(12).fill(8));
|
||||
assert.equal(mesures.collisionsCumulees, 4);
|
||||
assert.equal(mesures.pairesDistinctes, 4);
|
||||
assert.equal(mesures.excedentCollisions, 0);
|
||||
});
|
||||
|
||||
test('petite-conflit, deux générations accumulées : classer met la plus longue en tête, numérotée après la brève, et départage ses propositions égales par identifiant croissant (§ 5.7, § 15.5)', () => {
|
||||
// La génération brève prend les identifiants 1 à 3, la longue 4 à 6 :
|
||||
// l'ordre attendu contredit l'ordre des identifiants, et seul un
|
||||
// classement qui applique ses critères peut le rendre.
|
||||
const breve = generer(demo('petite-conflit'), 'petite-conflit, génération brève', REGLAGES_BREVES);
|
||||
const variante = genererVariante();
|
||||
const longue = {
|
||||
aPriori: variante.aPriori,
|
||||
propositions: variante.propositions.map(({ id, mesures }) => ({ id: id + REGLAGES.nombre, mesures })),
|
||||
};
|
||||
// Ce que l'ordre départage, mesuré avant d'être lu. Chaque proposition
|
||||
// longue atteint son plafond a priori, et les trois sont égales sur les
|
||||
// quatre autres critères du classement ; aucune brève n'atteint le sien.
|
||||
// Sur une même population, la redondance moyenne ordonne comme la somme
|
||||
// que lit le classement.
|
||||
const autresCriteres = ({ mesures }) => [
|
||||
mesures.excedentCollisions,
|
||||
mesures.collisionsCumulees,
|
||||
mesures.rencontresRepetees.choisies,
|
||||
mesures.aggRedondance.tous.moyenne,
|
||||
];
|
||||
for (const proposition of longue.propositions) {
|
||||
const nom = `proposition ${proposition.id}`;
|
||||
assert.equal(minimumAtteint(proposition.mesures, longue.aPriori), true, nom);
|
||||
assert.deepEqual(autresCriteres(proposition), autresCriteres(longue.propositions[0]), nom);
|
||||
}
|
||||
for (const { id, mesures } of breve.propositions) {
|
||||
assert.equal(minimumAtteint(mesures, breve.aPriori), false, `proposition ${id}`);
|
||||
}
|
||||
// Le premier critère renvoie les brèves derrière les longues ; les
|
||||
// longues, égales sur les cinq, se départagent par identifiant croissant
|
||||
// (§ 15.5, point 4).
|
||||
const { classement } = classerPropositions({
|
||||
aPriori: longue.aPriori,
|
||||
propositions: [...breve.propositions, ...longue.propositions],
|
||||
});
|
||||
assert.deepEqual(classement.ordre.slice(0, REGLAGES.nombre), [4, 5, 6]);
|
||||
assert.deepEqual([...classement.ordre.slice(REGLAGES.nombre)].sort((a, b) => a - b), [1, 2, 3]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue