[IMP] engine: single owners, regeneration, property grid
The search recomputed the collision excess and the gaps that the measure already gives — two arithmetics for one quantity. It now reads them from the measure, and the redundancy total has one owner. Contract values, the indexed-plan guard and the population split are defined once. regenerer rebuilds a proposition from its derived seed, stop count and history length, which the spec now lists (§ 8.9). A property grid runs seeded random rooms through the whole chain. Checked: 435 node and 20 long tests; redefining the excess or the gaps in the measure now fails the search tests too. --- FR --- [IMP] moteur : propriétaires uniques, régénération, grille de propriétés La recherche recalculait l'excédent de collisions et les écarts que la mesure donne déjà — deux arithmétiques pour une grandeur. Elle les lit désormais dans la mesure, et le total de redondance a un seul propriétaire. Valeurs du contrat, garde du plan indexé et partition des populations sont définies une fois. regenerer reconstruit une proposition depuis sa graine dérivée, son arrêt et la longueur de son historique, que le spec énumère désormais (§ 8.9). Une grille de propriétés fait traverser toute la chaîne à des salles tirées d'une graine. Vérifié : 435 épreuves node et 20 longues ; redéfinir l'excédent ou les écarts dans la mesure fait désormais tomber aussi la recherche. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
c7cd5f9142
commit
6c67cba949
21 changed files with 1229 additions and 381 deletions
48
spec.md
48
spec.md
|
|
@ -157,8 +157,9 @@ quand la configuration en impose un. Sans ce dernier, le troisième chiffre de l
|
|||
page de qualité envoie relancer vers un zéro que la configuration interdit.
|
||||
|
||||
**9. Générer.** *Nécessaire pour obtenir un placement, reprenable sans perte.*
|
||||
La commande prend une **graine** et un **compte d'arrêt**, que chaque
|
||||
proposition conserve (§ 5.7, § 8.9). Le logiciel produit plusieurs propositions
|
||||
La commande prend une **graine** et un **compte d'arrêt** ; chaque proposition
|
||||
conserve sa graine dérivée, ce compte et la longueur d'historique qui l'ont
|
||||
produite (§ 5.7, § 8.9). Le logiciel produit plusieurs propositions
|
||||
comparables ; relancer **ajoute** des candidats, un classement unique les
|
||||
ordonne tous. Les réservations des étapes 6 et 7 sont honorées, le moteur place
|
||||
les autres autour (§ 5.2). Le logiciel nomme automatiquement l'instant qui
|
||||
|
|
@ -611,10 +612,11 @@ réglages ajoute des candidats plutôt que d'effacer les précédents, et un
|
|||
classement unique les ordonne tous. Une commande efface la liste en épargnant
|
||||
celui qui est retenu.
|
||||
|
||||
**La commande prend une graine entière et un compte d'arrêt**, que chaque
|
||||
proposition conserve (§ 8.9). C'est le support de « à graine et entrée égales,
|
||||
placement identique » ; sans lui, une proposition retenue cesse d'être
|
||||
régénérable.
|
||||
**La commande prend une graine entière et un compte d'arrêt**, et une longueur
|
||||
d'historique d'acceptation dont l'algorithme fixe la valeur par défaut. Chaque
|
||||
proposition conserve sa graine dérivée, ce compte et cette longueur (§ 8.9).
|
||||
C'est le support de « à graine et entrée égales, placement identique » ; sans
|
||||
eux, une proposition retenue cesse d'être régénérable.
|
||||
|
||||
**L'identifiant entier d'une proposition vient de sa place dans la suite de
|
||||
graines dérivées, jamais de son ordre d'achèvement.** Quand plusieurs
|
||||
|
|
@ -1505,11 +1507,22 @@ Chaque proposition déclare, **une fois** :
|
|||
- la **liste des capacités** correspondantes ;
|
||||
- le **nombre de tours** ;
|
||||
- l'**ensemble des identifiants de participants** qu'elle place ;
|
||||
- sa **graine** entière et le **compte d'arrêt** de la recherche qui l'a produite.
|
||||
- sa **graine dérivée** (§ 5.7), le **compte d'arrêt** et la **longueur de
|
||||
l'historique** d'acceptation de la recherche qui l'a produite : les trois
|
||||
réglages qui la déterminent.
|
||||
|
||||
Les deux derniers champs sont le support de « à graine et entrée égales, placement
|
||||
identique » (§ 5.10) ; sans eux, cette phrase n'existe que dans un scénario de
|
||||
pilotage, et une proposition retenue cesse d'être régénérable.
|
||||
Ces trois champs sont le support de « à graine et entrée égales, placement
|
||||
identique » (§ 5.10) : à configuration égale et pour une même version du moteur
|
||||
(`produit_version`, § 8.8), la régénération d'une proposition à partir d'eux
|
||||
seuls rend son plan, quel que soit son rang dans sa génération. Sans eux, cette
|
||||
phrase n'existe que dans un scénario de pilotage, et une proposition retenue
|
||||
cesse d'être régénérable. La longueur de l'historique a une valeur par défaut,
|
||||
mais la proposition porte celle qui l'a produite : régénérer sous une valeur
|
||||
supposée rendrait une autre proposition, sans le dire. Les trois champs ne
|
||||
déterminent le plan que pour l'algorithme de recherche qui l'a produit, dont
|
||||
l'ordre des tirages et les règles d'acceptation font partie du résultat : une
|
||||
proposition produite par une autre version du moteur n'est pas garantie
|
||||
régénérable.
|
||||
|
||||
Puis, **par tour** : la liste des tables, chacune étant la liste des
|
||||
**identifiants entiers** de ses occupants **dans l'ordre des sièges** ; et la
|
||||
|
|
@ -1525,7 +1538,7 @@ différents, et le § 8.8 est violé par la seule forme du stockage.
|
|||
# illustration, annotée ; le fichier livré est du JSON strict
|
||||
propositions: [
|
||||
{ id: 3,
|
||||
graine: 48271, arret: 20000,
|
||||
graine: 48271, arret: 20000, historique: 1000,
|
||||
tables: [7, 12, 2, ...], identifiants de table (§ 4)
|
||||
capacites: [8, 8, 7, ...],
|
||||
tours: 4,
|
||||
|
|
@ -3655,9 +3668,16 @@ certitude fausse.
|
|||
gros groupe tient dans le nombre de tables » est nécessaire mais **ne suffit
|
||||
pas** : trois tables de 10, 1 et 1 ne peuvent séparer quatre groupes de 3, parce
|
||||
que la table de 10 exigerait 10 appartenances distinctes. Le logiciel vérifie
|
||||
**deux** inégalités — plus gros groupe ≤ nombre de tables, plus grande capacité
|
||||
≤ nombre de groupes — puis **construit un tour témoin**. Le générateur **refuse
|
||||
une graine dont aucun tour témoin ne se construit**.
|
||||
**deux** inégalités — plus gros groupe ≤ nombre de tables, plus grande
|
||||
capacité − places vides ≤ nombre de groupes — puis **construit un tour
|
||||
témoin**. Les places vides sont les sièges qui restent une fois chaque membre
|
||||
assis, zéro quand la salle n'en laisse pas : une table reçoit au moins sa
|
||||
capacité moins ces places, chacun de ses occupants d'une appartenance
|
||||
différente. Dans une salle exactement pleine, la seconde inégalité se lit
|
||||
« plus grande capacité ≤ nombre de groupes » ; ailleurs, cette lecture
|
||||
refuserait à tort deux tables de 3 pour deux membres d'appartenances
|
||||
différentes, qu'un tour sépare. Le générateur **refuse une graine dont aucun
|
||||
tour témoin ne se construit**.
|
||||
|
||||
### 15.3 La petite démonstration — 12 membres
|
||||
|
||||
|
|
|
|||
|
|
@ -66,11 +66,12 @@ export function tirerTailles(rng, total) {
|
|||
* (§ 15.2), contrôlées dans cet ordre ; la première rompue est nommée.
|
||||
* - PLUS_GROS_GROUPE : le plus gros groupe dépasse le nombre de tables, alors
|
||||
* que chacun de ses membres occupe une table différente.
|
||||
* - PLUS_GRANDE_CAPACITE : la plus grande capacité, diminuée du surplus de
|
||||
* sièges sur les membres quand il est positif, dépasse le nombre de groupes.
|
||||
* Une table reçoit au moins sa capacité moins ce surplus, chacun de ses
|
||||
* membres d'un groupe différent. Dans une salle exactement pleine, c'est
|
||||
* « plus grande capacité ≤ nombre de groupes ».
|
||||
* - PLUS_GRANDE_CAPACITE : la plus grande capacité, diminuée des places
|
||||
* vides, dépasse le nombre de groupes. Les places vides sont les sièges qui
|
||||
* restent une fois chaque membre assis, zéro quand la salle n'en laisse
|
||||
* pas. Une table reçoit au moins sa capacité moins ces places, chacun de
|
||||
* ses membres d'un groupe différent. Dans une salle exactement pleine,
|
||||
* c'est « plus grande capacité ≤ nombre de groupes ».
|
||||
* Nécessaires, elles ne suffisent pas : seul tourTemoin conclut.
|
||||
*
|
||||
* @param {number[]} tailles taille de chaque groupe
|
||||
|
|
@ -79,8 +80,8 @@ export function tirerTailles(rng, total) {
|
|||
*/
|
||||
export function faisabilite(tailles, capacites) {
|
||||
if (maximum(tailles) > capacites.length) return { ok: false, raison: 'PLUS_GROS_GROUPE' };
|
||||
const surplus = Math.max(0, somme(capacites) - somme(tailles));
|
||||
if (maximum(capacites) - surplus > tailles.length) {
|
||||
const placesVides = Math.max(0, somme(capacites) - somme(tailles));
|
||||
if (maximum(capacites) - placesVides > tailles.length) {
|
||||
return { ok: false, raison: 'PLUS_GRANDE_CAPACITE' };
|
||||
}
|
||||
return { ok: true };
|
||||
|
|
|
|||
|
|
@ -158,19 +158,19 @@ describe('appartenances : les deux inégalités', () => {
|
|||
[[2, 2], [3, 1], { ok: false, raison: 'PLUS_GRANDE_CAPACITE' },
|
||||
'salle pleine : une table de 3 pour 2 groupes'],
|
||||
[[2, 2, 2], [5, 2], { ok: false, raison: 'PLUS_GRANDE_CAPACITE' },
|
||||
'un siège en surplus : la table de 5 reçoit au moins 4 membres, pour 3 groupes'],
|
||||
'une place vide : la table de 5 reçoit au moins 4 membres, pour 3 groupes'],
|
||||
[[4, 2], [3, 3, 3], { ok: false, raison: 'PLUS_GROS_GROUPE' }, 'un groupe de 4 pour 3 tables'],
|
||||
[[3, 2, 1], [3, 3], { ok: false, raison: 'PLUS_GROS_GROUPE' }, 'un groupe de 3 pour 2 tables'],
|
||||
[[3, 3, 3, 3], [10, 1], { ok: false, raison: 'PLUS_GROS_GROUPE' },
|
||||
'les deux rompues : la première est nommée'],
|
||||
[[2, 2, 1], [3, 2], { ok: true }, 'les deux inégalités à égalité'],
|
||||
[[2, 2, 2], [4, 3], { ok: true },
|
||||
'un siège en surplus : la table de 4 reçoit au moins 3 membres, pour 3 groupes'],
|
||||
[[1, 1, 1], [8, 2], { ok: true }, 'sept sièges en surplus'],
|
||||
'une place vide : la table de 4 reçoit au moins 3 membres, pour 3 groupes'],
|
||||
[[1, 1, 1], [8, 2], { ok: true }, 'sept places vides'],
|
||||
[[2, 2], [2, 1], { ok: true },
|
||||
'salle trop petite : un surplus négatif compte pour zéro, la table de 2 tient dans 2 groupes'],
|
||||
'salle trop petite : aucune place vide, la table de 2 tient dans 2 groupes'],
|
||||
[[2, 2, 2], [4, 1], { ok: false, raison: 'PLUS_GRANDE_CAPACITE' },
|
||||
'salle trop petite : un surplus négatif compte pour zéro, la table de 4 dépasse 3 groupes'],
|
||||
'salle trop petite : aucune place vide, la table de 4 dépasse 3 groupes'],
|
||||
[[3, 3, 3, 1, 1, 1], [6, 4, 2], { ok: true }, 'inégalités tenues, sans tour pourtant'],
|
||||
[[], [8], { ok: true }, 'aucun membre'],
|
||||
];
|
||||
|
|
@ -235,7 +235,7 @@ describe('appartenances : le tour témoin', () => {
|
|||
test('petites instances : un tour quand, et seulement quand, la recherche exhaustive en trouve un', () => {
|
||||
// Domaine : 1 à 4 groupes de 1 à 4 membres, 1 à 4 tables de 1 à 4 sièges,
|
||||
// soit 340 × 340 instances. Il contient des instances sans tour où les
|
||||
// deux inégalités tiennent, et des instances à sièges en surplus dont la
|
||||
// deux inégalités tiennent, et des instances à places vides dont la
|
||||
// plus grande table dépasse le nombre de groupes et qui ont un tour.
|
||||
const ecarts = [];
|
||||
let avecTour = 0;
|
||||
|
|
@ -264,6 +264,6 @@ describe('appartenances : le tour témoin', () => {
|
|||
assert.deepEqual(ecarts.slice(0, 12), []);
|
||||
assert.ok(avecTour > 0, 'aucune instance avec tour');
|
||||
assert.ok(sansTourInegalitesTenues > 0, 'aucune instance où seul le tour témoin conclut');
|
||||
assert.ok(avecTourTableAuDelaDesGroupes > 0, 'aucune instance où le surplus de sièges compte');
|
||||
assert.ok(avecTourTableAuDelaDesGroupes > 0, 'aucune instance où les places vides comptent');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,17 +15,18 @@ import { ecartsAuPlafondAPriori } from './manque.js';
|
|||
|
||||
/**
|
||||
* Les critères du classement, dans leur ordre d'application :
|
||||
* - ecartAPrioriMax : le plus grand écart au plafond a priori, plafond a
|
||||
* priori − rencontres, sur toutes les personnes ;
|
||||
* - ecartAuPlafondAPrioriMax : le plus grand écart au plafond a priori,
|
||||
* plafond a priori − rencontres, sur toutes les personnes, sous le nom
|
||||
* que lui donne le glossaire du § 5.5 ;
|
||||
* - excedentCollisions et collisionsCumulees, tels que mesurer les rend ;
|
||||
* - rencontresRepetees : les rencontres répétées que le moteur a choisies ;
|
||||
* celles qu'imposent les réservations sont les mêmes pour toute
|
||||
* proposition, et ne départagent rien (§ 5.4) ;
|
||||
* - redondance : Σ_p r(p), la redondance d'appartenance sommée sur les
|
||||
* personnes.
|
||||
* personnes, que mesurer rend sous le nom totalRedondance.
|
||||
*/
|
||||
export const CRITERES = Object.freeze([
|
||||
'ecartAPrioriMax',
|
||||
'ecartAuPlafondAPrioriMax',
|
||||
'excedentCollisions',
|
||||
'collisionsCumulees',
|
||||
'rencontresRepetees',
|
||||
|
|
@ -34,7 +35,7 @@ export const CRITERES = Object.freeze([
|
|||
|
||||
// Le critère qui lit le plafond a priori par personne : sauté quand il est
|
||||
// inconnu (§ 17, point 4).
|
||||
const CRITERE_A_PRIORI = 'ecartAPrioriMax';
|
||||
const CRITERE_A_PRIORI = 'ecartAuPlafondAPrioriMax';
|
||||
|
||||
// Le plus grand élément d'une liste ; null pour une liste vide.
|
||||
function plusGrand(valeurs) {
|
||||
|
|
@ -43,22 +44,16 @@ function plusGrand(valeurs) {
|
|||
return plus;
|
||||
}
|
||||
|
||||
// La somme d'une liste ; 0 pour une liste vide.
|
||||
function somme(valeurs) {
|
||||
let total = 0;
|
||||
for (const valeur of valeurs) total += valeur;
|
||||
return total;
|
||||
}
|
||||
|
||||
// Valeur de chaque critère pour une proposition lue : { mesures, ecarts },
|
||||
// ecarts étant ses écarts au plafond a priori. La Map ne sert qu'à retrouver
|
||||
// un critère par son nom ; CRITERES en fixe l'ordre.
|
||||
// ecarts étant ses écarts au plafond a priori. Chaque grandeur se lit dans
|
||||
// la mesure ou dans manque.js, aucune ne se refait ici. La Map ne sert qu'à
|
||||
// retrouver un critère par son nom ; CRITERES en fixe l'ordre.
|
||||
const VALEUR = new Map([
|
||||
['ecartAPrioriMax', ({ ecarts }) => plusGrand(ecarts)],
|
||||
['ecartAuPlafondAPrioriMax', ({ ecarts }) => plusGrand(ecarts)],
|
||||
['excedentCollisions', ({ mesures }) => mesures.excedentCollisions],
|
||||
['collisionsCumulees', ({ mesures }) => mesures.collisionsCumulees],
|
||||
['rencontresRepetees', ({ mesures }) => mesures.rencontresRepetees.choisies],
|
||||
['redondance', ({ mesures }) => somme(mesures.redondance)],
|
||||
['redondance', ({ mesures }) => mesures.totalRedondance],
|
||||
]);
|
||||
|
||||
// Lève TypeError pour un identifiant qui n'est pas un entier, et RangeError
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ function figer(valeur) {
|
|||
// plafond a priori de chacun, 8 − rencontres ; collisions est le triplet
|
||||
// (cumulées, distinctes, excédent) du § 5.4 ; repetees et imposees comptent
|
||||
// les rencontres répétées que le moteur a choisies et celles qu'imposent les
|
||||
// réservations ; redondance est r(p) pour chacun.
|
||||
// réservations ; redondance est Σ_p r(p), le total que mesurer rend sous le
|
||||
// nom totalRedondance. La liste des r(p) n'y figure pas : classer ne la lit
|
||||
// pas.
|
||||
function proposition(
|
||||
id,
|
||||
{
|
||||
|
|
@ -38,7 +40,7 @@ function proposition(
|
|||
collisions = [0, 0, 0],
|
||||
repetees = 0,
|
||||
imposees = 0,
|
||||
redondance = [0, 0, 0],
|
||||
redondance = 0,
|
||||
} = {},
|
||||
) {
|
||||
const [collisionsCumulees, pairesDistinctes, excedentCollisions] = collisions;
|
||||
|
|
@ -50,7 +52,7 @@ function proposition(
|
|||
pairesDistinctes,
|
||||
excedentCollisions,
|
||||
rencontresRepetees: { choisies: repetees, imposees },
|
||||
redondance,
|
||||
totalRedondance: redondance,
|
||||
},
|
||||
plafondsAPriori: [8, 8, 8],
|
||||
};
|
||||
|
|
@ -65,7 +67,7 @@ const permutations = ([a, b, c]) => [[a, b, c], [a, c, b], [b, a, c], [b, c, a],
|
|||
describe('classer : les critères et leur ordre (§ 5.7)', () => {
|
||||
test('CRITERES nomme les cinq critères dans leur ordre, et ne se modifie pas', () => {
|
||||
assert.deepEqual(CRITERES, [
|
||||
'ecartAPrioriMax',
|
||||
'ecartAuPlafondAPrioriMax',
|
||||
'excedentCollisions',
|
||||
'collisionsCumulees',
|
||||
'rencontresRepetees',
|
||||
|
|
@ -104,7 +106,7 @@ describe('classer : les critères et leur ordre (§ 5.7)', () => {
|
|||
proposition(2, { collisions: [2, 1, 1], repetees: 1 }), // excédent 1
|
||||
proposition(3, { collisions: [3, 3, 0] }), // 3 cumulées, excédent 0
|
||||
proposition(4, { repetees: 1 }), // une rencontre répétée
|
||||
proposition(5, { redondance: [1, 0, 0] }), // redondance 1
|
||||
proposition(5, { redondance: 1 }), // redondance 1
|
||||
proposition(6),
|
||||
];
|
||||
assert.deepEqual(classer(figer(entrees)), {
|
||||
|
|
@ -128,9 +130,11 @@ describe('classer : les critères et leur ordre (§ 5.7)', () => {
|
|||
assert.deepEqual(classer(entrees).ordre, [2, 1]);
|
||||
});
|
||||
|
||||
test('la redondance se somme sur les personnes', () => {
|
||||
// 1 : 1, 1, 1, somme 3 et plus grande 1 ; 2 : 2, 0, 0, somme 2 et plus grande 2.
|
||||
const entrees = [proposition(1, { redondance: [1, 1, 1] }), proposition(2, { redondance: [2, 0, 0] })];
|
||||
test('la redondance se lit sur totalRedondance, la somme que mesurer rend', () => {
|
||||
// 1 : r(p) vaut 1, 1, 1, somme 3 et plus grand 1 ; 2 : r(p) vaut 2, 0,
|
||||
// 0, somme 2 et plus grand 2. La somme range 2 devant 1, le plus grand
|
||||
// ferait l'inverse.
|
||||
const entrees = [proposition(1, { redondance: 3 }), proposition(2, { redondance: 2 })];
|
||||
assert.deepEqual(classer(entrees).ordre, [2, 1]);
|
||||
});
|
||||
});
|
||||
|
|
@ -148,7 +152,7 @@ describe('classer : le critère sauté (§ 5.7, § 12.6)', () => {
|
|||
});
|
||||
// Un seul a priori inconnu suffit : l'excédent ouvre alors le classement
|
||||
// de toutes, y compris de celle dont l'a priori est connu.
|
||||
const attendu = { ordre: [2, 1], criteresAppliques: SANS_A_PRIORI, critereSaute: 'ecartAPrioriMax' };
|
||||
const attendu = { ordre: [2, 1], criteresAppliques: SANS_A_PRIORI, critereSaute: 'ecartAuPlafondAPrioriMax' };
|
||||
assert.deepEqual(classer([premiere, inconnu(seconde)]), attendu);
|
||||
assert.deepEqual(classer([inconnu(premiere), seconde]), attendu);
|
||||
assert.deepEqual(classer([inconnu(premiere), inconnu(seconde)]), attendu);
|
||||
|
|
@ -158,7 +162,7 @@ describe('classer : le critère sauté (§ 5.7, § 12.6)', () => {
|
|||
describe('classer : le départage par identifiant (§ 15.5, point 4)', () => {
|
||||
test("à égalité sur les cinq critères, l'identifiant croissant, quel que soit l'ordre reçu", () => {
|
||||
const egales = [7, 3, 5].map((id) =>
|
||||
proposition(id, { rencontres: [7, 8, 8], collisions: [2, 1, 1], repetees: 1, redondance: [1, 0, 0] }),
|
||||
proposition(id, { rencontres: [7, 8, 8], collisions: [2, 1, 1], repetees: 1, redondance: 1 }),
|
||||
);
|
||||
for (const ordreRecu of permutations(egales)) {
|
||||
assert.deepEqual(classer(ordreRecu).ordre, [3, 5, 7]);
|
||||
|
|
@ -203,7 +207,7 @@ describe('classer : petite démonstration, conflit inévitable (§ 14.10, § 15.
|
|||
assert.deepEqual(classer(entrees.map(inconnu)), {
|
||||
ordre: [2, 1],
|
||||
criteresAppliques: SANS_A_PRIORI,
|
||||
critereSaute: 'ecartAPrioriMax',
|
||||
critereSaute: 'ecartAuPlafondAPrioriMax',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -216,7 +220,7 @@ describe('classer : les entrées reçues', () => {
|
|||
test("population vide, plafond a priori connu : le premier critère s'applique, sans rien départager", () => {
|
||||
// Une population vide n'a pas de plus grand écart au plafond a priori ;
|
||||
// ce n'est pas un plafond a priori inconnu, et rien n'est sauté.
|
||||
const vide = (id) => ({ ...proposition(id, { rencontres: [], redondance: [] }), plafondsAPriori: [] });
|
||||
const vide = (id) => ({ ...proposition(id, { rencontres: [] }), plafondsAPriori: [] });
|
||||
assert.deepEqual(classer([vide(2), vide(1)]), {
|
||||
ordre: [1, 2],
|
||||
criteresAppliques: [...CRITERES],
|
||||
|
|
@ -238,7 +242,7 @@ describe('classer : les entrées reçues', () => {
|
|||
|
||||
test('des mesures de populations de tailles différentes lèvent RangeError', () => {
|
||||
const quatre = {
|
||||
...proposition(2, { rencontres: [8, 8, 8, 8], redondance: [0, 0, 0, 0] }),
|
||||
...proposition(2, { rencontres: [8, 8, 8, 8] }),
|
||||
plafondsAPriori: [8, 8, 8, 8],
|
||||
};
|
||||
assert.throws(() => classer([proposition(1), quatre]), RangeError);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@
|
|||
// que consomme le moteur (§ 4, § 5.2), et conversion d'un plan entre ses deux
|
||||
// formes (§ 8.9). Les formes sont décrites dans types.js.
|
||||
//
|
||||
// Ce module définit aussi, une seule fois pour tout le moteur (§ 13.2), ce
|
||||
// que les autres modules lisent de ces formes : les valeurs du contrat —
|
||||
// STATUT, LIBRE, RESERVE, SANS_GROUPE —, la garde de forme d'un plan indexé,
|
||||
// le nombre de places d'un tour, et la répartition des personnes entre les
|
||||
// trois populations du § 5.4. Les autres modules et leurs épreuves les
|
||||
// importent, sans en redéfinir aucun.
|
||||
//
|
||||
// Trois familles de refus :
|
||||
// - ErreurConfiguration, avec un code, pour ce qu'une saisie, un import ou un
|
||||
// plan périmé peut porter : une donnée incohérente avec le reste de la
|
||||
|
|
@ -26,15 +33,27 @@
|
|||
// sont pas examinés. Aucune fonction ne modifie ce qu'elle reçoit.
|
||||
import { ErreurConfiguration } from './erreurs.js';
|
||||
|
||||
// Statuts d'un participant (§ 4.2), valeurs de instance.statut.
|
||||
const MOBILE = 0;
|
||||
const PARTIELLEMENT_FIXE = 1;
|
||||
const ANCRE = 2;
|
||||
/**
|
||||
* Statuts d'un participant (§ 4.2), valeurs de instance.statut. normaliser
|
||||
* les dérive des réservations ; aucun n'est déclaré. Un partiellement fixé
|
||||
* compte parmi les mobiles dans les populations du § 5.4.
|
||||
*/
|
||||
export const STATUT = Object.freeze({ MOBILE: 0, PARTIELLEMENT_FIXE: 1, ANCRE: 2 });
|
||||
|
||||
/** Case de instance.fixe sans table imposée. */
|
||||
export const LIBRE = -1;
|
||||
|
||||
/**
|
||||
* Case d'un plan indexé : la réserve. Elle vaut LIBRE, et une rangée de
|
||||
* instance.fixe se lit ainsi comme le plan qui assied chacun à ses seuls
|
||||
* tours fixés et le laisse en réserve aux autres : le diagnostic mesure ce
|
||||
* plan, le plafond a priori part de cet itinéraire.
|
||||
*/
|
||||
export const RESERVE = -1;
|
||||
|
||||
/** instance.groupe d'un participant sans appartenance. */
|
||||
export const SANS_GROUPE = -1;
|
||||
|
||||
// Case de instance.fixe sans table imposée.
|
||||
const LIBRE = -1;
|
||||
// Case d'un plan indexé : la réserve.
|
||||
const RESERVE = -1;
|
||||
// Case d'un plan indexé qu'indexerPlan n'a pas encore remplie ; jamais rendue.
|
||||
const NON_PLACE = -2;
|
||||
|
||||
|
|
@ -97,9 +116,9 @@ function copierContraintes(contraintes) {
|
|||
}
|
||||
|
||||
// Groupe de chaque participant présent, présents triés par identifiant
|
||||
// croissant : un libellé reçoit le numéro de sa première apparition, −1
|
||||
// marque l'absence d'appartenance. L'égalité de chaînes fait le groupe. La
|
||||
// Map ne sert qu'à retrouver un libellé déjà numéroté.
|
||||
// croissant : un libellé reçoit le numéro de sa première apparition,
|
||||
// SANS_GROUPE marque l'absence d'appartenance. L'égalité de chaînes fait le
|
||||
// groupe. La Map ne sert qu'à retrouver un libellé déjà numéroté.
|
||||
function numeroterGroupes(presents) {
|
||||
const groupe = new Int32Array(presents.length);
|
||||
const groupes = [];
|
||||
|
|
@ -107,7 +126,7 @@ function numeroterGroupes(presents) {
|
|||
for (let p = 0; p < presents.length; p += 1) {
|
||||
const { appartenance } = presents[p];
|
||||
if (appartenance === null) {
|
||||
groupe[p] = -1;
|
||||
groupe[p] = SANS_GROUPE;
|
||||
continue;
|
||||
}
|
||||
if (!numeroDe.has(appartenance)) {
|
||||
|
|
@ -218,13 +237,13 @@ function deriverStatuts(fixe, N, T, R) {
|
|||
if (t !== LIBRE) aucune = false;
|
||||
}
|
||||
if (aucune) {
|
||||
statut[p] = MOBILE;
|
||||
statut[p] = STATUT.MOBILE;
|
||||
} else if (memeTable) {
|
||||
statut[p] = ANCRE;
|
||||
statut[p] = STATUT.ANCRE;
|
||||
ancresParTable[premiere] += 1;
|
||||
k += 1;
|
||||
} else {
|
||||
statut[p] = PARTIELLEMENT_FIXE;
|
||||
statut[p] = STATUT.PARTIELLEMENT_FIXE;
|
||||
}
|
||||
}
|
||||
return { statut, ancresParTable, k };
|
||||
|
|
@ -415,21 +434,49 @@ export function indexerPlan(instance, plan) {
|
|||
return tableDe;
|
||||
}
|
||||
|
||||
/**
|
||||
* La garde de forme d'un plan indexé, la seule du moteur : planDepuisIndex,
|
||||
* mesurer et plafondsRealises l'appellent avant de lire une case. Lève
|
||||
* RangeError quand tableDe n'a pas N × R cases, ou qu'une case n'est ni
|
||||
* RESERVE ni un index de table entier de 0 à T − 1. Le message nomme alors
|
||||
* le participant, par identifiant, le tour et la valeur de la première case
|
||||
* fautive dans l'ordre des cases, p × R + r croissant ; une chaîne s'y cite
|
||||
* entre guillemets, pour ne pas se lire comme le nombre qu'elle contient.
|
||||
*
|
||||
* Sans elle, une case de trop serait ignorée, et une chaîne numérique comme
|
||||
* "0" assiérait la personne à la table qu'elle désigne là où une
|
||||
* comparaison stricte à instance.fixe n'y reconnaîtrait pas sa réservation.
|
||||
*
|
||||
* @param {import('./types.js').Instance} instance
|
||||
* @param {ArrayLike<number>} tableDe
|
||||
*/
|
||||
export function exigerPlanIndexe({ N, T, R, ids }, tableDe) {
|
||||
if (tableDe.length !== N * R) {
|
||||
throw new RangeError(`plan indexé de ${tableDe.length} cases, N × R = ${N * R} attendues`);
|
||||
}
|
||||
for (let i = 0; i < tableDe.length; i += 1) {
|
||||
const t = tableDe[i];
|
||||
if (t !== RESERVE && !(Number.isInteger(t) && t >= 0 && t < T)) {
|
||||
throw new RangeError(
|
||||
`plan indexé : participant ${ids[Math.floor(i / R)]}, tour ${(i % R) + 1}, `
|
||||
+ `index de table ${decrire(t)} hors de −1..${T - 1}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan par identifiants d'un plan indexé : les tables dans l'ordre de
|
||||
* l'instance, chaque liste de table et chaque réserve en ids croissants
|
||||
* (§ 8.9). Lève RangeError quand tableDe n'a pas N × R cases, ou qu'une case
|
||||
* n'est ni −1 ni un index de table.
|
||||
* (§ 8.9). Lève ce que lève exigerPlanIndexe.
|
||||
*
|
||||
* @param {import('./types.js').Instance} instance
|
||||
* @param {ArrayLike<number>} tableDe
|
||||
* @returns {import('./types.js').Plan}
|
||||
*/
|
||||
export function planDepuisIndex(instance, tableDe) {
|
||||
exigerPlanIndexe(instance, tableDe);
|
||||
const { N, T, R, ids, idsTables } = instance;
|
||||
if (tableDe.length !== N * R) {
|
||||
throw new RangeError(`plan indexé de ${tableDe.length} cases, N × R = ${N * R} attendues`);
|
||||
}
|
||||
const tours = [];
|
||||
const reserves = [];
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
|
|
@ -439,16 +486,8 @@ export function planDepuisIndex(instance, tableDe) {
|
|||
// range chaque liste en ids croissants, sans tri.
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const t = tableDe[p * R + r];
|
||||
if (t === RESERVE) {
|
||||
reserve.push(ids[p]);
|
||||
} else if (Number.isInteger(t) && t >= 0 && t < T) {
|
||||
listes[t].push(ids[p]);
|
||||
} else {
|
||||
throw new RangeError(
|
||||
`plan indexé : participant ${ids[p]}, tour ${r + 1}, `
|
||||
+ `index de table ${decrire(t)} hors de −1..${T - 1}`,
|
||||
);
|
||||
}
|
||||
if (t === RESERVE) reserve.push(ids[p]);
|
||||
else listes[t].push(ids[p]);
|
||||
}
|
||||
tours.push(listes);
|
||||
reserves.push(reserve);
|
||||
|
|
@ -456,6 +495,20 @@ export function planDepuisIndex(instance, tableDe) {
|
|||
return { tables: [...idsTables], tours, reserves };
|
||||
}
|
||||
|
||||
/**
|
||||
* Places qu'offre chaque tour : Σ c_t, la somme des capacités. Ni les
|
||||
* participants ni les réservations n'y entrent. Le nombre de places
|
||||
* manquantes et le diagnostic d'une salle tendue la lisent ici.
|
||||
*
|
||||
* @param {import('./types.js').Instance} instance T et capacite sont lus
|
||||
* @returns {number}
|
||||
*/
|
||||
export function nombrePlaces({ T, capacite }) {
|
||||
let places = 0;
|
||||
for (let t = 0; t < T; t += 1) places += capacite[t];
|
||||
return places;
|
||||
}
|
||||
|
||||
/**
|
||||
* Places qui manquent à chaque tour pour asseoir tous les participants :
|
||||
* max(0, N − Σ c_t). La quantité égale max(0, n − Σ (c_t − a_t)) et ne
|
||||
|
|
@ -467,7 +520,36 @@ export function planDepuisIndex(instance, tableDe) {
|
|||
* @returns {number}
|
||||
*/
|
||||
export function nombrePlacesManquantes(instance) {
|
||||
let places = 0;
|
||||
for (let t = 0; t < instance.T; t += 1) places += instance.capacite[t];
|
||||
return Math.max(0, instance.N - places);
|
||||
return Math.max(0, instance.N - nombrePlaces(instance));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replie valeurs[p] sur les trois populations du § 5.4, en un passage, dans
|
||||
* l'ordre canonique : chacun compte dans « tous », puis parmi les ancrés, ou
|
||||
* parmi les mobiles, partiellement fixés compris. C'est le seul endroit qui
|
||||
* range une personne dans sa population : les agrégats des indicateurs et
|
||||
* les chiffres du manque le lisent tous deux.
|
||||
*
|
||||
* replier(cumul, valeur) rend le cumul suivant sans modifier celui qu'il
|
||||
* reçoit : les trois populations partent du même initial, et une population
|
||||
* sans membre le garde.
|
||||
*
|
||||
* @template C
|
||||
* @param {ArrayLike<*>} valeurs une par personne, dans l'ordre de instance.ids
|
||||
* @param {ArrayLike<number>} statut instance.statut
|
||||
* @param {C} initial
|
||||
* @param {function(C, *): C} replier
|
||||
* @returns {{tous: C, mobiles: C, ancres: C}}
|
||||
*/
|
||||
export function replierParPopulation(valeurs, statut, initial, replier) {
|
||||
let tous = initial;
|
||||
let mobiles = initial;
|
||||
let ancres = initial;
|
||||
for (let p = 0; p < valeurs.length; p += 1) {
|
||||
const valeur = valeurs[p];
|
||||
tous = replier(tous, valeur);
|
||||
if (statut[p] === STATUT.ANCRE) ancres = replier(ancres, valeur);
|
||||
else mobiles = replier(mobiles, valeur);
|
||||
}
|
||||
return { tous, mobiles, ancres };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Épreuves de la configuration (§ 4, § 5.2, § 5.9) : l'instance indexée que
|
||||
// rend normaliser et ses refus, le nombre de places manquantes, la conversion
|
||||
// d'un plan entre la forme par identifiants et la forme indexée. Les
|
||||
// Épreuves de la configuration (§ 4, § 5.2, § 5.9) : les valeurs du contrat
|
||||
// de données, l'instance indexée que rend normaliser et ses refus, le nombre
|
||||
// de places et de places manquantes, la conversion d'un plan entre la forme
|
||||
// par identifiants et la forme indexée, la garde de forme d'un plan indexé
|
||||
// et la répartition des personnes entre les trois populations du § 5.4. Les
|
||||
// identifiants diffèrent des index qu'ils reçoivent et des tables arrivent
|
||||
// hors de l'ordre de leurs identifiants : confondre un identifiant et un index
|
||||
// fait échouer une épreuve au lieu de passer inaperçu.
|
||||
|
|
@ -11,11 +13,22 @@ import assert from 'node:assert/strict';
|
|||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { ErreurAnnulee, ErreurConfiguration } from './erreurs.js';
|
||||
import {
|
||||
LIBRE,
|
||||
RESERVE,
|
||||
SANS_GROUPE,
|
||||
STATUT,
|
||||
exigerPlanIndexe,
|
||||
indexerPlan,
|
||||
nombrePlaces,
|
||||
nombrePlacesManquantes,
|
||||
normaliser,
|
||||
planDepuisIndex,
|
||||
replierParPopulation,
|
||||
} from './configuration.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
import { plafondsRealises } from './plafond.js';
|
||||
|
||||
const { MOBILE, PARTIELLEMENT_FIXE, ANCRE } = STATUT;
|
||||
|
||||
const SANS_CONTRAINTE = {
|
||||
separerAppartenances: false,
|
||||
|
|
@ -89,6 +102,17 @@ function ecartsDeRefus(cas) {
|
|||
.map(([libelle, ecart]) => `${libelle} : ${ecart}`);
|
||||
}
|
||||
|
||||
describe('valeurs du contrat de données (types.js)', () => {
|
||||
test('STATUT, LIBRE, RESERVE et SANS_GROUPE valent ce que types.js écrit, et STATUT ne se modifie pas', () => {
|
||||
// Seule épreuve qui écrit ces valeurs en clair : les autres les
|
||||
// importent. LIBRE et RESERVE sont égales : une rangée de instance.fixe se
|
||||
// lit comme le plan qui laisse en réserve chaque tour libre.
|
||||
assert.deepEqual(STATUT, { MOBILE: 0, PARTIELLEMENT_FIXE: 1, ANCRE: 2 });
|
||||
assert.ok(Object.isFrozen(STATUT));
|
||||
assert.deepEqual([LIBRE, RESERVE, SANS_GROUPE], [-1, -1, -1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normaliser : statuts (§ 4.2, § 4.3)', () => {
|
||||
test('le statut se dérive du fait : ancré, partiellement fixé ou mobile', () => {
|
||||
const instance = normaliser(configuration({
|
||||
|
|
@ -112,16 +136,21 @@ describe('normaliser : statuts (§ 4.2, § 4.3)', () => {
|
|||
// seul dernier tour ; 9 : chaque tour, la même table au premier et au
|
||||
// dernier, une autre entre les deux. Seuls 1 et 2 comptent dans k : un
|
||||
// partiellement fixé compte parmi les mobiles.
|
||||
assert.deepEqual(instance.statut, Uint8Array.from([2, 2, 1, 1, 1, 0, 1, 1, 1]));
|
||||
assert.deepEqual(instance.statut, Uint8Array.from([
|
||||
ANCRE, ANCRE,
|
||||
PARTIELLEMENT_FIXE, PARTIELLEMENT_FIXE, PARTIELLEMENT_FIXE,
|
||||
MOBILE,
|
||||
PARTIELLEMENT_FIXE, PARTIELLEMENT_FIXE, PARTIELLEMENT_FIXE,
|
||||
]));
|
||||
assert.deepEqual(instance.fixe, Int32Array.from([
|
||||
0, 0, 0,
|
||||
1, 1, 1,
|
||||
-1, 2, -1,
|
||||
0, 1, -1,
|
||||
LIBRE, 2, LIBRE,
|
||||
0, 1, LIBRE,
|
||||
2, 2, 3,
|
||||
-1, -1, -1,
|
||||
3, -1, -1,
|
||||
-1, -1, 3,
|
||||
LIBRE, LIBRE, LIBRE,
|
||||
3, LIBRE, LIBRE,
|
||||
LIBRE, LIBRE, 3,
|
||||
1, 2, 1,
|
||||
]));
|
||||
assert.equal(instance.k, 2);
|
||||
|
|
@ -135,8 +164,8 @@ describe('normaliser : statuts (§ 4.2, § 4.3)', () => {
|
|||
tours: 1,
|
||||
reservations: [auTour(1, 11, 1)],
|
||||
}));
|
||||
assert.deepEqual(unTour.statut, Uint8Array.from([2, 0]));
|
||||
assert.deepEqual(unTour.fixe, Int32Array.from([0, -1]));
|
||||
assert.deepEqual(unTour.statut, Uint8Array.from([ANCRE, MOBILE]));
|
||||
assert.deepEqual(unTour.fixe, Int32Array.from([0, LIBRE]));
|
||||
assert.equal(unTour.k, 1);
|
||||
});
|
||||
});
|
||||
|
|
@ -162,7 +191,7 @@ describe('normaliser : ancrés et instance réduite (§ 5.2)', () => {
|
|||
assert.equal(instance.k, 4);
|
||||
assert.equal(instance.n, 16);
|
||||
assert.deepEqual(instance.ancresParTable, Int32Array.from([1, 1, 1, 1]));
|
||||
assert.equal(instance.statut[instance.indexDe.get(2)], 1);
|
||||
assert.equal(instance.statut[instance.indexDe.get(2)], PARTIELLEMENT_FIXE);
|
||||
});
|
||||
|
||||
test('ancresParTable suit les index de table, dans l’ordre de configuration.tables', () => {
|
||||
|
|
@ -195,8 +224,8 @@ describe('normaliser : exclusion (§ 4.4)', () => {
|
|||
assert.equal(instance.k, 0);
|
||||
assert.equal(instance.n, 3);
|
||||
assert.deepEqual(instance.ancresParTable, Int32Array.from([0, 0]));
|
||||
assert.deepEqual(instance.fixe, new Int32Array(6).fill(-1));
|
||||
assert.deepEqual(instance.statut, new Uint8Array(3));
|
||||
assert.deepEqual(instance.fixe, new Int32Array(6).fill(LIBRE));
|
||||
assert.deepEqual(instance.statut, new Uint8Array(3).fill(MOBILE));
|
||||
});
|
||||
|
||||
test('la réservation d’un exclu rend sa place : elle n’entre pas dans la surréservation', () => {
|
||||
|
|
@ -234,7 +263,7 @@ describe('normaliser : exclusion (§ 4.4)', () => {
|
|||
reservations: [tous(2, 11), tous(1, 12)],
|
||||
}));
|
||||
assert.equal(instance.k, 1);
|
||||
assert.deepEqual(instance.statut, Uint8Array.from([2]));
|
||||
assert.deepEqual(instance.statut, Uint8Array.from([ANCRE]));
|
||||
assert.deepEqual(instance.fixe, Int32Array.from([1, 1]));
|
||||
assert.deepEqual(
|
||||
ecartsDeRefus([[
|
||||
|
|
@ -290,7 +319,7 @@ describe('normaliser : appartenances', () => {
|
|||
const instance = normaliser(configuration({ participants, tables: tablesDe([11], 8), tours: 1 }));
|
||||
assert.deepEqual(instance.ids, [1, 2, 4, 5, 7, 9]);
|
||||
assert.deepEqual(instance.groupes, ['Y', 'Z', 'X']);
|
||||
assert.deepEqual(instance.groupe, Int32Array.from([0, 1, 1, 2, -1, 0]));
|
||||
assert.deepEqual(instance.groupe, Int32Array.from([0, 1, 1, 2, SANS_GROUPE, 0]));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -308,7 +337,7 @@ describe('normaliser : appartenances', () => {
|
|||
];
|
||||
const instance = normaliser(configuration({ participants, tables: tablesDe([11], 8), tours: 1 }));
|
||||
assert.deepEqual(instance.groupes, ['X', 'x', 'X ', '']);
|
||||
assert.deepEqual(instance.groupe, Int32Array.from([0, 1, 2, 0, 3, -1, 3]));
|
||||
assert.deepEqual(instance.groupe, Int32Array.from([0, 1, 2, 0, 3, SANS_GROUPE, 3]));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -513,6 +542,25 @@ describe('normaliser : refus (§ 5.9, § 6.2)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('nombrePlaces (§ 5.9)', () => {
|
||||
test('somme les capacités : ni les participants, ni les exclus, ni les réservations ne la changent', () => {
|
||||
const tables = [
|
||||
{ id: 13, numero: 1, capacite: 3 },
|
||||
{ id: 11, numero: 2, capacite: 2 },
|
||||
{ id: 12, numero: 3, capacite: 7 },
|
||||
];
|
||||
const salle = (participants, reservations = []) =>
|
||||
normaliser(configuration({ participants, tables, tours: 2, reservations }));
|
||||
assert.equal(nombrePlaces(salle(personnes(suite(1, 4)))), 12);
|
||||
assert.equal(nombrePlaces(salle(personnes(suite(1, 20)))), 12);
|
||||
assert.equal(nombrePlaces(salle([], [])), 12);
|
||||
assert.equal(
|
||||
nombrePlaces(salle([...personnes(suite(1, 4)), exclu(5)], [tous(1, 13), auTour(2, 12, 2), tous(5, 11)])),
|
||||
12,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nombrePlacesManquantes (§ 5.9)', () => {
|
||||
const salle = (reservations = [], participants = personnes(suite(1, 15))) =>
|
||||
normaliser(configuration({ participants, tables: tablesDe([11, 12, 13], 4), tours: 2, reservations }));
|
||||
|
|
@ -579,8 +627,8 @@ describe('plans : forme par identifiants et forme indexée (§ 8.9)', () => {
|
|||
// reste vide à un tour, et une liste de table comme une réserve portent
|
||||
// deux ids, dont l'ordre croissant se voit.
|
||||
const TABLE_DE = [
|
||||
-1, 0,
|
||||
-1, 1,
|
||||
RESERVE, 0,
|
||||
RESERVE, 1,
|
||||
0, 2,
|
||||
2, 1,
|
||||
];
|
||||
|
|
@ -610,13 +658,13 @@ describe('plans : forme par identifiants et forme indexée (§ 8.9)', () => {
|
|||
test('indexerPlan ∘ planDepuisIndex est l’identité sur chaque plan indexé de l’instance', () => {
|
||||
const instance = instancePlans();
|
||||
const cases = instance.N * instance.R;
|
||||
// Chaque case prend une valeur de −1 (la réserve) à T − 1.
|
||||
// Chaque case prend une valeur de RESERVE, −1, à T − 1.
|
||||
const valeurs = instance.T + 1;
|
||||
let examines = 0;
|
||||
for (let rang = 0; rang < valeurs ** cases; rang += 1) {
|
||||
const tableDe = new Int32Array(cases);
|
||||
for (let i = 0, reste = rang; i < cases; i += 1, reste = Math.floor(reste / valeurs)) {
|
||||
tableDe[i] = (reste % valeurs) - 1;
|
||||
tableDe[i] = RESERVE + (reste % valeurs);
|
||||
}
|
||||
const retour = indexerPlan(instance, planDepuisIndex(instance, tableDe));
|
||||
if (retour.length !== tableDe.length || retour.some((valeur, i) => valeur !== tableDe[i])) {
|
||||
|
|
@ -719,30 +767,113 @@ describe('plans : forme par identifiants et forme indexée (§ 8.9)', () => {
|
|||
assert.deepEqual(ecartsDeRefus(cas), []);
|
||||
});
|
||||
|
||||
test('planDepuisIndex refuse un plan indexé hors de la forme de l’instance', () => {
|
||||
// TABLE_DE dont les cases nommées reçoivent une autre valeur : fautes est
|
||||
// une liste de couples [case, valeur].
|
||||
const avecFautes = (fautes) => {
|
||||
const tableDe = [...TABLE_DE];
|
||||
for (const [i, valeur] of fautes) tableDe[i] = valeur;
|
||||
return tableDe;
|
||||
};
|
||||
|
||||
// Plans indexés hors de la forme de l'instance, et le message qui les
|
||||
// refuse. Les ids [3, 6, 8, 11] occupent les index 0 à 3 ; la case p × 2 + r
|
||||
// est celle de l'index p au tour r + 1. Trois tables : un index de table va
|
||||
// de 0 à 2. Une chaîne numérique n'est pas un index, et se cite entre
|
||||
// guillemets. Deux cases fautives : la première dans l'ordre des cases est
|
||||
// nommée, la case 1 (3 au tour 2) avant la case 2 (6 au tour 1).
|
||||
const FAUTIFS = [
|
||||
['longueur N × R − 1', TABLE_DE.slice(1), 'plan indexé de 7 cases, N × R = 8 attendues'],
|
||||
['longueur N × R + 1', [...TABLE_DE, RESERVE], 'plan indexé de 9 cases, N × R = 8 attendues'],
|
||||
['longueur N × T', Array(12).fill(RESERVE), 'plan indexé de 12 cases, N × R = 8 attendues'],
|
||||
['index de table T', avecFautes([[0, 3]]), 'participant 3, tour 1, index de table 3'],
|
||||
['index −2', avecFautes([[3, -2]]), 'participant 6, tour 2, index de table -2'],
|
||||
['index non entier', avecFautes([[5, 0.5]]), 'participant 8, tour 2, index de table 0.5'],
|
||||
['index null', avecFautes([[4, null]]), 'participant 8, tour 1, index de table null'],
|
||||
['chaîne "0"', avecFautes([[6, '0']]), 'participant 11, tour 1, index de table "0"'],
|
||||
['chaîne "-1"', avecFautes([[7, '-1']]), 'participant 11, tour 2, index de table "-1"'],
|
||||
['deux cases fautives', avecFautes([[2, 9], [1, 9]]), 'participant 3, tour 2, index de table 9'],
|
||||
].map(([libelle, tableDe, message]) => [
|
||||
libelle,
|
||||
tableDe,
|
||||
message.startsWith('plan indexé de') ? message : `plan indexé : ${message} hors de −1..2`,
|
||||
]);
|
||||
|
||||
// « Nom : message » de l'erreur que lève fonction, « aucun refus » sinon.
|
||||
function erreurDe(fonction) {
|
||||
try {
|
||||
fonction();
|
||||
} catch (erreur) {
|
||||
return `${erreur?.name} : ${erreur?.message}`;
|
||||
}
|
||||
return 'aucun refus';
|
||||
}
|
||||
|
||||
test('exigerPlanIndexe accepte un plan indexé de la forme de l’instance, Int32Array ou liste, sans le modifier', () => {
|
||||
const instance = instancePlans();
|
||||
const variantes = [
|
||||
['longueur N × R − 1', TABLE_DE.slice(1)],
|
||||
['longueur N × R + 1', [...TABLE_DE, -1]],
|
||||
['longueur N × T', Array.from({ length: instance.N * instance.T }, () => -1)],
|
||||
['index de table T', TABLE_DE.map((valeur, i) => (i === 0 ? instance.T : valeur))],
|
||||
['index −2', TABLE_DE.map((valeur, i) => (i === 3 ? -2 : valeur))],
|
||||
['index non entier', TABLE_DE.map((valeur, i) => (i === 5 ? 0.5 : valeur))],
|
||||
];
|
||||
assert.ok(variantes.length > 0, 'aucune variante examinée');
|
||||
assert.equal(exigerPlanIndexe(instance, Int32Array.from(TABLE_DE)), undefined);
|
||||
assert.equal(exigerPlanIndexe(instance, geler([...TABLE_DE])), undefined);
|
||||
});
|
||||
|
||||
test('exigerPlanIndexe refuse un plan indexé hors forme : RangeError qui nomme le participant, le tour et la valeur de la première case fautive', () => {
|
||||
const instance = instancePlans();
|
||||
assert.ok(FAUTIFS.length > 0, 'aucune variante examinée');
|
||||
const ecarts = [];
|
||||
for (const [libelle, tableDe] of variantes) {
|
||||
try {
|
||||
planDepuisIndex(instance, tableDe);
|
||||
ecarts.push(`${libelle} : aucun refus`);
|
||||
} catch (erreur) {
|
||||
if (!(erreur instanceof RangeError)) {
|
||||
ecarts.push(`${libelle} : ${erreur?.name} au lieu de RangeError`);
|
||||
}
|
||||
}
|
||||
for (const [libelle, tableDe, message] of FAUTIFS) {
|
||||
const recue = erreurDe(() => exigerPlanIndexe(instance, tableDe));
|
||||
const attendue = `RangeError : ${message}`;
|
||||
if (recue !== attendue) ecarts.push(`${libelle} : « ${recue} » au lieu de « ${attendue} »`);
|
||||
}
|
||||
assert.deepEqual(ecarts, []);
|
||||
});
|
||||
|
||||
test('une seule garde : planDepuisIndex, mesurer et plafondsRealises lèvent l’erreur de exigerPlanIndexe, au même message', () => {
|
||||
const instance = instancePlans();
|
||||
const appelants = [
|
||||
['planDepuisIndex', (tableDe) => planDepuisIndex(instance, tableDe)],
|
||||
['mesurer', (tableDe) => mesurer(instance, tableDe)],
|
||||
['plafondsRealises', (tableDe) => plafondsRealises(instance, tableDe)],
|
||||
];
|
||||
const ecarts = [];
|
||||
let confrontees = 0;
|
||||
for (const [libelle, tableDe] of FAUTIFS) {
|
||||
const attendue = erreurDe(() => exigerPlanIndexe(instance, tableDe));
|
||||
for (const [nom, appeler] of appelants) {
|
||||
confrontees += 1;
|
||||
const recue = erreurDe(() => appeler(tableDe));
|
||||
if (recue !== attendue) ecarts.push(`${nom}, ${libelle} : « ${recue} » au lieu de « ${attendue} »`);
|
||||
}
|
||||
}
|
||||
assert.equal(confrontees, 3 * FAUTIFS.length);
|
||||
assert.deepEqual(ecarts, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replierParPopulation (§ 5.4)', () => {
|
||||
test('chacun compte dans tous, puis parmi les ancrés ou parmi les mobiles, partiellement fixés compris, dans l’ordre canonique', () => {
|
||||
const statut = Uint8Array.from([ANCRE, MOBILE, PARTIELLEMENT_FIXE, ANCRE, MOBILE, PARTIELLEMENT_FIXE]);
|
||||
const enListe = (cumul, valeur) => [...cumul, valeur];
|
||||
assert.deepEqual(replierParPopulation([10, 11, 12, 13, 14, 15], statut, [], enListe), {
|
||||
tous: [10, 11, 12, 13, 14, 15],
|
||||
mobiles: [11, 12, 14, 15],
|
||||
ancres: [10, 13],
|
||||
});
|
||||
});
|
||||
|
||||
test('les trois populations partent de la valeur initiale ; une population sans membre la garde', () => {
|
||||
const plusGrand = (cumul, valeur) => (cumul === null || valeur > cumul ? valeur : cumul);
|
||||
assert.deepEqual(
|
||||
replierParPopulation([4, 9], Uint8Array.from([MOBILE, PARTIELLEMENT_FIXE]), null, plusGrand),
|
||||
{ tous: 9, mobiles: 9, ancres: null },
|
||||
);
|
||||
assert.deepEqual(
|
||||
replierParPopulation([3], Uint8Array.from([ANCRE]), null, plusGrand),
|
||||
{ tous: 3, mobiles: null, ancres: 3 },
|
||||
);
|
||||
assert.deepEqual(
|
||||
replierParPopulation([], new Uint8Array(0), 'vide', plusGrand),
|
||||
{ tous: 'vide', mobiles: 'vide', ancres: 'vide' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('erreurs du moteur', () => {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@
|
|||
// ailleurs, jamais 0 par défaut.
|
||||
//
|
||||
// Les quantités que le moteur calcule déjà viennent de leur unique
|
||||
// implémentation (§ 13.2) : places manquantes de configuration.js, plafond a
|
||||
// priori de plafond.js, retours imposés de la mesure d'indicateurs.js. Le
|
||||
// diagnostic ne modifie pas la configuration et lève ce que normaliser lève.
|
||||
// implémentation (§ 13.2) : nombre de places et places manquantes de
|
||||
// configuration.js, plafond a priori de plafond.js, retours imposés de la
|
||||
// mesure d'indicateurs.js. Le diagnostic ne modifie pas la configuration et
|
||||
// lève ce que normaliser lève.
|
||||
|
||||
/**
|
||||
* @typedef {Object} PlancherCollisions
|
||||
|
|
@ -39,19 +40,17 @@
|
|||
* @property {number} retoursImposes
|
||||
* @property {{plancher: number, mobilesAuMoins: number}|null} ecartItineraire
|
||||
*/
|
||||
import { nombrePlacesManquantes, normaliser } from './configuration.js';
|
||||
import {
|
||||
LIBRE,
|
||||
SANS_GROUPE,
|
||||
STATUT,
|
||||
nombrePlaces,
|
||||
nombrePlacesManquantes,
|
||||
normaliser,
|
||||
} from './configuration.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
import { plafondsAPriori } from './plafond.js';
|
||||
|
||||
// Statuts d'un participant dans instance.statut (§ 4.2).
|
||||
const MOBILE = 0;
|
||||
const PARTIELLEMENT_FIXE = 1;
|
||||
const ANCRE = 2;
|
||||
// Case de instance.fixe sans table imposée.
|
||||
const LIBRE = -1;
|
||||
// instance.groupe d'un participant sans appartenance.
|
||||
const SANS_GROUPE = -1;
|
||||
|
||||
// Paires que forment n personnes : C(n, 2).
|
||||
const paires = (n) => (n * (n - 1)) / 2;
|
||||
|
||||
|
|
@ -145,7 +144,7 @@ function animateursMemeAppartenance({ N, statut, groupe }) {
|
|||
let commun = SANS_GROUPE;
|
||||
let animateurs = 0;
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
if (statut[p] !== ANCRE) continue;
|
||||
if (statut[p] !== STATUT.ANCRE) continue;
|
||||
if (groupe[p] === SANS_GROUPE || (animateurs > 0 && groupe[p] !== commun)) return false;
|
||||
commun = groupe[p];
|
||||
animateurs += 1;
|
||||
|
|
@ -205,10 +204,8 @@ function plafondPassantPar(configuration, instance, p, t) {
|
|||
// s'ajoutent au plafond hors du terme que n_p borne.
|
||||
function ecartItineraire(configuration, instance, aPriori) {
|
||||
const { N, T, R, capacite, ancresParTable, statut } = instance;
|
||||
let places = 0;
|
||||
for (let t = 0; t < T; t += 1) places += capacite[t];
|
||||
if (places !== N || statut.includes(PARTIELLEMENT_FIXE)) return null;
|
||||
const mobile = statut.indexOf(MOBILE);
|
||||
if (nombrePlaces(instance) !== N || statut.includes(STATUT.PARTIELLEMENT_FIXE)) return null;
|
||||
const mobile = statut.indexOf(STATUT.MOBILE);
|
||||
if (mobile === -1) return null;
|
||||
const plafondCommun = aPriori[mobile];
|
||||
const parGabarit = new Map();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { CATALOGUE, PLAN_PARFAIT_PETITE } from '../demo/catalogue.js';
|
||||
import { indexerPlan, normaliser } from './configuration.js';
|
||||
import { LIBRE, STATUT, indexerPlan, normaliser } from './configuration.js';
|
||||
import { diagnostiquer } from './diagnostic.js';
|
||||
import { ErreurConfiguration } from './erreurs.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
|
|
@ -24,10 +24,6 @@ const SANS_CONTRAINTE = {
|
|||
varierAppartenances: false,
|
||||
};
|
||||
|
||||
// instance.statut d'un mobile, d'un ancré.
|
||||
const MOBILE = 0;
|
||||
const ANCRE = 2;
|
||||
|
||||
/**
|
||||
* Configuration écrite à la main. tables porte une paire [id, capacité] par
|
||||
* table, dans l'ordre de la configuration, le numéro suivant la position ;
|
||||
|
|
@ -81,7 +77,7 @@ function enumererPlans(instance, visiter) {
|
|||
return;
|
||||
}
|
||||
const r = i % R;
|
||||
for (const t of fixe[i] === -1 ? toutes : [fixe[i]]) {
|
||||
for (const t of fixe[i] === LIBRE ? toutes : [fixe[i]]) {
|
||||
if (occupation[t * R + r] === capacite[t]) continue;
|
||||
occupation[t * R + r] += 1;
|
||||
tableDe[i] = t;
|
||||
|
|
@ -133,7 +129,7 @@ function ecartsItineraireSurTousLesPlans(configuration, seuil) {
|
|||
for (let p = 0; p < instance.N; p += 1) {
|
||||
const ecartItineraire = aPriori[p] - realises[p];
|
||||
maximum = Math.max(maximum, ecartItineraire);
|
||||
if (instance.statut[p] !== ANCRE && ecartItineraire >= seuil) mobiles += 1;
|
||||
if (instance.statut[p] !== STATUT.ANCRE && ecartItineraire >= seuil) mobiles += 1;
|
||||
}
|
||||
plusPetitMaximum = Math.min(plusPetitMaximum, maximum);
|
||||
moinsDeMobiles = Math.min(moinsDeMobiles, mobiles);
|
||||
|
|
@ -152,7 +148,7 @@ function ecartsItineraireSurTousLesPlans(configuration, seuil) {
|
|||
function meilleursItineraires(configuration) {
|
||||
const instance = normaliser(configuration);
|
||||
const { T, R, capacite, ancresParTable } = instance;
|
||||
const mobile = instance.statut.indexOf(MOBILE);
|
||||
const mobile = instance.statut.indexOf(STATUT.MOBILE);
|
||||
assert.notEqual(mobile, -1, 'aucun mobile');
|
||||
const admissibles = [];
|
||||
for (let t = 0; t < T; t += 1) if (ancresParTable[t] < capacite[t]) admissibles.push(t);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@
|
|||
// sans appartenance ne sont pas collègues. Un ensemble vide rend null, jamais
|
||||
// 0 : le taux de diversité de qui ne rencontre aucun affilié, le minimum et la
|
||||
// moyenne d'une population sans membre.
|
||||
import {
|
||||
RESERVE,
|
||||
SANS_GROUPE,
|
||||
exigerPlanIndexe,
|
||||
replierParPopulation,
|
||||
} from './configuration.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Agregat
|
||||
|
|
@ -39,6 +45,9 @@
|
|||
* @property {number[]} retoursImposes par personne
|
||||
* @property {TroisAgregats} aggRencontres
|
||||
* @property {TroisAgregats} aggRedondance
|
||||
* @property {number} totalRedondance Σ_p r(p), le seul total de la
|
||||
* redondance : la recherche et le
|
||||
* classement le lisent
|
||||
* @property {number} collisionsCumulees Σ paires de même groupe × tours ensemble
|
||||
* @property {number} pairesDistinctes paires de même groupe réunies ≥ 1 fois
|
||||
* @property {number} excedentCollisions cumulées − distinctes
|
||||
|
|
@ -53,44 +62,9 @@
|
|||
* ordre de instance.groupes
|
||||
*/
|
||||
|
||||
// Case d'un plan indexé : la réserve.
|
||||
const RESERVE = -1;
|
||||
// instance.groupe d'un participant sans appartenance.
|
||||
const SANS_GROUPE = -1;
|
||||
// instance.statut d'un ancré ; 0 marque un mobile, 1 un partiellement fixé.
|
||||
const ANCRE = 2;
|
||||
// Plus grand compte que tient un Uint16Array.
|
||||
const UINT16_MAX = 0xffff;
|
||||
|
||||
// Valeur citée dans un message : une chaîne s'écrit entre guillemets, pour
|
||||
// ne pas se lire comme le nombre qu'elle contient.
|
||||
const decrire = (valeur) => (typeof valeur === 'string' ? JSON.stringify(valeur) : String(valeur));
|
||||
|
||||
// Lève RangeError quand tableDe n'a pas N × R cases, ou qu'une case n'est ni
|
||||
// −1 ni un index de table entier ; le message nomme alors le participant et
|
||||
// le tour. Sans cette garde, deux écarts passeraient sans bruit : une case de
|
||||
// trop serait ignorée ; une chaîne numérique comme "0" assiérait la personne
|
||||
// à la table qu'elle désigne, mais compterRetours, qui la compare strictement
|
||||
// à instance.fixe, n'y reconnaîtrait pas sa réservation. Une case manquante
|
||||
// ou toute autre valeur lèverait dans compterRencontres une TypeError qui ne
|
||||
// nomme ni la personne ni le tour.
|
||||
function exigerForme({ N, T, R, ids }, tableDe) {
|
||||
if (tableDe.length !== N * R) {
|
||||
throw new RangeError(`plan indexé de ${tableDe.length} cases, N × R = ${N * R} attendues`);
|
||||
}
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
const t = tableDe[p * R + r];
|
||||
if (t !== RESERVE && !(Number.isInteger(t) && t >= 0 && t < T)) {
|
||||
throw new RangeError(
|
||||
`plan indexé : participant ${ids[p]}, tour ${r + 1}, `
|
||||
+ `index de table ${decrire(t)} hors de −1..${T - 1}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rang de la paire (a, b), a < b, dans le triangle supérieur d'une matrice
|
||||
// N × N rangé ligne par ligne : la ligne a s'ouvre après les
|
||||
// (N − 1) + (N − 2) + … + (N − a) paires des lignes qui la précèdent.
|
||||
|
|
@ -232,13 +206,16 @@ function compterRetours({ N, T, R, fixe }, tableDe) {
|
|||
return { toursAssis, retoursChoisis, retoursImposes };
|
||||
}
|
||||
|
||||
// Ajoute une valeur à une population. min reste null tant que la population
|
||||
// est vide : aucun Infinity ne sert de minimum initial.
|
||||
function ajouter(population, valeur) {
|
||||
if (population.min === null || valeur < population.min) population.min = valeur;
|
||||
population.somme += valeur;
|
||||
population.effectif += 1;
|
||||
}
|
||||
// Population sans membre. min reste null tant qu'aucune valeur n'y entre :
|
||||
// aucun Infinity ne sert de minimum initial.
|
||||
const VIDE = Object.freeze({ min: null, somme: 0, effectif: 0 });
|
||||
|
||||
// La population, une valeur de plus ; celle qu'elle reçoit reste intacte.
|
||||
const ajouter = ({ min, somme, effectif }, valeur) => ({
|
||||
min: min === null || valeur < min ? valeur : min,
|
||||
somme: somme + valeur,
|
||||
effectif: effectif + 1,
|
||||
});
|
||||
|
||||
// Agrégat d'une population ; la moyenne d'une population vide vaut null.
|
||||
const conclure = ({ min, somme, effectif }) => ({
|
||||
|
|
@ -247,17 +224,10 @@ const conclure = ({ min, somme, effectif }) => ({
|
|||
effectif,
|
||||
});
|
||||
|
||||
// Minimum, moyenne et effectif de valeurs[p] sur les trois populations, en un
|
||||
// passage : chacun compte dans « tous », puis parmi les ancrés ou parmi les
|
||||
// mobiles, qui comprennent les partiellement fixés (§ 5.4).
|
||||
// Minimum, moyenne et effectif de valeurs[p] sur les trois populations du
|
||||
// § 5.4, que replierParPopulation forme.
|
||||
function troisAgregats(valeurs, statut) {
|
||||
const tous = { min: null, somme: 0, effectif: 0 };
|
||||
const mobiles = { min: null, somme: 0, effectif: 0 };
|
||||
const ancres = { min: null, somme: 0, effectif: 0 };
|
||||
for (let p = 0; p < valeurs.length; p += 1) {
|
||||
ajouter(tous, valeurs[p]);
|
||||
ajouter(statut[p] === ANCRE ? ancres : mobiles, valeurs[p]);
|
||||
}
|
||||
const { tous, mobiles, ancres } = replierParPopulation(valeurs, statut, VIDE, ajouter);
|
||||
return { tous: conclure(tous), mobiles: conclure(mobiles), ancres: conclure(ancres) };
|
||||
}
|
||||
|
||||
|
|
@ -265,15 +235,17 @@ const somme = (valeurs) => valeurs.reduce((total, valeur) => total + valeur, 0);
|
|||
|
||||
/**
|
||||
* Mesure complète d'un plan indexé : la mesure qui fait foi (§ 5.10). Ne
|
||||
* modifie ni l'instance ni le plan. Lève RangeError quand tableDe n'a pas
|
||||
* N × R cases, ou qu'une case n'est ni −1 ni un index de table.
|
||||
* modifie ni l'instance ni le plan. Lève ce que lève exigerPlanIndexe
|
||||
* (configuration.js) avant de lire une case : une case manquante ou d'une
|
||||
* autre forme lèverait plus loin une erreur qui ne nommerait ni la personne
|
||||
* ni le tour.
|
||||
*
|
||||
* @param {import('./types.js').Instance} instance
|
||||
* @param {ArrayLike<number>} tableDe tableDe[p * R + r] = index de table, −1 pour la réserve
|
||||
* @param {ArrayLike<number>} tableDe tableDe[p * R + r] = index de table, RESERVE pour la réserve
|
||||
* @returns {Mesures}
|
||||
*/
|
||||
export function mesurer(instance, tableDe) {
|
||||
exigerForme(instance, tableDe);
|
||||
exigerPlanIndexe(instance, tableDe);
|
||||
const { N, groupe, groupes, statut } = instance;
|
||||
const paires = lirePaires(instance, compterRencontres(instance, tableDe));
|
||||
const { collisions, distinctes } = paires;
|
||||
|
|
@ -311,6 +283,7 @@ export function mesurer(instance, tableDe) {
|
|||
retoursImposes,
|
||||
aggRencontres: troisAgregats(rencontres, statut),
|
||||
aggRedondance: troisAgregats(redondance, statut),
|
||||
totalRedondance: somme(redondance),
|
||||
collisionsCumulees,
|
||||
pairesDistinctes,
|
||||
excedentCollisions: collisionsCumulees - pairesDistinctes,
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ describe('mesurer', () => {
|
|||
mobiles: { min: 6, moyenne: 6, effectif: 12 },
|
||||
ancres: AUCUN,
|
||||
},
|
||||
totalRedondance: 72,
|
||||
collisionsCumulees: 0,
|
||||
pairesDistinctes: 0,
|
||||
excedentCollisions: 0,
|
||||
|
|
@ -236,6 +237,21 @@ describe('mesurer', () => {
|
|||
assert.deepEqual(mesures.rencontresRepetees, { choisies: 1, imposees: 0 });
|
||||
});
|
||||
|
||||
test('totalRedondance somme r(p) sur les personnes, et non son plus grand', () => {
|
||||
// Le plan de l'épreuve précédente : r(p) vaut 1, 0, 1, 0 et 0. Le total
|
||||
// est le seul Σ r(p) du moteur ; la recherche et le classement le lisent.
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: ['X', 'X', 'Y', 'X', null],
|
||||
capacite: 3,
|
||||
tours: [
|
||||
[[1, 2, 3], [4], [], [5]],
|
||||
[[], [1, 4], [2, 3], [5]],
|
||||
],
|
||||
});
|
||||
assert.deepEqual(mesures.redondance, [1, 0, 1, 0, 0]);
|
||||
assert.equal(mesures.totalRedondance, 2);
|
||||
});
|
||||
|
||||
test("F(p) ne retient que les affiliés : qui ne croise aucun affilié n'a pas de taux de diversité", () => {
|
||||
// 3, seul affilié de la table, rencontre deux personnes sans
|
||||
// appartenance ; chacune d'elles rencontre un seul affilié, 3.
|
||||
|
|
@ -505,6 +521,7 @@ describe('mesurer', () => {
|
|||
retoursImposes: [],
|
||||
aggRencontres: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
||||
aggRedondance: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
||||
totalRedondance: 0,
|
||||
collisionsCumulees: 0,
|
||||
pairesDistinctes: 0,
|
||||
excedentCollisions: 0,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
// © 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
|
||||
// Épreuves d'intégration lourdes du moteur. D'abord 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).
|
||||
// posés après mesure, la mesure en commentaire (§ 14.1). Puis la grille de
|
||||
// propriétés du § 14.12, sur des configurations tirées.
|
||||
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 { FLUX, creerPcg32 } from '../demo/prng.js';
|
||||
import { STATUT, indexerPlan, nombrePlaces, normaliser } from './configuration.js';
|
||||
import { diagnostiquer } from './diagnostic.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
import {
|
||||
|
|
@ -22,12 +24,17 @@ import {
|
|||
troisChiffres,
|
||||
} from './manque.js';
|
||||
import { plafondsAPriori, plafondsRealises } from './plafond.js';
|
||||
import { rechercher } from './recherche.js';
|
||||
import {
|
||||
creerEtat,
|
||||
defaire,
|
||||
proposerEtAppliquer,
|
||||
rechercher,
|
||||
regenerer,
|
||||
scoreComplet,
|
||||
scoreIncremental,
|
||||
} 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
|
||||
|
|
@ -80,6 +87,17 @@ 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));
|
||||
|
||||
// Nombre de personnes non ancrées dont l'écart d'itinéraire, ecarts[p] dans
|
||||
// l'ordre canonique, atteint plancher. Le diagnostic en écrit le minimum,
|
||||
// mobilesAuMoins, à côté de plancher.
|
||||
function mobilesAuPlancher(instance, ecarts, plancher) {
|
||||
let compte = 0;
|
||||
for (let p = 0; p < instance.N; p += 1) {
|
||||
if (instance.statut[p] !== STATUT.ANCRE && ecarts[p] >= plancher) compte += 1;
|
||||
}
|
||||
return compte;
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -157,10 +175,7 @@ describe('intégration : la grande démonstration de bout en bout (§ 5.5, § 12
|
|||
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;
|
||||
}
|
||||
const mobilesSous = mobilesAuPlancher(instance, ecarts, plancher);
|
||||
assert.ok(
|
||||
mobilesSous >= mobilesAuMoins,
|
||||
`proposition ${id} : ${mobilesSous} mobiles sous leur plafond a priori, au moins ${mobilesAuMoins} attendus`,
|
||||
|
|
@ -200,3 +215,326 @@ describe('intégration : la grande démonstration de bout en bout (§ 5.5, § 12
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Grille de propriétés (§ 14.12). Les quatre démonstrations ont quatre
|
||||
// tours, chacun y porte une appartenance, aucune n'a de partiellement fixé
|
||||
// ni d'exclu, et une seule laisse des places vides : la grille tire des
|
||||
// configurations hors de ce cadre, d'une graine écrite ici. Les propriétés
|
||||
// éprouvées valent pour tout plan que rend la recherche, quelle qu'en soit
|
||||
// la qualité : un compte d'arrêt bref suffit. Un historique court fait jouer
|
||||
// la bascule et la relance de la descente : avec cette graine, 86 des 460
|
||||
// propositions basculent dans l'ordre des contraintes, et 74 relancent leur
|
||||
// descente au moins une fois.
|
||||
const GRILLE = Object.freeze({ graine: 1_729, configurations: 300, arret: 3_000, nombre: 2 });
|
||||
|
||||
// Une salle sur cinq est trop petite, deux sont tendues, deux ont des places
|
||||
// vides.
|
||||
const SALLES = Object.freeze(['tendue', 'tendue', 'places vides', 'places vides', 'trop petite']);
|
||||
const LIBELLES = Object.freeze(['A', 'B', 'C']);
|
||||
|
||||
// Mouvements appliqués où la marche confronte le score tenu à jour au score
|
||||
// recalculé, après le placement initial ; un mouvement appliqué sur trois est
|
||||
// défait, comme dans recherche.long.test.js.
|
||||
const PALIERS = Object.freeze([1, 10, 100, 1_000]);
|
||||
const DEFAIT_TOUS_LES = 3;
|
||||
|
||||
// k entiers distincts de 0 à n − 1, tirés sans remise par le mélange
|
||||
// partiel de Fisher et Yates.
|
||||
function tirerSansRemise(rng, n, k) {
|
||||
const pile = Array.from({ length: n }, (_, i) => i);
|
||||
for (let i = 0; i < k; i += 1) {
|
||||
const j = i + rng.borne(n - i);
|
||||
[pile[i], pile[j]] = [pile[j], pile[i]];
|
||||
}
|
||||
return pile.slice(0, k);
|
||||
}
|
||||
|
||||
// Une configuration de la grille, ses réglages de génération, sa salle et le
|
||||
// nombre de places qui lui manquent, tirés de rng :
|
||||
// - de 2 à 6 tables de 2 à 6 sièges, de 1 à 5 tours ;
|
||||
// - N = S personnes présentes dans une salle tendue, S = Σ c_t ; S − v, v de
|
||||
// 1 à ⌊S / 2⌋, dans une salle à places vides ; S + 1 à S + 3 dans une salle
|
||||
// trop petite ;
|
||||
// - chacun une fois sur deux : des ancrés, des réservations d'une partie des
|
||||
// tours, un ou deux exclus. Une réservation d'une partie des tours fait un
|
||||
// partiellement fixé quand R ≥ 2, parfois deux fois à la même table, et un
|
||||
// ancré par la portée « tour » quand R = 1 ;
|
||||
// - de 1 à 3 appartenances, et des personnes sans appartenance ;
|
||||
// - des identifiants de participant croissants à trous, des identifiants de
|
||||
// table dans un ordre quelconque, une liste de participants mélangée ;
|
||||
// - une réservation d'exclu une fois sur deux, suspendue et sans siège, sa
|
||||
// table fût-elle pleine (§ 4.4), et une réservation en double une fois sur
|
||||
// quatre ;
|
||||
// - chaque contrainte active trois fois sur quatre ; la graine de la
|
||||
// génération, et un historique de 16 à 64 cases.
|
||||
// Une réservation de personne présente ne se pose que sur une table où il
|
||||
// reste un siège à chacun de ses tours : la configuration ne lève jamais
|
||||
// SURRESERVATION, et normaliser l'accepte toujours.
|
||||
function tirerConfiguration(rng) {
|
||||
const T = 2 + rng.borne(5);
|
||||
const capacites = Array.from({ length: T }, () => 2 + rng.borne(5));
|
||||
const R = 1 + rng.borne(5);
|
||||
const S = capacites.reduce((somme, c) => somme + c, 0);
|
||||
const salle = SALLES[rng.borne(SALLES.length)];
|
||||
let N = S;
|
||||
if (salle === 'places vides') N = S - 1 - rng.borne(Math.floor(S / 2));
|
||||
if (salle === 'trop petite') N = S + 1 + rng.borne(3);
|
||||
const avecAncres = rng.borne(2) === 0;
|
||||
const avecToursReserves = rng.borne(2) === 0;
|
||||
const exclus = rng.borne(2) === 0 ? 1 + rng.borne(2) : 0;
|
||||
const G = 1 + rng.borne(LIBELLES.length);
|
||||
|
||||
const ids = [];
|
||||
for (let i = 0, id = 0; i < N + exclus; i += 1) {
|
||||
id += 1 + rng.borne(2);
|
||||
ids.push(id);
|
||||
}
|
||||
const idsExclus = new Set(tirerSansRemise(rng, ids.length, exclus).map((i) => ids[i]));
|
||||
const participants = ids.map((id) => {
|
||||
const g = rng.borne(G + 1);
|
||||
return {
|
||||
id,
|
||||
nom: `P${id}`,
|
||||
appartenance: g < G ? LIBELLES[g] : null,
|
||||
exclu: idsExclus.has(id),
|
||||
};
|
||||
});
|
||||
const idsTables = [];
|
||||
for (let t = 0, id = 0; t < T; t += 1) {
|
||||
id += 1 + rng.borne(3);
|
||||
idsTables.push(id);
|
||||
}
|
||||
rng.melanger(idsTables);
|
||||
const tables = idsTables.map((id, t) => ({ id, numero: t + 1, capacite: capacites[t] }));
|
||||
|
||||
// fixees[t × R + r] : les personnes présentes que les réservations
|
||||
// assoient à la table t au tour r.
|
||||
const fixees = new Int32Array(T * R);
|
||||
const siegeLibre = (t, r) => fixees[t * R + r] < capacites[t];
|
||||
const tousLesTours = Array.from({ length: R }, (_, r) => r);
|
||||
const reservations = [];
|
||||
for (const { id, exclu } of participants) {
|
||||
const role = rng.borne(6);
|
||||
if (exclu) {
|
||||
if (role < 3) {
|
||||
const table = idsTables[rng.borne(T)];
|
||||
const portee = role === 0 ? { portee: 'tous' } : { portee: 'tour', tour: 1 + rng.borne(R) };
|
||||
reservations.push({ participant: id, table, ...portee });
|
||||
}
|
||||
} else if (avecAncres && role === 0) {
|
||||
const t = rng.borne(T);
|
||||
if (tousLesTours.every((r) => siegeLibre(t, r))) {
|
||||
for (const r of tousLesTours) fixees[t * R + r] += 1;
|
||||
reservations.push({ participant: id, table: idsTables[t], portee: 'tous' });
|
||||
}
|
||||
} else if (avecToursReserves && role === 1) {
|
||||
const tours = R === 1 ? 1 : 1 + rng.borne(R - 1);
|
||||
for (const r of tirerSansRemise(rng, R, tours)) {
|
||||
const t = rng.borne(T);
|
||||
if (!siegeLibre(t, r)) continue;
|
||||
fixees[t * R + r] += 1;
|
||||
reservations.push({ participant: id, table: idsTables[t], portee: 'tour', tour: r + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reservations.length > 0 && rng.borne(4) === 0) {
|
||||
reservations.push({ ...reservations[rng.borne(reservations.length)] });
|
||||
}
|
||||
rng.melanger(participants);
|
||||
const contraintes = {
|
||||
separerAppartenances: rng.borne(4) > 0,
|
||||
nouveauxVoisins: rng.borne(4) > 0,
|
||||
nouvelleTable: rng.borne(4) > 0,
|
||||
varierAppartenances: rng.borne(4) > 0,
|
||||
};
|
||||
const reglages = {
|
||||
graine: rng.suivant(),
|
||||
arret: GRILLE.arret,
|
||||
nombre: GRILLE.nombre,
|
||||
historique: 16 + rng.borne(49),
|
||||
};
|
||||
return {
|
||||
configuration: { participants, tables, tours: R, reservations, contraintes },
|
||||
reglages,
|
||||
salle,
|
||||
placesManquantes: Math.max(0, N - S),
|
||||
};
|
||||
}
|
||||
|
||||
// Une marche depuis le placement initial de la proposition de graine
|
||||
// dérivée graine, mouvements tirés du même générateur que sa descente :
|
||||
// chaque palier ajoute à fautes un désaccord entre le score tenu à jour et le
|
||||
// score recalculé. Rend vrai quand la marche atteint le dernier palier. Une
|
||||
// configuration où les mouvements ne s'appliquent pas l'arrête plus tôt :
|
||||
// celle dont chaque tour n'a de sièges mobiles qu'à une seule table, ou n'y
|
||||
// a que des places fantômes.
|
||||
function marcher(nom, instance, graine, fautes) {
|
||||
const rng = creerPcg32(graine, FLUX.RECHERCHE);
|
||||
const etat = creerEtat(instance, rng);
|
||||
const confronter = (appliques) => {
|
||||
const tenu = scoreIncremental(etat);
|
||||
const recalcule = scoreComplet(etat);
|
||||
if (tenu.some((valeur, k) => valeur !== recalcule[k])) {
|
||||
fautes.push(`${nom}, ${appliques} mouvements : score tenu ${tenu}, recalculé ${recalcule}`);
|
||||
}
|
||||
};
|
||||
confronter(0);
|
||||
let appliques = 0;
|
||||
let palier = 0;
|
||||
for (let essai = 0; palier < PALIERS.length && essai < 100 * PALIERS.at(-1); essai += 1) {
|
||||
const mouvement = proposerEtAppliquer(etat, rng);
|
||||
if (mouvement === null) continue;
|
||||
appliques += 1;
|
||||
if (appliques % DEFAIT_TOUS_LES === 0) defaire(etat, mouvement);
|
||||
if (appliques === PALIERS[palier]) {
|
||||
confronter(appliques);
|
||||
palier += 1;
|
||||
}
|
||||
}
|
||||
return palier === PALIERS.length;
|
||||
}
|
||||
|
||||
// Ajoute à fautes ce qu'une proposition enfreint : les deux vérificateurs,
|
||||
// les retours imposés du diagnostic, ses planchers de collisions et d'écart
|
||||
// d'itinéraire, et sa régénération à partir de sa graine dérivée, de son
|
||||
// compte d'arrêt et de son historique, sans son plan ni son identifiant. Ses
|
||||
// mesures ne se lisent que sur un plan sans violation.
|
||||
function juger(nom, { configuration, instance, aPriori, diagnostic }, proposition, fautes) {
|
||||
const { graine, arret, historique, plan } = proposition;
|
||||
const violations = verifierInvariants(instance, plan);
|
||||
if (violations.length > 0) {
|
||||
fautes.push(`${nom} : ${JSON.stringify(violations)}`);
|
||||
return;
|
||||
}
|
||||
const tableDe = indexerPlan(instance, plan);
|
||||
const mesures = mesurer(instance, tableDe);
|
||||
const realises = plafondsRealises(instance, tableDe);
|
||||
const indicateurs = verifierIndicateurs(mesures, aPriori, realises);
|
||||
if (indicateurs.length > 0) fautes.push(`${nom} : ${JSON.stringify(indicateurs)}`);
|
||||
if (mesures.totalRetoursImposes !== diagnostic.retoursImposes) {
|
||||
fautes.push(`${nom} : ${mesures.totalRetoursImposes} retours imposés, ${diagnostic.retoursImposes} au diagnostic`);
|
||||
}
|
||||
for (const plancher of diagnostic.collisions) {
|
||||
const mesure = mesures.parGroupe.find(({ groupe }) => groupe === plancher.groupe);
|
||||
if (mesure.collisionsCumulees < plancher.plancherCumulees
|
||||
|| mesure.pairesDistinctes < plancher.plancherPairesDistinctes
|
||||
|| (plancher.plancherExcedent !== null && mesure.excedent < plancher.plancherExcedent)) {
|
||||
fautes.push(
|
||||
`${nom}, groupe ${plancher.groupe} : collisions ${mesure.collisionsCumulees} sur `
|
||||
+ `${mesure.pairesDistinctes} paires, excédent ${mesure.excedent}, sous le plancher `
|
||||
+ JSON.stringify(plancher),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (diagnostic.ecartItineraire !== null) {
|
||||
const { plancher, mobilesAuMoins } = diagnostic.ecartItineraire;
|
||||
const { ecartItineraireMax } = troisChiffres(instance, mesures, aPriori, realises);
|
||||
const mobilesSous = mobilesAuPlancher(instance, ecartsItineraire(aPriori, realises), plancher);
|
||||
if (ecartItineraireMax.tous < plancher || mobilesSous < mobilesAuMoins) {
|
||||
fautes.push(
|
||||
`${nom} : écart d'itinéraire maximal ${ecartItineraireMax.tous}, ${mobilesSous} mobiles `
|
||||
+ `à au moins ${plancher}, sous le plancher ${JSON.stringify(diagnostic.ecartItineraire)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (JSON.stringify(regenerer(configuration, { graine, arret, historique })) !== JSON.stringify(plan)) {
|
||||
fautes.push(`${nom} : la régénération rend un autre plan`);
|
||||
}
|
||||
}
|
||||
|
||||
// Planchers de la grille (§ 14.2) : ce que la boucle doit avoir éprouvé pour
|
||||
// que ses propriétés disent quelque chose. Un générateur qui cesserait de
|
||||
// tirer l'un de ces cas laisserait les propriétés vraies sans les éprouver.
|
||||
// Les comptes lisent donc ce que la boucle a éprouvé, l'instance, le
|
||||
// diagnostic ou la marche, et non l'étiquette que le générateur a tirée.
|
||||
// Seules les salles trop petites se comptent sur leur étiquette : une salle
|
||||
// ainsi étiquetée qui ne l'est pas fait échouer la boucle, qui exige son
|
||||
// refus. Chaque plancher est posé sous le compte que donne la graine écrite,
|
||||
// en commentaire.
|
||||
const PLANCHERS = Object.freeze([
|
||||
['configurations éprouvées', 'eprouvees', 200], // 230 sur 300
|
||||
['salles trop petites refusées', 'tropPetites', 50], // 70
|
||||
['salles tendues', 'tendues', 90], // 120
|
||||
['salles à places vides', 'placesVides', 80], // 110
|
||||
['avec ancrés', 'ancres', 90], // 122
|
||||
['avec partiellement fixés', 'partiellementFixes', 60], // 87
|
||||
['avec exclus', 'exclus', 90], // 122
|
||||
['à un seul tour', 'unTour', 30], // 47
|
||||
['avec retours imposés', 'retoursImposes', 70], // 103
|
||||
['avec plancher de collisions', 'plancherCollisions', 120], // 173
|
||||
["avec plancher d'excédent", 'plancherExcedent', 10], // 15
|
||||
["avec plancher d'écart d'itinéraire", 'plancherEcartItineraire', 35], // 50
|
||||
['marches au dernier palier', 'marches', 400], // 460, une par proposition
|
||||
]);
|
||||
|
||||
describe('intégration : grille de propriétés sur des configurations tirées (§ 14.12)', () => {
|
||||
test(`${GRILLE.configurations} configurations : chaque proposition tient les deux vérificateurs, les retours imposés et les planchers du diagnostic, et se régénère ; le score tenu à jour égale le recalcul le long d'une marche tirée de sa graine dérivée ; une salle trop petite est refusée du nombre de places du diagnostic`, () => {
|
||||
const rng = creerPcg32(GRILLE.graine, FLUX.DEMO);
|
||||
const comptes = {
|
||||
eprouvees: 0,
|
||||
tropPetites: 0,
|
||||
tendues: 0,
|
||||
placesVides: 0,
|
||||
ancres: 0,
|
||||
partiellementFixes: 0,
|
||||
exclus: 0,
|
||||
unTour: 0,
|
||||
retoursImposes: 0,
|
||||
plancherCollisions: 0,
|
||||
plancherExcedent: 0,
|
||||
plancherEcartItineraire: 0,
|
||||
marches: 0,
|
||||
};
|
||||
const fautes = [];
|
||||
for (let c = 1; c <= GRILLE.configurations; c += 1) {
|
||||
const { configuration, reglages, salle, placesManquantes } = tirerConfiguration(rng);
|
||||
const nom = `configuration ${c}`;
|
||||
const instance = normaliser(configuration);
|
||||
const diagnostic = diagnostiquer(configuration);
|
||||
if (diagnostic.placesManquantes !== placesManquantes) {
|
||||
fautes.push(`${nom} : ${diagnostic.placesManquantes} places manquantes au diagnostic, ${placesManquantes} tirées`);
|
||||
}
|
||||
if (salle === 'trop petite') {
|
||||
comptes.tropPetites += 1;
|
||||
try {
|
||||
rechercher(configuration, reglages);
|
||||
fautes.push(`${nom} : salle trop petite acceptée`);
|
||||
} catch (erreur) {
|
||||
if (erreur.code !== 'PLACES_MANQUANTES'
|
||||
|| erreur.details.placesManquantes !== diagnostic.placesManquantes) {
|
||||
fautes.push(`${nom} : ${erreur.message}`);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
comptes.eprouvees += 1;
|
||||
if (nombrePlaces(instance) === instance.N) comptes.tendues += 1;
|
||||
else comptes.placesVides += 1;
|
||||
if (instance.k > 0) comptes.ancres += 1;
|
||||
if (instance.statut.includes(STATUT.PARTIELLEMENT_FIXE)) comptes.partiellementFixes += 1;
|
||||
if (instance.exclus.size > 0) comptes.exclus += 1;
|
||||
if (instance.R === 1) comptes.unTour += 1;
|
||||
if (diagnostic.retoursImposes > 0) comptes.retoursImposes += 1;
|
||||
if (diagnostic.collisions.length > 0) comptes.plancherCollisions += 1;
|
||||
if (diagnostic.collisions.some(({ plancherExcedent }) => plancherExcedent !== null)) {
|
||||
comptes.plancherExcedent += 1;
|
||||
}
|
||||
if (diagnostic.ecartItineraire !== null) comptes.plancherEcartItineraire += 1;
|
||||
const contexte = { configuration, instance, aPriori: plafondsAPriori(instance), diagnostic };
|
||||
const propositions = rechercher(configuration, reglages);
|
||||
if (propositions.length !== GRILLE.nombre) fautes.push(`${nom} : ${propositions.length} propositions`);
|
||||
for (const proposition of propositions) {
|
||||
const nomProposition = `${nom}, proposition ${proposition.id}`;
|
||||
juger(nomProposition, contexte, proposition, fautes);
|
||||
if (marcher(nomProposition, instance, proposition.graine, fautes)) comptes.marches += 1;
|
||||
}
|
||||
}
|
||||
// Les vingt premières fautes suffisent à désigner un défaut ; le message
|
||||
// en donne le nombre.
|
||||
assert.deepEqual(fautes.slice(0, 20), [], `${fautes.length} fautes`);
|
||||
const sousLePlancher = PLANCHERS
|
||||
.filter(([, cle, plancher]) => comptes[cle] < plancher)
|
||||
.map(([libelle, cle, plancher]) => `${libelle} : ${comptes[cle]}, plancher ${plancher}`);
|
||||
assert.deepEqual(sousLePlancher, [], JSON.stringify(comptes));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,13 +17,15 @@
|
|||
// canonique, celui de instance.ids.
|
||||
//
|
||||
// Les agrégats portent sur les trois populations du § 5.4 : tous, les
|
||||
// mobiles — partiellement fixés compris — et les ancrés. Une population vide
|
||||
// rend null, jamais 0.
|
||||
// mobiles — partiellement fixés compris — et les ancrés, que forme
|
||||
// replierParPopulation (configuration.js). Une population vide rend null,
|
||||
// jamais 0.
|
||||
//
|
||||
// Le plafond a priori vaut null quand il est inconnu (§ 12.10.9) : seul null
|
||||
// le dit. undefined, la valeur d'un champ manquant, lève TypeError au lieu
|
||||
// de passer pour inconnu. Une liste qui n'a pas une case par personne lève
|
||||
// RangeError : elle décrit une autre population.
|
||||
import { replierParPopulation } from './configuration.js';
|
||||
|
||||
/**
|
||||
* @typedef {{tous: number|null, mobiles: number|null, ancres: number|null}} ParPopulation
|
||||
|
|
@ -49,10 +51,6 @@
|
|||
* @property {'A'|'B'|null} [domine]
|
||||
*/
|
||||
|
||||
// Statut d'un ancré dans instance.statut (§ 4.2) ; 0 marque un mobile, 1 un
|
||||
// partiellement fixé.
|
||||
const ANCRE = 2;
|
||||
|
||||
// Lève TypeError quand valeurs n'est pas une liste, faute de longueur
|
||||
// numérique ; attendu nomme ce que le paramètre nom admet.
|
||||
function exigerListe(valeurs, nom, attendu = 'liste') {
|
||||
|
|
@ -83,19 +81,11 @@ function soustraire(gauche, droite) {
|
|||
return differences;
|
||||
}
|
||||
|
||||
// Replie valeurs[p] sur les trois populations en un passage : chacun compte
|
||||
// dans « tous », puis parmi les ancrés ou parmi les mobiles. replier(cumul,
|
||||
// valeur) rend le cumul suivant ; cumul vaut null au premier membre d'une
|
||||
// population, et une population sans membre reste à null.
|
||||
function parPopulation(valeurs, statut, replier) {
|
||||
const chiffres = { tous: null, mobiles: null, ancres: null };
|
||||
for (let p = 0; p < valeurs.length; p += 1) {
|
||||
const secondaire = statut[p] === ANCRE ? 'ancres' : 'mobiles';
|
||||
chiffres.tous = replier(chiffres.tous, valeurs[p]);
|
||||
chiffres[secondaire] = replier(chiffres[secondaire], valeurs[p]);
|
||||
}
|
||||
return chiffres;
|
||||
}
|
||||
// Replie valeurs[p] sur les trois populations (replierParPopulation).
|
||||
// replier(cumul, valeur) rend le cumul suivant ; cumul vaut null au premier
|
||||
// membre d'une population, et une population sans membre reste à null.
|
||||
const parPopulation = (valeurs, statut, replier) =>
|
||||
replierParPopulation(valeurs, statut, null, replier);
|
||||
|
||||
// Le plus grand des valeurs repliées.
|
||||
const plusGrand = (cumul, valeur) => (cumul === null || valeur > cumul ? valeur : cumul);
|
||||
|
|
|
|||
|
|
@ -38,15 +38,13 @@
|
|||
// Une personne se désigne par son index dans l'instance, une table par son
|
||||
// index ; un tableau rendu suit l'ordre de instance.ids. Aucune fonction ne
|
||||
// modifie l'instance.
|
||||
//
|
||||
// Une case d'itinéraire en réserve vaut RESERVE, et RESERVE vaut LIBRE
|
||||
// (configuration.js) : une rangée de instance.fixe se lit comme l'itinéraire
|
||||
// qui laisse en réserve chaque tour libre.
|
||||
import { LIBRE, RESERVE, STATUT, exigerPlanIndexe } from './configuration.js';
|
||||
import { ErreurConfiguration } from './erreurs.js';
|
||||
|
||||
// Statut d'un ancré dans instance.statut (§ 4.2).
|
||||
const ANCRE = 2;
|
||||
// Case de instance.fixe sans table imposée, et case d'un itinéraire ou d'un
|
||||
// plan indexé en réserve : la même valeur, si bien qu'une rangée de fixe se
|
||||
// lit comme l'itinéraire qui laisse en réserve chaque tour libre.
|
||||
const LIBRE = -1;
|
||||
const RESERVE = -1;
|
||||
// Nombre de multiensembles au-delà duquel l'énumération refuse une
|
||||
// signature.
|
||||
const LIMITE_ENUMERATION = 300_000;
|
||||
|
|
@ -60,11 +58,11 @@ function borne(instance, nP, ancres, sieges) {
|
|||
// n_p : les mobiles — partiellement fixés compris — que p peut rencontrer,
|
||||
// p excepté quand il en est un.
|
||||
const mobilesRencontrables = (instance, p) =>
|
||||
instance.statut[p] === ANCRE ? instance.n : instance.n - 1;
|
||||
instance.statut[p] === STATUT.ANCRE ? instance.n : instance.n - 1;
|
||||
|
||||
// Table d'ancrage de p, −1 s'il n'est pas ancré.
|
||||
const ancrageDe = (instance, p) =>
|
||||
instance.statut[p] === ANCRE ? instance.fixe[p * instance.R] : -1;
|
||||
instance.statut[p] === STATUT.ANCRE ? instance.fixe[p * instance.R] : -1;
|
||||
|
||||
// a_t vu de p : les ancrés de la table t, p non compté.
|
||||
const ancresVus = (instance, t, ancrage) => instance.ancresParTable[t] - (t === ancrage ? 1 : 0);
|
||||
|
|
@ -103,12 +101,13 @@ function plafondSurRangee(instance, p, cases, debut, occupation) {
|
|||
return borne(instance, mobilesRencontrables(instance, p), ancres, sieges);
|
||||
}
|
||||
|
||||
// Lève RangeError quand une case de cases n'est ni −1 ni un index de table.
|
||||
function exigerCases(cases, T, quoi) {
|
||||
for (let i = 0; i < cases.length; i += 1) {
|
||||
const t = cases[i];
|
||||
if (!Number.isInteger(t) || t < RESERVE || t >= T) {
|
||||
throw new RangeError(`${quoi}, case ${i} : index de table ${String(t)} hors de −1..${T - 1}`);
|
||||
// Lève RangeError quand une case de l'itinéraire n'est ni RESERVE ni un index
|
||||
// de table. Un plan indexé passe par exigerPlanIndexe (configuration.js).
|
||||
function exigerCases(itineraire, T) {
|
||||
for (let i = 0; i < itineraire.length; i += 1) {
|
||||
const t = itineraire[i];
|
||||
if (t !== RESERVE && !(Number.isInteger(t) && t >= 0 && t < T)) {
|
||||
throw new RangeError(`itinéraire, case ${i} : index de table ${String(t)} hors de −1..${T - 1}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,7 +137,7 @@ export function plafondItineraire(instance, p, itineraire) {
|
|||
if (itineraire.length !== R) {
|
||||
throw new RangeError(`itinéraire de ${itineraire.length} tours, ${R} attendus`);
|
||||
}
|
||||
exigerCases(itineraire, T, 'itinéraire');
|
||||
exigerCases(itineraire, T);
|
||||
return plafondSurRangee(instance, p, itineraire, 0, null);
|
||||
}
|
||||
|
||||
|
|
@ -148,19 +147,15 @@ export function plafondItineraire(instance, p, itineraire) {
|
|||
* en réserve n'entre pas dans le plafond de la personne, ni dans
|
||||
* l'occupation d'aucune table.
|
||||
*
|
||||
* Lève RangeError quand tableDe n'a pas N × R cases, ou qu'une case n'est ni
|
||||
* −1 ni un index de table.
|
||||
* Lève ce que lève exigerPlanIndexe (configuration.js).
|
||||
*
|
||||
* @param {import('./types.js').Instance} instance
|
||||
* @param {ArrayLike<number>} tableDe plan indexé, [p × R + r]
|
||||
* @returns {number[]}
|
||||
*/
|
||||
export function plafondsRealises(instance, tableDe) {
|
||||
exigerPlanIndexe(instance, tableDe);
|
||||
const { N, T, R } = instance;
|
||||
if (tableDe.length !== N * R) {
|
||||
throw new RangeError(`plan indexé de ${tableDe.length} cases, N × R = ${N * R} attendues`);
|
||||
}
|
||||
exigerCases(tableDe, T, 'plan indexé');
|
||||
const occupation = new Int32Array(T * R);
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fc from 'fast-check';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { normaliser } from './configuration.js';
|
||||
import { LIBRE, STATUT, normaliser } from './configuration.js';
|
||||
import { ErreurConfiguration } from './erreurs.js';
|
||||
import { plafondsAPriori, plafondsAPrioriParEnumeration } from './plafond.js';
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ const GRAINE = 314_159;
|
|||
/**
|
||||
* Instance d'une salle : capacites[t] et ancres[t] décrivent la table
|
||||
* d'index t ; partiels[j][r] est la table imposée au j-ème partiellement
|
||||
* fixé au tour r, −1 pour un tour libre. Identifiants 1 à N : les ancrés,
|
||||
* fixé au tour r, LIBRE pour un tour libre. Identifiants 1 à N : les ancrés,
|
||||
* table par table, puis les partiels, puis les mobiles ; la table d'index t
|
||||
* a pour identifiant 100 + t. Lève ce que normaliser lève.
|
||||
*/
|
||||
|
|
@ -47,7 +47,7 @@ function instanceDe({ capacites, ancres, mobiles, tours, partiels = [] }) {
|
|||
for (const tables of partiels) {
|
||||
const id = nouveau();
|
||||
tables.forEach((t, r) => {
|
||||
if (t < 0) return;
|
||||
if (t === LIBRE) return;
|
||||
reservations.push({ participant: id, table: 100 + t, portee: 'tour', tour: r + 1 });
|
||||
});
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ function arbitraireCase(T, R) {
|
|||
),
|
||||
),
|
||||
);
|
||||
const tour = fc.oneof(fc.constant(-1), fc.integer({ min: 0, max: T - 1 }));
|
||||
const tour = fc.oneof(fc.constant(LIBRE), fc.integer({ min: 0, max: T - 1 }));
|
||||
return fc
|
||||
.record({
|
||||
salle: fc.array(table, { minLength: T, maxLength: T }),
|
||||
|
|
@ -92,7 +92,7 @@ function arbitraireCase(T, R) {
|
|||
if (a < c) ouvertes.push(t);
|
||||
});
|
||||
const partiels = rangs.map((tours) =>
|
||||
tours.map((rang) => (rang < 0 || ouvertes.length === 0 ? -1 : ouvertes[rang % ouvertes.length])),
|
||||
tours.map((rang) => (rang === LIBRE || ouvertes.length === 0 ? LIBRE : ouvertes[rang % ouvertes.length])),
|
||||
);
|
||||
return {
|
||||
capacites: salle.map(([c]) => c),
|
||||
|
|
@ -107,7 +107,9 @@ function arbitraireCase(T, R) {
|
|||
describe("plafond a priori : la programmation dynamique contre l'énumération, grille large", () => {
|
||||
test('de 2 à 8 tables et de 2 à 5 tours, chaque case de la grille : le même plafond pour chacun', () => {
|
||||
const parCase = [];
|
||||
const parStatut = [0, 0, 0];
|
||||
// Une valeur de STATUT et son compte ; la Map ne sert qu'à retrouver un
|
||||
// statut.
|
||||
const parStatut = new Map();
|
||||
for (let T = 2; T <= 8; T += 1) {
|
||||
for (let R = 2; R <= 5; R += 1) {
|
||||
let acceptees = 0;
|
||||
|
|
@ -122,7 +124,7 @@ describe("plafond a priori : la programmation dynamique contre l'énumération,
|
|||
}
|
||||
if (instance.N === 0) return;
|
||||
acceptees += 1;
|
||||
for (let p = 0; p < instance.N; p += 1) parStatut[instance.statut[p]] += 1;
|
||||
for (const statut of instance.statut) parStatut.set(statut, (parStatut.get(statut) ?? 0) + 1);
|
||||
assert.deepEqual(plafondsAPriori(instance), plafondsAPrioriParEnumeration(instance));
|
||||
}),
|
||||
{ seed: GRAINE + 10 * T + R, numRuns: 300 },
|
||||
|
|
@ -137,8 +139,10 @@ describe("plafond a priori : la programmation dynamique contre l'énumération,
|
|||
.filter(([, , acceptees]) => acceptees < 200)
|
||||
.map(([T, R, n]) => `${T} tables, ${R} tours : ${n} acceptées sur 300, 200 au moins`);
|
||||
assert.deepEqual(maigres, []);
|
||||
assert.ok(parStatut[1] >= 5000, `${parStatut[1]} partiellement fixés comparés, 5000 au moins`);
|
||||
assert.ok(parStatut[2] >= 5000, `${parStatut[2]} ancrés comparés, 5000 au moins`);
|
||||
const partiels = parStatut.get(STATUT.PARTIELLEMENT_FIXE) ?? 0;
|
||||
const ancres = parStatut.get(STATUT.ANCRE) ?? 0;
|
||||
assert.ok(partiels >= 5000, `${partiels} partiellement fixés comparés, 5000 au moins`);
|
||||
assert.ok(ancres >= 5000, `${ancres} ancrés comparés, 5000 au moins`);
|
||||
});
|
||||
|
||||
test("forme de la grande démonstration et sa variante : la programmation dynamique égale l'énumération", () => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,14 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fc from 'fast-check';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { indexerPlan, normaliser } from './configuration.js';
|
||||
import {
|
||||
LIBRE,
|
||||
RESERVE,
|
||||
STATUT,
|
||||
indexerPlan,
|
||||
normaliser,
|
||||
planDepuisIndex,
|
||||
} from './configuration.js';
|
||||
import { ErreurConfiguration } from './erreurs.js';
|
||||
import {
|
||||
plafondItineraire,
|
||||
|
|
@ -37,7 +44,7 @@ const suite = (debut, fin) => Array.from({ length: fin - debut + 1 }, (_, i) =>
|
|||
* Instance d'une forme de salle. capacites[t] et ancres[t] décrivent la
|
||||
* table d'index t ; autres compte les participants non ancrés, dont les
|
||||
* premiers sont partiellement fixés selon partiels : partiels[j][r] est
|
||||
* l'index de table imposé au tour r, −1 pour un tour libre. Les ancrés
|
||||
* l'index de table imposé au tour r, LIBRE pour un tour libre. Les ancrés
|
||||
* prennent les identifiants pairs tant qu'il reste des non-ancrés, et les
|
||||
* identifiants de table décroissent quand l'index croît : un index lu comme
|
||||
* un identifiant désigne une autre personne ou une autre table. Rend
|
||||
|
|
@ -66,7 +73,7 @@ function forme({ capacites, ancres, autres, tours, partiels = [] }) {
|
|||
}
|
||||
partiels.forEach((tables, j) => {
|
||||
tables.forEach((t, r) => {
|
||||
if (t < 0) return;
|
||||
if (t === LIBRE) return;
|
||||
reservations.push({
|
||||
participant: idsAutres[j],
|
||||
table: idTable(t),
|
||||
|
|
@ -115,14 +122,14 @@ const planIndexe = (instance, listes, reserves = listes.map(() => [])) =>
|
|||
// tables qui en ont encore ; sans siège, une personne va en réserve.
|
||||
function planBrasse(instance, graine) {
|
||||
const { N, T, R, fixe, capacite } = instance;
|
||||
const tableDe = new Int32Array(N * R).fill(-1);
|
||||
const tableDe = new Int32Array(N * R).fill(RESERVE);
|
||||
const brassage = (i, r) => ((i + 1) * 2_654_435_761 + (r + 1) * 40_503 + graine * 97) % 1_000_003;
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
const libres = Int32Array.from(capacite);
|
||||
const aPlacer = [];
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const t = fixe[p * R + r];
|
||||
if (t >= 0) {
|
||||
if (t !== LIBRE) {
|
||||
tableDe[p * R + r] = t;
|
||||
libres[t] -= 1;
|
||||
} else {
|
||||
|
|
@ -156,9 +163,9 @@ const ancresDe = (c) =>
|
|||
{ arbitrary: fc.constant(c), weight: 1 },
|
||||
);
|
||||
|
||||
// Un tour d'un non-ancré : libre (−1) une fois sur deux, fixé sinon à l'une
|
||||
// des T tables.
|
||||
const tourDe = (T) => fc.oneof(fc.constant(-1), fc.integer({ min: 0, max: T - 1 }));
|
||||
// Un tour d'un non-ancré : libre (LIBRE) une fois sur deux, fixé sinon à
|
||||
// l'une des T tables.
|
||||
const tourDe = (T) => fc.oneof(fc.constant(LIBRE), fc.integer({ min: 0, max: T - 1 }));
|
||||
|
||||
// Description d'une instance tirée dans les bornes données : chaque table
|
||||
// tire sa capacité puis ses ancrés, chaque partiel un tour par tour. Un
|
||||
|
|
@ -189,9 +196,10 @@ function arbitraireForme({ tables, tours, capacites, mobiles, partiels }) {
|
|||
// Une instance que normaliser refuse — une table qui reçoit plus de
|
||||
// personnes fixées que de sièges — ou sans participant n'est pas comptée ;
|
||||
// une autre l'est, avec ses personnes par statut et ses tables pleines
|
||||
// d'ancrés. Rend ces comptes.
|
||||
// d'ancrés. Rend ces comptes ; parStatut associe une valeur de STATUT à son
|
||||
// compte, et la Map ne sert qu'à retrouver un statut.
|
||||
function eprouverAccord(arbitraire, parametres) {
|
||||
const bilan = { acceptees: 0, parStatut: [0, 0, 0], tablesPleines: 0 };
|
||||
const bilan = { acceptees: 0, parStatut: new Map(), tablesPleines: 0 };
|
||||
fc.assert(
|
||||
fc.property(arbitraire, ({ salle, tours, mobiles, partiels }) => {
|
||||
let instance;
|
||||
|
|
@ -209,7 +217,9 @@ function eprouverAccord(arbitraire, parametres) {
|
|||
}
|
||||
if (instance.N === 0) return;
|
||||
bilan.acceptees += 1;
|
||||
for (let p = 0; p < instance.N; p += 1) bilan.parStatut[instance.statut[p]] += 1;
|
||||
for (const statut of instance.statut) {
|
||||
bilan.parStatut.set(statut, (bilan.parStatut.get(statut) ?? 0) + 1);
|
||||
}
|
||||
if (salle.some(([c, a]) => a === c)) bilan.tablesPleines += 1;
|
||||
assert.deepEqual(plafondsAPriori(instance), plafondsAPrioriParEnumeration(instance));
|
||||
}),
|
||||
|
|
@ -310,9 +320,9 @@ describe('plafonds : valeurs exactes (§ 5.5, § 15)', () => {
|
|||
|
||||
test('partiellement fixé : sa table fixée entre dans F, seuls ses tours libres varient', () => {
|
||||
// Un non-ancré de la grande forme, fixé au tour 1 à une table de 7.
|
||||
const { instance, partiels, mobiles, ancres } = grandeForme([[29, -1, -1, -1]]);
|
||||
const { instance, partiels, mobiles, ancres } = grandeForme([[29, LIBRE, LIBRE, LIBRE]]);
|
||||
const [p] = partiels;
|
||||
assert.equal(instance.statut[p], 1);
|
||||
assert.equal(instance.statut[p], STATUT.PARTIELLEMENT_FIXE);
|
||||
assert.equal(instance.k, 33);
|
||||
const aPriori = plafondsAPriori(instance);
|
||||
// La table de 7 entre dans F : 1 ancré et 5 sièges ; ses trois tours
|
||||
|
|
@ -336,7 +346,7 @@ describe('plafonds : valeurs exactes (§ 5.5, § 15)', () => {
|
|||
ancres: [0, 2],
|
||||
autres: 3,
|
||||
tours: 2,
|
||||
partiels: [[0, -1]],
|
||||
partiels: [[0, LIBRE]],
|
||||
});
|
||||
assert.equal(instance.n, 3);
|
||||
// Partiel et mobiles : seule la table 0 est admissible, 0 ancré et
|
||||
|
|
@ -370,7 +380,7 @@ describe('plafonds : valeurs exactes (§ 5.5, § 15)', () => {
|
|||
// 5 table 1, occupée par 2 aux deux tours : 1 + 1 = 2
|
||||
assert.deepEqual(plafondsRealises(instance, tableDe), [3, 3, 2, 2, 2]);
|
||||
const p = instance.indexDe.get(3);
|
||||
assert.equal(plafondItineraire(instance, p, [0, -1]), 2);
|
||||
assert.equal(plafondItineraire(instance, p, [0, RESERVE]), 2);
|
||||
// L'écart d'itinéraire porte ce que le manque ne porte pas : 4 − 2.
|
||||
assert.deepEqual(plafondsAPriori(instance), [4, 4, 4, 4, 4]);
|
||||
});
|
||||
|
|
@ -391,20 +401,20 @@ describe('plafonds : valeurs exactes (§ 5.5, § 15)', () => {
|
|||
attendu[mobiles[0]] = 0;
|
||||
assert.deepEqual(plafondsAPriori(instance), attendu);
|
||||
assert.deepEqual(plafondsAPrioriParEnumeration(instance), attendu);
|
||||
assert.equal(plafondItineraire(instance, mobiles[0], [-1, -1]), 0);
|
||||
assert.equal(plafondItineraire(instance, mobiles[0], [RESERVE, RESERVE]), 0);
|
||||
});
|
||||
|
||||
test('plafond a priori ≥ plafond réalisé pour chacun, sur trois plans quelconques de chaque forme', () => {
|
||||
const formes = [
|
||||
forme({ capacites: [5, 5, 5, 5], ancres: [1, 1, 1, 1], autres: 16, tours: 5 }),
|
||||
grandeForme(),
|
||||
grandeForme([[29, -1, -1, -1], [-1, 30, 30, -1]]),
|
||||
grandeForme([[29, LIBRE, LIBRE, LIBRE], [LIBRE, 30, 30, LIBRE]]),
|
||||
forme({ capacites: [3, 3, 3, 3], ancres: [0, 0, 0, 0], autres: 12, tours: 4 }),
|
||||
forme({ capacites: [4, 4, 4], ancres: [1, 1, 1], autres: 7, tours: 2 }),
|
||||
forme({ capacites: [5, 2], ancres: [0, 2], autres: 3, tours: 2, partiels: [[0, -1]] }),
|
||||
forme({ capacites: [5, 2], ancres: [0, 2], autres: 3, tours: 2, partiels: [[0, LIBRE]] }),
|
||||
// Une place de moins que de participants : chaque plan met quelqu'un
|
||||
// en réserve.
|
||||
forme({ capacites: [3, 3], ancres: [1, 0], autres: 6, tours: 3, partiels: [[-1, 1, -1]] }),
|
||||
forme({ capacites: [3, 3], ancres: [1, 0], autres: 6, tours: 3, partiels: [[LIBRE, 1, LIBRE]] }),
|
||||
];
|
||||
const ecarts = [];
|
||||
let comparaisons = 0;
|
||||
|
|
@ -455,6 +465,21 @@ describe('plafonds : valeurs exactes (§ 5.5, § 15)', () => {
|
|||
.filter((ecart) => ecart !== null);
|
||||
assert.deepEqual(ecarts, []);
|
||||
});
|
||||
|
||||
test('un plan indexé hors forme : plafondsRealises nomme le participant, le tour et la valeur, comme planDepuisIndex', () => {
|
||||
// Quatre participants d'identifiants 1 à 4, deux tables, deux tours. La
|
||||
// dernière case est celle de l'identifiant 4 au tour 2. La chaîne "-1"
|
||||
// n'est pas la réserve : le message la cite entre guillemets, pour
|
||||
// qu'elle ne se lise pas comme la valeur qu'elle contient.
|
||||
const { instance } = forme({ capacites: [3, 3], ancres: [0, 0], autres: 4, tours: 2 });
|
||||
const fautif = [0, 0, 0, 0, 1, 1, 1, '-1'];
|
||||
const attendu = {
|
||||
name: 'RangeError',
|
||||
message: 'plan indexé : participant 4, tour 2, index de table "-1" hors de −1..1',
|
||||
};
|
||||
assert.throws(() => planDepuisIndex(instance, fautif), attendu);
|
||||
assert.throws(() => plafondsRealises(instance, fautif), attendu);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plafond a priori : la programmation dynamique contre l'énumération (§ 14.11, § 14.12)", () => {
|
||||
|
|
@ -495,8 +520,10 @@ describe("plafond a priori : la programmation dynamique contre l'énumération (
|
|||
// les acceptées, et les personnes comparées de chaque statut.
|
||||
const { acceptees, parStatut, tablesPleines } = bilan;
|
||||
assert.ok(acceptees >= 200, `${acceptees} instances acceptées, 200 au moins`);
|
||||
assert.ok(parStatut[1] >= 100, `${parStatut[1]} partiellement fixés comparés, 100 au moins`);
|
||||
assert.ok(parStatut[2] >= 100, `${parStatut[2]} ancrés comparés, 100 au moins`);
|
||||
const partiels = parStatut.get(STATUT.PARTIELLEMENT_FIXE) ?? 0;
|
||||
const ancres = parStatut.get(STATUT.ANCRE) ?? 0;
|
||||
assert.ok(partiels >= 100, `${partiels} partiellement fixés comparés, 100 au moins`);
|
||||
assert.ok(ancres >= 100, `${ancres} ancrés comparés, 100 au moins`);
|
||||
assert.ok(tablesPleines >= 50, `${tablesPleines} instances à table pleine d'ancrés, 50 au moins`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
// configuration, de sa graine dérivée, du compte d'arrêt et de la longueur de
|
||||
// l'historique, trois réglages qu'elle porte : la proposition 2 d'une demande
|
||||
// de 3 est celle d'une demande de 5, quel que soit l'ordre dans lequel les
|
||||
// propositions s'achèvent.
|
||||
// propositions s'achèvent, et regenerer en rend le plan à partir de ces trois
|
||||
// champs seuls (§ 8.9). Les deux passent par la même descente, proposer.
|
||||
//
|
||||
// Sièges. Chaque tour offre S = Σ c_t sièges, rangés table par table dans
|
||||
// l'ordre des tables. Les personnes que instance.fixe impose à un tour
|
||||
|
|
@ -49,13 +50,14 @@
|
|||
// Incrémental. L'état tient à jour ce dont le score dépend : la matrice des
|
||||
// rencontres par paire ; rencontres(p) ; l'histogramme des écarts au plafond
|
||||
// a priori, d'où leur maximum, et la somme de leurs carrés ; les collisions
|
||||
// cumulées et les paires distinctes de même appartenance ; Σ_paires
|
||||
// max(0, M − 1) ; les visites par (personne, table), d'où les retours
|
||||
// choisis ; par personne, le nombre d'affiliés rencontrés de chaque
|
||||
// appartenance, d'où |F(p)|, A(p) et r(p). Un échange ne touche que les
|
||||
// paires des deux tables concernées, au tour concerné : O(capacité). Ce score
|
||||
// sert la boucle de recherche et rien d'autre ; scoreComplet recompose le
|
||||
// même n-uplet à partir de mesurer, la mesure qui fait foi (§ 5.10, § 13.2).
|
||||
// cumulées de même appartenance et leur excédent ; Σ_paires max(0, M − 1) ;
|
||||
// les visites par (personne, table), d'où les retours choisis ; par
|
||||
// personne, le nombre d'affiliés rencontrés de chaque appartenance, d'où
|
||||
// |F(p)|, A(p) et Σ r(p). Un échange ne touche que les paires des deux tables
|
||||
// concernées, au tour concerné : O(capacité). Ce score sert la boucle de
|
||||
// recherche et rien d'autre ; scoreComplet recompose le même n-uplet à partir
|
||||
// de la mesure qui fait foi, dont il lit chaque grandeur au lieu de la
|
||||
// refaire (§ 5.10, § 13.2).
|
||||
//
|
||||
// Acceptation tardive. L'historique H, de longueur L, part du score initial.
|
||||
// À l'itération i, le candidat est gardé s'il ne dépasse ni le score courant
|
||||
|
|
@ -91,9 +93,17 @@
|
|||
// inchangé y marque aussi bien un plateau que la descente parcourt, et une
|
||||
// relance couperait ce parcours, d'autant plus souvent que L est court.
|
||||
import { FLUX, creerPcg32 } from '../demo/prng.js';
|
||||
import { nombrePlacesManquantes, normaliser, planDepuisIndex } from './configuration.js';
|
||||
import {
|
||||
LIBRE,
|
||||
RESERVE,
|
||||
SANS_GROUPE,
|
||||
nombrePlacesManquantes,
|
||||
normaliser,
|
||||
planDepuisIndex,
|
||||
} from './configuration.js';
|
||||
import { ErreurAnnulee, ErreurConfiguration } from './erreurs.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
import { ecartsAuPlafondAPriori } from './manque.js';
|
||||
import { plafondsAPriori } from './plafond.js';
|
||||
|
||||
/**
|
||||
|
|
@ -112,27 +122,23 @@ import { plafondsAPriori } from './plafond.js';
|
|||
*
|
||||
* @typedef {Object} EtatRecherche
|
||||
* Rendu par creerEtat ; les champs qui suivent se lisent, aucun ne s'écrit
|
||||
* hors de ce module.
|
||||
* hors de ce module. Un champ qui tient une grandeur que mesurer rend en
|
||||
* porte le nom.
|
||||
* @property {Int32Array} tableDe plan indexé courant, N × R
|
||||
* @property {Int32Array} rencontres |met(p)|, ordre canonique
|
||||
* @property {Int32Array} affilies |F(p)|
|
||||
* @property {Int32Array} appartenancesVues A(p)
|
||||
* @property {number} collisionsCumulees
|
||||
* @property {number} pairesDistinctes paires de même appartenance réunies
|
||||
* @property {number} excedentCollisions collisions cumulées − paires distinctes
|
||||
* @property {number} totalRetoursChoisis
|
||||
* @property {number} totalRedondance Σ_p r(p)
|
||||
*
|
||||
* @typedef {{tour: number, a: number, b: number}} Mouvement
|
||||
* l'échange des occupants des sièges a et b au tour d'index tour
|
||||
*/
|
||||
|
||||
// Case de instance.fixe sans table imposée, et case d'un plan indexé en
|
||||
// réserve.
|
||||
const LIBRE = -1;
|
||||
const RESERVE = -1;
|
||||
// Siège sans occupant : la place fantôme.
|
||||
const FANTOME = -1;
|
||||
// instance.groupe d'un participant sans appartenance.
|
||||
const SANS_GROUPE = -1;
|
||||
const TAILLE_SCORE = 6;
|
||||
// Ordres de comparaison du n-uplet, par rang de composante : celui du score,
|
||||
// et celui des contraintes, qui passe les composantes 2, 3 et 4 devant les
|
||||
|
|
@ -164,6 +170,17 @@ function exigerReglage(nom, valeur, min, max) {
|
|||
}
|
||||
}
|
||||
|
||||
// Lève une RangeError qui nomme le premier des trois réglages qu'une
|
||||
// proposition porte à sortir de son domaine, contrôlés dans cet ordre :
|
||||
// graine de 0 à 2^32 − 1, arret et historique ≥ 1. rechercher et regenerer
|
||||
// l'appellent tous deux : ce que l'un admet, l'autre l'admet, et regenerer
|
||||
// accepte les champs de toute proposition que rechercher rend.
|
||||
function exigerChampsProposition({ graine, arret, historique }) {
|
||||
exigerReglage('graine', graine, 0, MOT_MAX);
|
||||
exigerReglage('arret', arret, 1, Number.MAX_SAFE_INTEGER);
|
||||
exigerReglage('historique', historique, 1, Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
|
||||
// Ce que toutes les propositions d'une instance partagent : la disposition
|
||||
// des sièges, les sièges fixés et les sièges mobiles de chaque tour, les
|
||||
// personnes libres de chaque tour par index croissant, le plafond a priori
|
||||
|
|
@ -171,8 +188,8 @@ function exigerReglage(nom, valeur, min, max) {
|
|||
// une visite est un retour choisi. Lève PLACES_MANQUANTES quand les sièges
|
||||
// d'un tour ne suffisent pas à asseoir tout le monde (§ 5.9).
|
||||
function preparer(instance) {
|
||||
const manque = nombrePlacesManquantes(instance);
|
||||
if (manque > 0) throw new ErreurConfiguration('PLACES_MANQUANTES', { manque });
|
||||
const placesManquantes = nombrePlacesManquantes(instance);
|
||||
if (placesManquantes > 0) throw new ErreurConfiguration('PLACES_MANQUANTES', { placesManquantes });
|
||||
const { N, T, R, capacite, fixe } = instance;
|
||||
|
||||
const debut = new Int32Array(T + 1);
|
||||
|
|
@ -280,29 +297,35 @@ function changerAffilie(etat, p, g, sens) {
|
|||
etat.appartenancesVues[p] += sens;
|
||||
vues = sens;
|
||||
}
|
||||
etat.sommeRedondance += sens - vues;
|
||||
etat.totalRedondance += sens - vues;
|
||||
}
|
||||
|
||||
// Ajoute sens, 1 ou −1, aux rencontres de la paire (a, b), a ≠ b, et
|
||||
// propage : Σ max(0, M − 1) ; collisions cumulées pour deux personnes de la
|
||||
// même appartenance ; et, quand la paire devient réunie ou cesse de l'être,
|
||||
// les paires distinctes, les rencontres et les affiliés des deux.
|
||||
// propage : les collisions cumulées pour deux personnes de la même
|
||||
// appartenance. Quand la paire était réunie avant un ajout, ou le reste
|
||||
// après un retrait, la rencontre ajoutée ou retirée est une répétition :
|
||||
// elle compte dans Σ max(0, M − 1) et, entre collègues, dans l'excédent de
|
||||
// collisions, qui vaut Σ max(0, M − 1) sur les paires de même appartenance.
|
||||
// Sinon la paire devient réunie ou cesse de l'être : les rencontres et les
|
||||
// affiliés des deux suivent.
|
||||
function modifierPaire(etat, a, b, sens) {
|
||||
const k = a < b ? a * etat.N + b : b * etat.N + a;
|
||||
const avant = etat.paires[k];
|
||||
const apres = avant + sens;
|
||||
etat.paires[k] = apres;
|
||||
if ((sens > 0 ? avant : apres) >= 1) etat.excedentPaires += sens;
|
||||
const ga = etat.groupe[a];
|
||||
const gb = etat.groupe[b];
|
||||
const memeGroupe = ga !== SANS_GROUPE && ga === gb;
|
||||
const memeGroupe = ga === gb && etat.affilie[a] === 1;
|
||||
if (memeGroupe) etat.collisionsCumulees += sens;
|
||||
if ((sens > 0 ? avant : apres) !== 0) return;
|
||||
if (memeGroupe) etat.pairesDistinctes += sens;
|
||||
if ((sens > 0 ? avant : apres) !== 0) {
|
||||
etat.excedentPaires += sens;
|
||||
if (memeGroupe) etat.excedentCollisions += sens;
|
||||
return;
|
||||
}
|
||||
changerRencontres(etat, a, sens);
|
||||
changerRencontres(etat, b, sens);
|
||||
if (gb !== SANS_GROUPE) changerAffilie(etat, a, gb, sens);
|
||||
if (ga !== SANS_GROUPE) changerAffilie(etat, b, ga, sens);
|
||||
if (etat.affilie[b] === 1) changerAffilie(etat, a, gb, sens);
|
||||
if (etat.affilie[a] === 1) changerAffilie(etat, b, ga, sens);
|
||||
}
|
||||
|
||||
// Ajoute sens aux visites de p à la table t, et ajuste les retours choisis.
|
||||
|
|
@ -367,6 +390,11 @@ function etatInitial(structure, rng) {
|
|||
instance,
|
||||
contraintes: instance.contraintes,
|
||||
groupe: instance.groupe,
|
||||
// affilie[p] vaut 1 quand p porte une appartenance, 0 pour SANS_GROUPE.
|
||||
// modifierPaire le lit au lieu de comparer un groupe à SANS_GROUPE : sous
|
||||
// le lanceur d'épreuves, chaque lecture d'une valeur importée passe par
|
||||
// un accesseur, que la boucle de recherche paierait à chaque paire.
|
||||
affilie: Uint8Array.from(instance.groupe, (g) => (g === SANS_GROUPE ? 0 : 1)),
|
||||
N,
|
||||
T,
|
||||
R,
|
||||
|
|
@ -390,10 +418,10 @@ function etatInitial(structure, rng) {
|
|||
ecartAuPlafondAPrioriMax: 0,
|
||||
carresEcartsAuPlafondAPriori: 0,
|
||||
collisionsCumulees: 0,
|
||||
pairesDistinctes: 0,
|
||||
excedentCollisions: 0,
|
||||
excedentPaires: 0,
|
||||
totalRetoursChoisis: 0,
|
||||
sommeRedondance: 0,
|
||||
totalRedondance: 0,
|
||||
};
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
etat.histogramme[aPriori[p]] += 1;
|
||||
|
|
@ -424,16 +452,17 @@ function etatInitial(structure, rng) {
|
|||
|
||||
// Écrit dans sortie le n-uplet du score, à partir des grandeurs nommées de
|
||||
// q, et le rend. Une seule composition sert le score tenu à jour et le score
|
||||
// recalculé.
|
||||
// recalculé ; une grandeur que mesurer rend y porte son nom, et la
|
||||
// composition n'en refait aucune.
|
||||
function composer(contraintes, q, sortie) {
|
||||
const separer = contraintes.separerAppartenances;
|
||||
sortie[0] = q.ecartAuPlafondAPrioriMax;
|
||||
sortie[1] = q.carresEcartsAuPlafondAPriori;
|
||||
sortie[2] = separer ? q.collisionsCumulees - q.pairesDistinctes : 0;
|
||||
sortie[2] = separer ? q.excedentCollisions : 0;
|
||||
sortie[3] = separer ? q.collisionsCumulees : 0;
|
||||
sortie[4] = (contraintes.nouveauxVoisins ? q.excedentPaires : 0)
|
||||
+ (contraintes.nouvelleTable ? q.totalRetoursChoisis : 0);
|
||||
sortie[5] = contraintes.varierAppartenances ? q.sommeRedondance : 0;
|
||||
sortie[5] = contraintes.varierAppartenances ? q.totalRedondance : 0;
|
||||
return sortie;
|
||||
}
|
||||
|
||||
|
|
@ -531,13 +560,25 @@ function descendre(structure, rng, { arret, historique, dejaFaits, total, signal
|
|||
return meilleurPlan;
|
||||
}
|
||||
|
||||
// Le plan de la proposition de graine dérivée graineDerivee : la descente
|
||||
// depuis creerPcg32(graineDerivee, FLUX.RECHERCHE), rendue par identifiants.
|
||||
// rechercher et regenerer l'appellent tous deux. À instance égale, le plan ne
|
||||
// dépend que de la graine dérivée, d'options.arret et d'options.historique :
|
||||
// les autres options ne servent qu'à rapporter l'avancement et à lire le
|
||||
// signal.
|
||||
function proposer(instance, structure, graineDerivee, options) {
|
||||
const rng = creerPcg32(graineDerivee, FLUX.RECHERCHE);
|
||||
return planDepuisIndex(instance, descendre(structure, rng, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Propositions de placement d'une configuration (§ 5.7) : nombre
|
||||
* propositions, d'identifiants 1 à nombre, chacune la meilleure affectation
|
||||
* vue en arret mouvements évalués, relances comprises (en-tête du module).
|
||||
* Chacune porte sa graine dérivée, arret et historique, défaut résolu : les
|
||||
* trois décident de son plan. À configuration et réglages égaux, le résultat
|
||||
* est identique. Ne modifie pas la configuration.
|
||||
* trois décident de son plan, que regenerer rend à partir d'eux. À
|
||||
* configuration et réglages égaux, le résultat est identique. Ne modifie pas
|
||||
* la configuration.
|
||||
*
|
||||
* Toutes les 1 000 itérations, comptées sur la génération entière,
|
||||
* progression(fait, total) reçoit le rang du mouvement en cours parmi
|
||||
|
|
@ -549,8 +590,9 @@ function descendre(structure, rng, { arret, historique, dejaFaits, total, signal
|
|||
* Lève RangeError quand un réglage n'est pas un entier de son domaine —
|
||||
* graine de 0 à 2^32 − 1, arret, nombre et historique ≥ 1 ;
|
||||
* ErreurConfiguration de normaliser ; ErreurConfiguration
|
||||
* ('PLACES_MANQUANTES', { manque }) quand les tables n'offrent pas assez de
|
||||
* sièges, manque étant le nombre de places qui manquent à chaque tour.
|
||||
* ('PLACES_MANQUANTES', { placesManquantes }) quand les tables n'offrent pas
|
||||
* assez de sièges, placesManquantes étant le nombre de places qui manquent à
|
||||
* chaque tour, celui que rend nombrePlacesManquantes.
|
||||
*
|
||||
* @param {import('./types.js').Configuration} configuration
|
||||
* @param {ReglagesGeneration} reglages
|
||||
|
|
@ -559,10 +601,8 @@ function descendre(structure, rng, { arret, historique, dejaFaits, total, signal
|
|||
*/
|
||||
export function rechercher(configuration, reglages, { signal, progression } = {}) {
|
||||
const { graine, arret, nombre, historique = HISTORIQUE_PAR_DEFAUT } = reglages;
|
||||
exigerReglage('graine', graine, 0, MOT_MAX);
|
||||
exigerReglage('arret', arret, 1, Number.MAX_SAFE_INTEGER);
|
||||
exigerChampsProposition({ graine, arret, historique });
|
||||
exigerReglage('nombre', nombre, 1, Number.MAX_SAFE_INTEGER);
|
||||
exigerReglage('historique', historique, 1, Number.MAX_SAFE_INTEGER);
|
||||
const instance = normaliser(configuration);
|
||||
const structure = preparer(instance);
|
||||
const graines = creerPcg32(graine, FLUX.GRAINES);
|
||||
|
|
@ -571,8 +611,7 @@ export function rechercher(configuration, reglages, { signal, progression } = {}
|
|||
for (let id = 1; id <= nombre; id += 1) {
|
||||
lireSignal(signal);
|
||||
const graineDerivee = graines.suivant();
|
||||
const rng = creerPcg32(graineDerivee, FLUX.RECHERCHE);
|
||||
const meilleur = descendre(structure, rng, {
|
||||
const plan = proposer(instance, structure, graineDerivee, {
|
||||
arret,
|
||||
historique,
|
||||
dejaFaits: (id - 1) * arret,
|
||||
|
|
@ -580,17 +619,49 @@ export function rechercher(configuration, reglages, { signal, progression } = {}
|
|||
signal,
|
||||
progression,
|
||||
});
|
||||
const plan = planDepuisIndex(instance, meilleur);
|
||||
propositions.push({ id, graine: graineDerivee, arret, historique, plan });
|
||||
}
|
||||
return propositions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Le plan d'une proposition, régénéré à partir de ses seuls champs (§ 5.7,
|
||||
* § 8.9) : la descente part de proposition.graine, la graine dérivée qu'elle
|
||||
* porte, et compte proposition.arret mouvements sous un historique de
|
||||
* proposition.historique cases. Ces trois champs décident du plan : à
|
||||
* configuration égale, regenerer rend le plan que rechercher a rendu pour
|
||||
* cette proposition, quel que soit son rang dans sa génération. La descente
|
||||
* elle-même fait aussi partie du résultat, ordre des tirages, PATIENCE et
|
||||
* ordres d'acceptation : une proposition rendue par une autre version de ce
|
||||
* module n'est pas garantie régénérable (§ 8.9). Aucun autre champ n'est lu,
|
||||
* ni id ni plan. Ne modifie ni la configuration ni la proposition.
|
||||
*
|
||||
* Lève RangeError quand l'un des trois champs n'est pas un entier de son
|
||||
* domaine, graine de 0 à 2^32 − 1, arret et historique ≥ 1 : un champ absent
|
||||
* ne reçoit pas la valeur par défaut de rechercher, qui ferait régénérer une
|
||||
* autre proposition sans le dire. Lève ce que rechercher lève pour la
|
||||
* configuration : ErreurConfiguration de normaliser, PLACES_MANQUANTES.
|
||||
*
|
||||
* @param {import('./types.js').Configuration} configuration
|
||||
* @param {{graine: number, arret: number, historique: number}} proposition
|
||||
* @returns {import('./types.js').Plan}
|
||||
*/
|
||||
export function regenerer(configuration, { graine, arret, historique }) {
|
||||
exigerChampsProposition({ graine, arret, historique });
|
||||
const instance = normaliser(configuration);
|
||||
return proposer(instance, preparer(instance), graine, {
|
||||
arret,
|
||||
historique,
|
||||
dejaFaits: 0,
|
||||
total: arret,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* État de recherche au placement initial (§ 5.10) : fixés à leurs tables,
|
||||
* libres mélangés par rng.melanger, tour après tour, puis assis table après
|
||||
* table dans l'ordre des tables. Lève ErreurConfiguration('PLACES_MANQUANTES',
|
||||
* { manque }) comme rechercher.
|
||||
* { placesManquantes }) comme rechercher.
|
||||
*
|
||||
* @param {import('./types.js').Instance} instance
|
||||
* @param {ReturnType<typeof creerPcg32>} rng
|
||||
|
|
@ -611,11 +682,19 @@ export function scoreIncremental(etat) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Le même n-uplet, recalculé de zéro sur etat.tableDe : plafondsAPriori et
|
||||
* mesurer en donnent les grandeurs. Σ_paires max(0, M − 1) vaut Σ_paires M
|
||||
* − Σ_paires min(M, 1) : le premier terme compte les paires réunies à chaque
|
||||
* table de chaque tour, o(o − 1)/2 pour o occupants ; le second, les paires
|
||||
* réunies au moins une fois, Σ_p rencontres(p) / 2.
|
||||
* Le même n-uplet, recalculé de zéro sur etat.tableDe à partir de la mesure
|
||||
* qui fait foi, dont chaque grandeur se lit au lieu de se refaire : les
|
||||
* écarts au plafond a priori par ecartsAuPlafondAPriori (manque.js), sur les
|
||||
* plafonds de plafondsAPriori ; l'excédent de collisions, les collisions
|
||||
* cumulées, les retours choisis et Σ r(p) par mesurer. Une redéfinition de
|
||||
* l'une d'elles dans son module change donc ce score, et son accord avec le
|
||||
* score tenu à jour tombe.
|
||||
*
|
||||
* Seule Σ_paires max(0, M − 1), que la mesure ne rend pas, se calcule ici :
|
||||
* elle vaut Σ_paires M − Σ_paires min(M, 1). Le premier terme compte les
|
||||
* paires réunies à chaque table de chaque tour, o(o − 1)/2 pour o
|
||||
* occupants ; le second, les paires réunies au moins une fois,
|
||||
* Σ_p rencontres(p) / 2.
|
||||
*
|
||||
* @param {EtatRecherche} etat
|
||||
* @returns {number[]}
|
||||
|
|
@ -623,16 +702,12 @@ export function scoreIncremental(etat) {
|
|||
export function scoreComplet(etat) {
|
||||
const { instance, tableDe } = etat;
|
||||
const { N, T, R } = instance;
|
||||
const aPriori = plafondsAPriori(instance);
|
||||
const mesures = mesurer(instance, tableDe);
|
||||
let ecartAuPlafondAPrioriMax = 0;
|
||||
let carresEcartsAuPlafondAPriori = 0;
|
||||
let sommeRencontres = 0;
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const ecart = aPriori[p] - mesures.rencontres[p];
|
||||
for (const ecart of ecartsAuPlafondAPriori(mesures, plafondsAPriori(instance))) {
|
||||
ecartAuPlafondAPrioriMax = Math.max(ecartAuPlafondAPrioriMax, ecart);
|
||||
carresEcartsAuPlafondAPriori += ecart * ecart;
|
||||
sommeRencontres += mesures.rencontres[p];
|
||||
}
|
||||
const occupation = new Int32Array(T * R);
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
|
|
@ -643,14 +718,16 @@ export function scoreComplet(etat) {
|
|||
}
|
||||
let reunions = 0;
|
||||
for (const o of occupation) reunions += (o * (o - 1)) / 2;
|
||||
let sommeRencontres = 0;
|
||||
for (const rencontres of mesures.rencontres) sommeRencontres += rencontres;
|
||||
const grandeurs = {
|
||||
ecartAuPlafondAPrioriMax,
|
||||
carresEcartsAuPlafondAPriori,
|
||||
excedentCollisions: mesures.excedentCollisions,
|
||||
collisionsCumulees: mesures.collisionsCumulees,
|
||||
pairesDistinctes: mesures.pairesDistinctes,
|
||||
excedentPaires: reunions - sommeRencontres / 2,
|
||||
totalRetoursChoisis: mesures.totalRetoursChoisis,
|
||||
sommeRedondance: mesures.redondance.reduce((somme, r) => somme + r, 0),
|
||||
totalRedondance: mesures.totalRedondance,
|
||||
};
|
||||
return composer(instance.contraintes, grandeurs, new Array(TAILLE_SCORE));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,10 @@ const DEFAIT_TOUS_LES = 3;
|
|||
const ESSAIS_MAX = 100 * PALIERS.at(-1);
|
||||
|
||||
// Vrai quand deux listes de nombres ont la même longueur et les mêmes
|
||||
// valeurs, rang par rang.
|
||||
const egales = (a, b) => a.length === b.length && a.every((valeur, i) => valeur === b[i]);
|
||||
// valeurs, rang par rang, chacune un nombre fini : deux champs absents, deux
|
||||
// undefined, ne s'accordent pas.
|
||||
const egales = (a, b) =>
|
||||
a.length === b.length && a.every((valeur, i) => Number.isFinite(valeur) && valeur === b[i]);
|
||||
|
||||
// Ajoute à releve un libellé « palier : grandeur » pour chaque grandeur où
|
||||
// l'état tenu à jour diffère du recalcul complet.
|
||||
|
|
@ -55,8 +57,9 @@ function relever(etat, instance, palier, releve) {
|
|||
confronter('affilies', Array.from(etat.affilies), mesures.affilies);
|
||||
confronter('appartenancesVues', Array.from(etat.appartenancesVues), mesures.appartenancesVues);
|
||||
confronter('collisionsCumulees', [etat.collisionsCumulees], [mesures.collisionsCumulees]);
|
||||
confronter('pairesDistinctes', [etat.pairesDistinctes], [mesures.pairesDistinctes]);
|
||||
confronter('excedentCollisions', [etat.excedentCollisions], [mesures.excedentCollisions]);
|
||||
confronter('totalRetoursChoisis', [etat.totalRetoursChoisis], [mesures.totalRetoursChoisis]);
|
||||
confronter('totalRedondance', [etat.totalRedondance], [mesures.totalRedondance]);
|
||||
}
|
||||
|
||||
describe('recherche : incrémental contre recalcul complet (§ 5.10, § 14.10)', () => {
|
||||
|
|
|
|||
|
|
@ -4,25 +4,35 @@
|
|||
// Épreuves de la recherche (§ 5.2, § 5.7, § 5.9, § 5.10, § 14.10) : à graine
|
||||
// et entrée égales, propositions identiques ; l'identifiant d'une proposition
|
||||
// est son rang dans la suite des graines dérivées, et son plan sort de sa
|
||||
// seule graine dérivée ; chaque plan rendu tient les invariants et les
|
||||
// réservations, et l'appartenance d'un ancré y pèse comme une autre ; une
|
||||
// salle trop petite est refusée avec le nombre de places manquantes ;
|
||||
// graine dérivée, de son compte d'arrêt et de son historique, trois champs à
|
||||
// partir desquels regenerer le rend ; chaque plan rendu tient les invariants
|
||||
// et les réservations, et l'appartenance d'un ancré y pèse comme une autre ;
|
||||
// une salle trop petite est refusée avec le nombre de places manquantes ;
|
||||
// l'avancement se rapporte toutes les 1 000 itérations, une descente
|
||||
// s'arrête sur un score nul, une annulation ne rend rien ; le placement
|
||||
// initial égale celui que décrit le paragraphe « Sièges » de l'en-tête de
|
||||
// recherche.js, récrit ici à la lettre ; la descente égale l'acceptation
|
||||
// tardive de son paragraphe « Acceptation tardive », récrite de même, là où
|
||||
// ni bascule ni relance ne jouent, et sinon augmentée de la bascule et de la
|
||||
// relance de son paragraphe « Deux ordres d'acceptation » ; le score tenu à jour et le score recalculé égalent un n-uplet
|
||||
// recalculé ici par énumération des paires ; la petite démonstration atteint
|
||||
// son plan parfait sans cas particulier. Un plan rendu se juge par
|
||||
// verifierInvariants et se mesure par mesurer, deux modules distincts du
|
||||
// module éprouvé.
|
||||
// recherche.js, récrit ici à la lettre ; un échange avec une place fantôme
|
||||
// fait passer une personne à une autre table, comme le dit son paragraphe
|
||||
// « Mouvement » ; la descente égale l'acceptation tardive de son paragraphe
|
||||
// « Acceptation tardive », récrite de même, là où ni bascule ni relance ne
|
||||
// jouent, et sinon augmentée de la bascule et de la relance de son
|
||||
// paragraphe « Deux ordres d'acceptation » ; le score tenu à jour et le score
|
||||
// recalculé égalent un n-uplet recalculé ici par énumération des paires ; la
|
||||
// petite démonstration atteint son plan parfait sans cas particulier. Un plan
|
||||
// rendu se juge par verifierInvariants et se mesure par mesurer, deux modules
|
||||
// distincts du module éprouvé.
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { CATALOGUE } from '../demo/catalogue.js';
|
||||
import { FLUX, creerPcg32 } from '../demo/prng.js';
|
||||
import { indexerPlan, normaliser, planDepuisIndex } from './configuration.js';
|
||||
import {
|
||||
LIBRE,
|
||||
RESERVE,
|
||||
SANS_GROUPE,
|
||||
indexerPlan,
|
||||
normaliser,
|
||||
planDepuisIndex,
|
||||
} from './configuration.js';
|
||||
import { ErreurAnnulee } from './erreurs.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
import { plafondsAPriori } from './plafond.js';
|
||||
|
|
@ -31,6 +41,7 @@ import {
|
|||
defaire,
|
||||
proposerEtAppliquer,
|
||||
rechercher,
|
||||
regenerer,
|
||||
scoreComplet,
|
||||
scoreIncremental,
|
||||
} from './recherche.js';
|
||||
|
|
@ -158,7 +169,7 @@ function placementLitteral(instance, rng) {
|
|||
const libres = [];
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const t = fixe[p * R + r];
|
||||
if (t === -1) {
|
||||
if (t === LIBRE) {
|
||||
libres.push(p);
|
||||
} else {
|
||||
tableDe[p * R + r] = t;
|
||||
|
|
@ -190,7 +201,7 @@ function scoreOracle(instance, tableDe, aPriori) {
|
|||
for (let a = 0; a < N; a += 1) {
|
||||
for (let b = a + 1; b < N; b += 1) {
|
||||
const t = tableDe[a * R + r];
|
||||
if (t !== -1 && t === tableDe[b * R + r]) M[a * N + b] += 1;
|
||||
if (t !== RESERVE && t === tableDe[b * R + r]) M[a * N + b] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -207,12 +218,12 @@ function scoreOracle(instance, tableDe, aPriori) {
|
|||
excedentPaires += m - 1;
|
||||
for (const [p, q] of [[a, b], [b, a]]) {
|
||||
rencontres[p] += 1;
|
||||
if (groupe[q] !== -1) {
|
||||
if (groupe[q] !== SANS_GROUPE) {
|
||||
affilies[p] += 1;
|
||||
appartenances[p].add(groupe[q]);
|
||||
}
|
||||
}
|
||||
if (groupe[a] !== -1 && groupe[a] === groupe[b]) {
|
||||
if (groupe[a] !== SANS_GROUPE && groupe[a] === groupe[b]) {
|
||||
cumulees += m;
|
||||
distinctes += 1;
|
||||
}
|
||||
|
|
@ -432,6 +443,153 @@ describe('rechercher : reproductibilité et identifiants (§ 5.7, § 19.4)', ()
|
|||
});
|
||||
});
|
||||
|
||||
describe('regenerer : une proposition depuis ses seuls champs (§ 5.7, § 8.9)', () => {
|
||||
test("pour chaque proposition d'une génération, regenerer rend le même plan à partir de ses trois champs, sans lire ni son plan ni son rang", () => {
|
||||
// arret 5 000 n'est pas un multiple de l'historique 37 : une descente
|
||||
// dont le plan dépendrait de son rang dans la génération, par le compte
|
||||
// des mouvements que la génération a déjà faits, ne se régénérerait pas
|
||||
// au-delà de la première proposition. Les propositions se régénèrent de
|
||||
// la dernière à la première, et regenerer ne reçoit que leur graine
|
||||
// dérivée, leur compte d'arrêt et leur historique, ni leur plan ni leur
|
||||
// identifiant : chacune sort de ces trois champs, et non de son rang ou
|
||||
// d'un état que la précédente aurait laissé. salleReservee porte des
|
||||
// places fantômes et des partiellement fixés.
|
||||
const reglages = { graine: 5, arret: 5_000, nombre: 3, historique: 37 };
|
||||
let regenerees = 0;
|
||||
let conflit = null;
|
||||
for (const [nom, construire] of [
|
||||
['salleReservee', salleReservee],
|
||||
['petite-conflit', () => demo('petite-conflit')],
|
||||
]) {
|
||||
const configuration = construire();
|
||||
const propositions = rechercher(configuration, reglages);
|
||||
for (const { id, graine, arret, historique, plan } of [...propositions].reverse()) {
|
||||
assert.deepStrictEqual(
|
||||
regenerer(configuration, { graine, arret, historique }),
|
||||
plan,
|
||||
`${nom}, proposition ${id}`,
|
||||
);
|
||||
regenerees += 1;
|
||||
}
|
||||
assert.deepStrictEqual(configuration, construire(), nom);
|
||||
// L'historique décide du plan : sous la longueur par défaut, les mêmes
|
||||
// graines donnent d'autres plans, ceux que rendrait une régénération
|
||||
// qui ignorerait ce champ.
|
||||
assert.notDeepStrictEqual(
|
||||
rechercher(configuration, { ...reglages, historique: HISTORIQUE_PAR_DEFAUT }).map(({ plan }) => plan),
|
||||
propositions.map(({ plan }) => plan),
|
||||
nom,
|
||||
);
|
||||
conflit = propositions;
|
||||
}
|
||||
assert.equal(regenerees, 6);
|
||||
// Dans la variante conflit, chaque descente atteint un écart nul,
|
||||
// bascule, puis relance : la réplique littérale rend les mêmes plans, et
|
||||
// compte bascules et relances. La régénération rejoue donc aussi le
|
||||
// placement neuf qu'une relance tire.
|
||||
const repliques = descentesALaLettre(demo('petite-conflit'), reglages, { bascule: true });
|
||||
assert.deepStrictEqual(repliques.map(({ plan }) => plan), conflit.map(({ plan }) => plan));
|
||||
assert.deepEqual(
|
||||
repliques.map(({ bascules, relances }) => [bascules, relances > 0]),
|
||||
[
|
||||
[1, true],
|
||||
[1, true],
|
||||
[1, true],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("le compte d'arrêt décide du plan : à chaque compte de 1 à 32, regenerer rend chaque plan, et un compte au moins change un plan du précédent ; à 100, regenerer rend chaque plan, et la moitié comme le double de ce compte en donnent d'autres", () => {
|
||||
// Là où la descente ne progresse plus, comme à 5 000 mouvements dans
|
||||
// l'épreuve précédente, la moitié ou le double du compte rendent les
|
||||
// mêmes plans, et une régénération qui compterait autrement passerait.
|
||||
//
|
||||
// Sur ses premiers mouvements, la descente change souvent de meilleure
|
||||
// affectation. À chaque compte de 1 à COMPTES_COURTS, regenerer rend le
|
||||
// plan de chaque proposition à partir de ses trois champs, et l'épreuve
|
||||
// exige qu'un compte au moins de la boucle change un plan par rapport au
|
||||
// compte précédent : une régénération qui compterait un mouvement de
|
||||
// moins rend un autre plan à ce compte, et une qui en compterait un de
|
||||
// plus, au compte précédent.
|
||||
//
|
||||
// À 100 mouvements, la descente progresse encore. Une descente plus
|
||||
// courte refait, mouvement pour mouvement, le début de celle-ci, et la
|
||||
// meilleure affectation ne cède qu'à une meilleure : des plans qui
|
||||
// changent à arret / 2 et à 2 × arret changent à tout compte hors de cet
|
||||
// intervalle.
|
||||
const COMPTES_COURTS = 32;
|
||||
const configuration = demo('petite-conflit');
|
||||
const reglages = { graine: 5, arret: 100, nombre: 3, historique: 37 };
|
||||
let precedents = null;
|
||||
let changements = 0;
|
||||
for (let compte = 1; compte <= COMPTES_COURTS; compte += 1) {
|
||||
const courtes = rechercher(configuration, { ...reglages, arret: compte });
|
||||
for (const { id, graine, arret, historique, plan } of courtes) {
|
||||
assert.deepStrictEqual(
|
||||
regenerer(configuration, { graine, arret, historique }),
|
||||
plan,
|
||||
`arret ${compte}, proposition ${id}`,
|
||||
);
|
||||
}
|
||||
const plans = courtes.map(({ plan }) => JSON.stringify(plan));
|
||||
if (precedents !== null && plans.some((plan, i) => plan !== precedents[i])) changements += 1;
|
||||
precedents = plans;
|
||||
}
|
||||
assert.ok(changements > 0, `aucun compte de 2 à ${COMPTES_COURTS} ne change un plan du compte précédent`);
|
||||
const propositions = rechercher(configuration, reglages);
|
||||
for (const { id, graine, arret, historique, plan } of [...propositions].reverse()) {
|
||||
assert.deepStrictEqual(regenerer(configuration, { graine, arret, historique }), plan, `proposition ${id}`);
|
||||
}
|
||||
for (const arret of [reglages.arret / 2, 2 * reglages.arret]) {
|
||||
assert.notDeepStrictEqual(
|
||||
rechercher(configuration, { ...reglages, arret }).map(({ plan }) => plan),
|
||||
propositions.map(({ plan }) => plan),
|
||||
`arret ${arret}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('champ hors de son domaine ou absent : RangeError qui le nomme ; salle trop petite : PLACES_MANQUANTES, comme rechercher', () => {
|
||||
// Un champ absent ne prend pas la valeur par défaut de rechercher : une
|
||||
// proposition qui ne porte pas son historique ne se régénère pas sous une
|
||||
// longueur supposée. Les deux bornes de la graine sont admises.
|
||||
const champs = { graine: 1, arret: 10, historique: 5 };
|
||||
for (const graine of [0, 2 ** 32 - 1]) {
|
||||
assert.equal(regenerer(demo('petite'), { ...champs, graine }).tours.length, 4, `graine ${graine}`);
|
||||
}
|
||||
const cas = [
|
||||
[{ ...champs, graine: -1 }, 'graine'],
|
||||
[{ ...champs, graine: 2 ** 32 }, 'graine'],
|
||||
[{ ...champs, graine: 1.5 }, 'graine'],
|
||||
[{ arret: 10, historique: 5 }, 'graine'],
|
||||
[{ ...champs, arret: 0 }, 'arret'],
|
||||
[{ ...champs, arret: '10' }, 'arret'],
|
||||
[{ ...champs, historique: 0 }, 'historique'],
|
||||
[{ graine: 1, arret: 10 }, 'historique'],
|
||||
];
|
||||
for (const [proposition, nom] of cas) {
|
||||
assert.throws(
|
||||
() => regenerer(demo('petite'), proposition),
|
||||
(erreur) => erreur instanceof RangeError && erreur.message.startsWith(`${nom} :`),
|
||||
JSON.stringify(proposition),
|
||||
);
|
||||
}
|
||||
const tropPetite = configurationDe({
|
||||
appartenances: Array(11).fill(null),
|
||||
tables: [
|
||||
{ id: 1, capacite: 4 },
|
||||
{ id: 2, capacite: 4 },
|
||||
],
|
||||
tours: 2,
|
||||
});
|
||||
assert.throws(() => regenerer(tropPetite, champs), {
|
||||
name: 'ErreurConfiguration',
|
||||
code: 'PLACES_MANQUANTES',
|
||||
details: { placesManquantes: 3 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rechercher : plans valides (§ 5.2, § 5.9, § 14.12)', () => {
|
||||
test('chaque proposition des quatre démonstrations tient tous les invariants, réserve interdite', () => {
|
||||
let examinees = 0;
|
||||
|
|
@ -497,7 +655,9 @@ describe('rechercher : plans valides (§ 5.2, § 5.9, § 14.12)', () => {
|
|||
test('salle trop petite : PLACES_MANQUANTES et le nombre exact de places manquantes, aucun plan', () => {
|
||||
// Douze inscrits dont un exclu, deux tables de 4 et une de 2 : onze
|
||||
// présents pour dix places. L'exclu ne compte pas, une réservation ne
|
||||
// change rien au compte (§ 5.9).
|
||||
// change rien au compte (§ 5.9). Le détail se nomme placesManquantes,
|
||||
// comme dans le diagnostic : « manque » désigne plafond réalisé −
|
||||
// rencontres (§ 5.5).
|
||||
const tables = [
|
||||
{ id: 1, capacite: 4 },
|
||||
{ id: 2, capacite: 4 },
|
||||
|
|
@ -517,13 +677,13 @@ describe('rechercher : plans valides (§ 5.2, § 5.9, § 14.12)', () => {
|
|||
assert.throws(() => rechercher(configuration, reglages), {
|
||||
name: 'ErreurConfiguration',
|
||||
code: 'PLACES_MANQUANTES',
|
||||
details: { manque: 1 },
|
||||
details: { placesManquantes: 1 },
|
||||
});
|
||||
}
|
||||
const unDePlus = configurationDe({ appartenances: Array(13).fill(null), tables, tours: 2 });
|
||||
assert.throws(() => rechercher(unDePlus, reglages), {
|
||||
code: 'PLACES_MANQUANTES',
|
||||
details: { manque: 3 },
|
||||
details: { placesManquantes: 3 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -644,6 +804,32 @@ describe('état de recherche : placement initial et score incrémental (§ 5.10)
|
|||
});
|
||||
});
|
||||
|
||||
describe("rechercher : l'échange avec une place fantôme (paragraphe « Mouvement » de recherche.js)", () => {
|
||||
test("une personne passe au siège vide d'une autre table : deux tables de 3 pour A, A, B, B, aucune collision sur vingt graines", () => {
|
||||
// Le placement initial assied trois personnes à la première table et la
|
||||
// quatrième à la seconde : deux d'une même appartenance s'y retrouvent.
|
||||
// Un échange entre deux personnes garde trois et une ; seul l'échange
|
||||
// avec une place fantôme fait passer quelqu'un à la seconde table. À
|
||||
// deux par table, chacune peut réunir un A et un B, sans collision.
|
||||
const configuration = configurationDe({
|
||||
appartenances: ['A', 'A', 'B', 'B'],
|
||||
tables: [
|
||||
{ id: 1, capacite: 3 },
|
||||
{ id: 2, capacite: 3 },
|
||||
],
|
||||
tours: 1,
|
||||
contraintes: { ...SANS_CONTRAINTE, separerAppartenances: true },
|
||||
});
|
||||
const instance = normaliser(configuration);
|
||||
const collisions = [];
|
||||
for (let graine = 0; graine < 20; graine += 1) {
|
||||
const [{ plan }] = rechercher(configuration, { graine, arret: 2_000, nombre: 1 });
|
||||
collisions.push(mesurer(instance, indexerPlan(instance, plan)).collisionsCumulees);
|
||||
}
|
||||
assert.deepEqual(collisions, Array(20).fill(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('score : composition du n-uplet (en-tête de recherche.js)', () => {
|
||||
test('scoreIncremental et scoreComplet égalent le n-uplet énuméré, pour aucune contrainte, chacune seule et toutes', () => {
|
||||
// Score tenu à jour et score recalculé passent par la même composition :
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
// Contrat de données du moteur : les formes qu'il reçoit et celles qu'il
|
||||
// rend. Ce module ne porte que des définitions JSDoc et n'exécute rien ; un
|
||||
// module cite un type par import('./types.js').Instance.
|
||||
// module cite un type par import('./types.js').Instance. Les valeurs que ces
|
||||
// formes portent — STATUT, LIBRE, RESERVE, SANS_GROUPE — sont définies et
|
||||
// exportées par configuration.js, leur seul propriétaire.
|
||||
|
||||
/**
|
||||
* @typedef {Object} Participant
|
||||
|
|
@ -54,20 +56,31 @@
|
|||
* @property {number[]} idsTables index → id table, ordre de configuration.tables
|
||||
* @property {Map<number, number>} indexTableDe
|
||||
* @property {Int32Array} capacite par index de table
|
||||
* @property {Int32Array} groupe par index participant ; −1 sans appartenance
|
||||
* @property {Int32Array} groupe par index participant ; SANS_GROUPE (−1)
|
||||
* sans appartenance
|
||||
* @property {string[]} groupes index de groupe → libellé ; ordre de première
|
||||
* apparition en parcourant les ids croissants
|
||||
* @property {Int32Array} fixe [p * R + r] → index de table imposé (r à
|
||||
* partir de 0), −1 si libre
|
||||
* @property {Uint8Array} statut 0 mobile, 1 partiellement fixé, 2 ancré (§ 4.2)
|
||||
* partir de 0), LIBRE (−1) si libre
|
||||
* @property {Uint8Array} statut STATUT.MOBILE (0), STATUT.PARTIELLEMENT_FIXE
|
||||
* (1), STATUT.ANCRE (2) (§ 4.2)
|
||||
* @property {Int32Array} ancresParTable a_t
|
||||
* @property {number} k ancrés
|
||||
* @property {number} n N − k
|
||||
* @property {Contraintes} contraintes
|
||||
*
|
||||
* Un plan indexé est un Int32Array `tableDe` de longueur N × R :
|
||||
* tableDe[p * R + r] = index de table, −1 pour la réserve.
|
||||
* tableDe[p * R + r] = index de table, RESERVE (−1) pour la réserve.
|
||||
* exigerPlanIndexe (configuration.js) en garde la forme.
|
||||
*
|
||||
* L'ordre canonique des tableaux par personne est celui de `instance.ids` :
|
||||
* tout tableau rendu par le moteur et indexé par personne suit cet ordre.
|
||||
*
|
||||
* Une personne que le moteur désigne sans identifiant l'est par son index
|
||||
* canonique, son rang dans `instance.ids`, et `instance.ids[index]` en donne
|
||||
* l'identifiant. C'est le cas d'une violation de verifierIndicateurs,
|
||||
* { code, index } : ses trois listes ne portent pas d'identifiant, et la
|
||||
* traduction revient à l'appelant, qui tient l'instance. Les détails d'une
|
||||
* ErreurConfiguration et les violations de verifierInvariants désignent au
|
||||
* contraire les participants par identifiant.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
// est une violation de la liste rendue, et la liste vide dit que tout tient.
|
||||
// Aucun ne modifie ce qu'il reçoit. Les formes reçues sont décrites dans
|
||||
// types.js.
|
||||
import { LIBRE } from './configuration.js';
|
||||
|
||||
/**
|
||||
* @typedef {{code: string, participant?: number, table?: number, tour?: number,
|
||||
|
|
@ -18,8 +19,6 @@
|
|||
* instance.ids
|
||||
*/
|
||||
|
||||
// Case de instance.fixe sans table imposée.
|
||||
const LIBRE = -1;
|
||||
// Index de table d'une liste que plan.tables ne rattache à aucune table de
|
||||
// l'instance. Aucune case de instance.fixe ne le porte : une réservation ne
|
||||
// s'honore jamais dans une telle liste.
|
||||
|
|
|
|||
Loading…
Reference in a new issue