[ADD] engine: conflicts per table, aggregated diversity, diagnosis extras
The plan view must say at which table and round a conflict sits, and the quality page must aggregate diversity. indicateurs.js now exports the single pass that reads a plan round by round and table by table, from which mesurer draws every figure; conflits.js gives its detail per table, so the totals and the table view share one arithmetic (§ 13.2). The diagnosis gains its repartition, remedies, single-affiliation flag and the anchors that force a return. Checked: 1582 node and 60 node-long tests; indicateurs.js at 100 %. --- FR --- [ADD] moteur : conflits par table, diversité agrégée, diagnostic enrichi La vue du plan doit dire à quelle table et à quel tour se trouve un conflit, et la page de qualité agréger la diversité. indicateurs.js exporte le seul passage qui lit un plan tour par tour et table par table, d'où mesurer tire chaque chiffre ; conflits.js en rend le détail par table : les totaux et la vue par table partagent une arithmétique (§ 13.2). Le diagnostic gagne sa répartition, ses remèdes, le drapeau d'appartenance unique et les ancrages qui imposent un retour. Vérifié : 1582 node et 60 node-long ; indicateurs.js à 100 %. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
03c04bdf16
commit
7667134bd7
7 changed files with 1431 additions and 133 deletions
66
src/moteur/conflits.js
Normal file
66
src/moteur/conflits.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
// Conflits par table (§ 7.5) : les violations des règles actives, table par
|
||||||
|
// table et tour par tour, que le plan lit sans rien recalculer. Elles se
|
||||||
|
// lisent dans parcourirTours (indicateurs.js), le passage dont mesurer tire
|
||||||
|
// ses chiffres : la somme des paires séparer d'un plan égale ses collisions
|
||||||
|
// cumulées, celle des personnes nouvelle table ses retours choisis, et les
|
||||||
|
// paires distinctes nouveaux voisins ses rencontres répétées choisies
|
||||||
|
// (§ 13.2). La pastille d'une table compte des occurrences de violation —
|
||||||
|
// une paire en collision, une paire revue, une personne revenue —, et chaque
|
||||||
|
// règle porte les siennes dans son unité (§ 5.4).
|
||||||
|
//
|
||||||
|
// « Varier les appartenances » ne se localise pas à une table : aucune
|
||||||
|
// violation n'en sort.
|
||||||
|
import { parcourirTours } from './indicateurs.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ConflitsTable
|
||||||
|
* @property {number} table identifiant
|
||||||
|
* @property {number} nombre violations des règles actives à ce tour, toutes règles
|
||||||
|
* @property {Array<{regle: 'separerAppartenances'|'nouveauxVoisins', paires: number[][]}
|
||||||
|
* |{regle: 'nouvelleTable', personnes: number[]}>} regles
|
||||||
|
* identifiants croissants, règles dans cet ordre ; une règle inactive
|
||||||
|
* ou sans violation est omise
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Violations des règles actives (§ 7.5), lues dans parcourirTours et rendues
|
||||||
|
* en identifiants : séparer les appartenances — les collègues réunis ;
|
||||||
|
* nouveaux voisins — les paires revues ; nouvelle table — les personnes
|
||||||
|
* revenues. Une paire revue se signale à chaque rencontre répétée, sauf quand
|
||||||
|
* toutes ses rencontres jusque-là ont eu lieu à une table où les deux étaient
|
||||||
|
* réservés ; une personne revenue, à chaque visite répétée d'une table, sauf
|
||||||
|
* quand elle y est réservée à ce tour et l'a été à une visite antérieure.
|
||||||
|
*
|
||||||
|
* Les index de personnes croissent avec les identifiants : les listes de
|
||||||
|
* parcourirTours, traduites terme à terme, restent croissantes.
|
||||||
|
*
|
||||||
|
* Ne modifie ni l'instance ni le plan ; lève ce que lève exigerPlanIndexe.
|
||||||
|
*
|
||||||
|
* @param {import('./types.js').Instance} instance
|
||||||
|
* @param {ArrayLike<number>} tableDe
|
||||||
|
* @returns {ConflitsTable[][]} [tour][index de table], tables dans l'ordre de l'instance
|
||||||
|
*/
|
||||||
|
export function conflitsParTable(instance, tableDe) {
|
||||||
|
const { ids, idsTables, contraintes } = instance;
|
||||||
|
const enIds = (paires) => paires.map(([a, b]) => [ids[a], ids[b]]);
|
||||||
|
return parcourirTours(instance, tableDe).parTour.map((tables) =>
|
||||||
|
tables.map(({ collegues, revues, revenus }, t) => {
|
||||||
|
const regles = [];
|
||||||
|
if (contraintes.separerAppartenances && collegues.length > 0) {
|
||||||
|
regles.push({ regle: 'separerAppartenances', paires: enIds(collegues) });
|
||||||
|
}
|
||||||
|
if (contraintes.nouveauxVoisins && revues.length > 0) {
|
||||||
|
regles.push({ regle: 'nouveauxVoisins', paires: enIds(revues) });
|
||||||
|
}
|
||||||
|
if (contraintes.nouvelleTable && revenus.length > 0) {
|
||||||
|
regles.push({ regle: 'nouvelleTable', personnes: revenus.map((p) => ids[p]) });
|
||||||
|
}
|
||||||
|
let nombre = 0;
|
||||||
|
for (const regle of regles) nombre += (regle.paires ?? regle.personnes).length;
|
||||||
|
return { table: idsTables[t], nombre, regles };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
227
src/moteur/conflits.long.test.js
Normal file
227
src/moteur/conflits.long.test.js
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
// Épreuves lourdes des conflits par table (§ 7.5, § 13.2) : les violations
|
||||||
|
// par table, sommées sur un plan entier, rendent les chiffres de la mesure.
|
||||||
|
// Σ des paires séparer = collisionsCumulees ; Σ des personnes nouvelle table
|
||||||
|
// = totalRetoursChoisis ; paires distinctes nouveaux voisins =
|
||||||
|
// rencontresRepetees.choisies. D'abord les propositions d'une génération sur
|
||||||
|
// la grande démonstration, puis cent plans tirés sur de petites
|
||||||
|
// configurations, chacun de sa propre graine.
|
||||||
|
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 { conflitsParTable } from './conflits.js';
|
||||||
|
import { LIBRE, indexerPlan, normaliser, planDepuisIndex } from './configuration.js';
|
||||||
|
import { mesurer } from './indicateurs.js';
|
||||||
|
import { rechercher } from './recherche.js';
|
||||||
|
import { verifierInvariants } from './verification.js';
|
||||||
|
|
||||||
|
// Une génération courte : la qualité des propositions n'importe pas ici, et
|
||||||
|
// des propositions qui portent encore des conflits éprouvent mieux l'accord.
|
||||||
|
const GENERATION = Object.freeze({ graine: 7, arret: 20_000, nombre: 2 });
|
||||||
|
|
||||||
|
// Les cent plans tirés : la graine du plan i est GRAINE + i.
|
||||||
|
const PLANS = 100;
|
||||||
|
const GRAINE = 2_718;
|
||||||
|
const PLANS_ACCEPTES_MIN = 90;
|
||||||
|
|
||||||
|
// Planchers des comptes de la boucle, posés au-dessous de ce que donnent les
|
||||||
|
// graines écrites ci-dessus : 100 plans acceptés ; 69 dont les trois totaux
|
||||||
|
// sont non nuls ; 32 où une paire réunie librement se retrouve à une table où
|
||||||
|
// les deux sont réservés ; 41 où une personne assise librement à une table y
|
||||||
|
// revient réservée.
|
||||||
|
const TOUS_NON_NULS_MIN = 55;
|
||||||
|
const PAIRE_PUIS_RESERVEE_MIN = 25;
|
||||||
|
const PERSONNE_PUIS_RESERVEE_MIN = 32;
|
||||||
|
|
||||||
|
const LIBELLES = Object.freeze(['Grisaille', 'Club des Merles', 'Ondée']);
|
||||||
|
|
||||||
|
// Les trois sommes d'un plan, lues dans conflitsParTable : paires séparer,
|
||||||
|
// personnes nouvelle table, et paires distinctes nouveaux voisins, clé
|
||||||
|
// « a,b » en identifiants.
|
||||||
|
function sommer(conflits) {
|
||||||
|
let separer = 0;
|
||||||
|
let nouvelleTable = 0;
|
||||||
|
const revues = new Set();
|
||||||
|
for (const tables of conflits) {
|
||||||
|
for (const { regles } of tables) {
|
||||||
|
for (const { regle, paires, personnes } of regles) {
|
||||||
|
if (regle === 'separerAppartenances') separer += paires.length;
|
||||||
|
else if (regle === 'nouvelleTable') nouvelleTable += personnes.length;
|
||||||
|
else for (const [a, b] of paires) revues.add(`${a},${b}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { separer, nouvelleTable, nouveauxVoisins: revues.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Les écarts entre les sommes par table et la mesure du même plan.
|
||||||
|
function ecarts(nom, instance, tableDe) {
|
||||||
|
const sommes = sommer(conflitsParTable(instance, tableDe));
|
||||||
|
const mesures = mesurer(instance, tableDe);
|
||||||
|
const attendu = {
|
||||||
|
separer: mesures.collisionsCumulees,
|
||||||
|
nouvelleTable: mesures.totalRetoursChoisis,
|
||||||
|
nouveauxVoisins: mesures.rencontresRepetees.choisies,
|
||||||
|
};
|
||||||
|
const fautes = [];
|
||||||
|
for (const cle of ['separer', 'nouvelleTable', 'nouveauxVoisins']) {
|
||||||
|
if (sommes[cle] !== attendu[cle]) {
|
||||||
|
fautes.push(`${nom} : ${cle} ${sommes[cle]} par table, ${attendu[cle]} mesurés`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { fautes, sommes };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Une petite configuration tirée de rng : de 2 à 6 tables de 2 à 5 sièges,
|
||||||
|
// de 1 à 5 tours, de la moitié des sièges à tous occupés ; une appartenance
|
||||||
|
// parmi trois, ou aucune, pour chacun. Un participant sur cinq est ancré, un
|
||||||
|
// sur trois réservé à une partie des tours quand R ≥ 2, chacun à une table
|
||||||
|
// où il reste un siège à chacun de ses tours : normaliser l'accepte toujours.
|
||||||
|
// Séparer, nouveaux voisins et nouvelle table sont actifs.
|
||||||
|
function tirerConfiguration(rng) {
|
||||||
|
const T = 2 + rng.borne(5);
|
||||||
|
const capacites = Array.from({ length: T }, () => 2 + rng.borne(4));
|
||||||
|
const R = 1 + rng.borne(5);
|
||||||
|
const S = capacites.reduce((somme, c) => somme + c, 0);
|
||||||
|
const N = S - rng.borne(Math.floor(S / 2) + 1);
|
||||||
|
const fixees = new Int32Array(T * R);
|
||||||
|
const siegeLibre = (t, r) => fixees[t * R + r] < capacites[t];
|
||||||
|
const participants = [];
|
||||||
|
const reservations = [];
|
||||||
|
for (let id = 1; id <= N; id += 1) {
|
||||||
|
const g = rng.borne(LIBELLES.length + 1);
|
||||||
|
participants.push({ id, nom: `P${id}`, appartenance: g < LIBELLES.length ? LIBELLES[g] : null });
|
||||||
|
const role = rng.borne(15);
|
||||||
|
if (role < 3) {
|
||||||
|
const t = rng.borne(T);
|
||||||
|
if (Array.from({ length: R }, (_, r) => r).every((r) => siegeLibre(t, r))) {
|
||||||
|
for (let r = 0; r < R; r += 1) fixees[t * R + r] += 1;
|
||||||
|
reservations.push({ participant: id, table: t + 1, portee: 'tous' });
|
||||||
|
}
|
||||||
|
} else if (role < 8 && R >= 2) {
|
||||||
|
for (let r = 0; r < R; r += 1) {
|
||||||
|
if (rng.borne(2) === 0) continue;
|
||||||
|
const t = rng.borne(T);
|
||||||
|
if (!siegeLibre(t, r)) continue;
|
||||||
|
fixees[t * R + r] += 1;
|
||||||
|
reservations.push({ participant: id, table: t + 1, portee: 'tour', tour: r + 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
participants,
|
||||||
|
tables: capacites.map((capacite, t) => ({ id: t + 1, numero: t + 1, capacite })),
|
||||||
|
tours: R,
|
||||||
|
reservations,
|
||||||
|
contraintes: {
|
||||||
|
separerAppartenances: true,
|
||||||
|
nouveauxVoisins: true,
|
||||||
|
nouvelleTable: true,
|
||||||
|
varierAppartenances: rng.borne(2) === 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Un plan qui assied chacun à chaque tour : les réservés à leur table, puis
|
||||||
|
// chaque autre, par index croissants, à une table tirée parmi celles qui lui
|
||||||
|
// laissent un siège.
|
||||||
|
function tirerPlan(rng, instance) {
|
||||||
|
const { N, T, R, capacite, fixe } = instance;
|
||||||
|
const tableDe = new Int32Array(N * R);
|
||||||
|
for (let r = 0; r < R; r += 1) {
|
||||||
|
const occupation = new Int32Array(T);
|
||||||
|
for (let p = 0; p < N; p += 1) {
|
||||||
|
const t = fixe[p * R + r];
|
||||||
|
if (t === LIBRE) continue;
|
||||||
|
tableDe[p * R + r] = t;
|
||||||
|
occupation[t] += 1;
|
||||||
|
}
|
||||||
|
for (let p = 0; p < N; p += 1) {
|
||||||
|
if (fixe[p * R + r] !== LIBRE) continue;
|
||||||
|
const ouvertes = [];
|
||||||
|
for (let t = 0; t < T; t += 1) if (occupation[t] < capacite[t]) ouvertes.push(t);
|
||||||
|
const t = ouvertes[rng.borne(ouvertes.length)];
|
||||||
|
tableDe[p * R + r] = t;
|
||||||
|
occupation[t] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tableDe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vrai quand une paire réunie à une table où les deux ne sont pas réservés
|
||||||
|
// se retrouve, à un tour suivant, à une table où les deux le sont.
|
||||||
|
function pairePuisReservee({ N, R, fixe }, tableDe) {
|
||||||
|
for (let a = 0; a < N; a += 1) {
|
||||||
|
for (let b = a + 1; b < N; b += 1) {
|
||||||
|
let libre = false;
|
||||||
|
for (let r = 0; r < R; r += 1) {
|
||||||
|
const t = tableDe[a * R + r];
|
||||||
|
if (t !== tableDe[b * R + r]) continue;
|
||||||
|
const reservee = fixe[a * R + r] === t && fixe[b * R + r] === t;
|
||||||
|
if (reservee && libre) return true;
|
||||||
|
if (!reservee) libre = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vrai quand une personne assise sans réservation à une table y revient, à
|
||||||
|
// un tour suivant, réservée.
|
||||||
|
function personnePuisReservee({ N, T, R, fixe }, tableDe) {
|
||||||
|
for (let p = 0; p < N; p += 1) {
|
||||||
|
const libreA = new Uint8Array(T);
|
||||||
|
for (let r = 0; r < R; r += 1) {
|
||||||
|
const t = tableDe[p * R + r];
|
||||||
|
if (fixe[p * R + r] !== t) libreA[t] = 1;
|
||||||
|
else if (libreA[t] === 1) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('conflits par table : leurs sommes rendent la mesure (§ 7.5, § 13.2)', () => {
|
||||||
|
test('grande démonstration, deux propositions de 20 000 mouvements', () => {
|
||||||
|
const configuration = CATALOGUE.find(({ cle }) => cle === 'grande').construire();
|
||||||
|
const instance = normaliser(configuration);
|
||||||
|
const propositions = rechercher(configuration, GENERATION);
|
||||||
|
assert.equal(propositions.length, GENERATION.nombre);
|
||||||
|
const fautes = [];
|
||||||
|
for (const { id, plan } of propositions) {
|
||||||
|
assert.deepEqual(verifierInvariants(instance, plan), [], `proposition ${id}`);
|
||||||
|
fautes.push(...ecarts(`proposition ${id}`, instance, indexerPlan(instance, plan)).fautes);
|
||||||
|
}
|
||||||
|
assert.deepEqual(fautes, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cent plans tirés sur de petites configurations, ancrés et partiellement fixés', () => {
|
||||||
|
const fautes = [];
|
||||||
|
const comptes = { acceptes: 0, tousNonNuls: 0, pairePuisReservee: 0, personnePuisReservee: 0 };
|
||||||
|
for (let i = 1; i <= PLANS; i += 1) {
|
||||||
|
const rng = creerPcg32(GRAINE + i, FLUX.DEMO);
|
||||||
|
const instance = normaliser(tirerConfiguration(rng));
|
||||||
|
const tableDe = tirerPlan(rng, instance);
|
||||||
|
const violations = verifierInvariants(instance, planDepuisIndex(instance, tableDe));
|
||||||
|
if (violations.length > 0) {
|
||||||
|
fautes.push(`graine ${GRAINE + i} : plan refusé, ${JSON.stringify(violations)}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
comptes.acceptes += 1;
|
||||||
|
const { fautes: fautesPlan, sommes } = ecarts(`graine ${GRAINE + i}`, instance, tableDe);
|
||||||
|
fautes.push(...fautesPlan);
|
||||||
|
if (sommes.separer > 0 && sommes.nouvelleTable > 0 && sommes.nouveauxVoisins > 0) {
|
||||||
|
comptes.tousNonNuls += 1;
|
||||||
|
}
|
||||||
|
if (pairePuisReservee(instance, tableDe)) comptes.pairePuisReservee += 1;
|
||||||
|
if (personnePuisReservee(instance, tableDe)) comptes.personnePuisReservee += 1;
|
||||||
|
}
|
||||||
|
assert.deepEqual(fautes, []);
|
||||||
|
assert.ok(comptes.acceptes >= PLANS_ACCEPTES_MIN, JSON.stringify(comptes));
|
||||||
|
assert.ok(comptes.tousNonNuls >= TOUS_NON_NULS_MIN, JSON.stringify(comptes));
|
||||||
|
assert.ok(comptes.pairePuisReservee >= PAIRE_PUIS_RESERVEE_MIN, JSON.stringify(comptes));
|
||||||
|
assert.ok(comptes.personnePuisReservee >= PERSONNE_PUIS_RESERVEE_MIN, JSON.stringify(comptes));
|
||||||
|
});
|
||||||
|
});
|
||||||
297
src/moteur/conflits.test.js
Normal file
297
src/moteur/conflits.test.js
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
// Épreuves des conflits par table (§ 7.5, § 5.4). Chaque plan s'écrit par
|
||||||
|
// identifiants, une liste par table, et passe par normaliser et indexerPlan
|
||||||
|
// comme un plan enregistré ; chaque valeur attendue se compte à la main dans
|
||||||
|
// le commentaire qui la précède. Les mesures citées viennent de mesurer :
|
||||||
|
// conflitsParTable et elle lisent le même parcours, et leurs chiffres
|
||||||
|
// s'accordent par construction.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { describe, test } from '../../test/lanceur.js';
|
||||||
|
import { CATALOGUE, PLAN_PARFAIT_PETITE } from '../demo/catalogue.js';
|
||||||
|
import { conflitsParTable } from './conflits.js';
|
||||||
|
import { indexerPlan, normaliser } from './configuration.js';
|
||||||
|
import { mesurer } from './indicateurs.js';
|
||||||
|
|
||||||
|
const TOUTES = Object.freeze({
|
||||||
|
separerAppartenances: true,
|
||||||
|
nouveauxVoisins: true,
|
||||||
|
nouvelleTable: true,
|
||||||
|
varierAppartenances: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const MERLES = 'Club des Merles';
|
||||||
|
|
||||||
|
const tous = (participant, table) => ({ participant, table, portee: 'tous' });
|
||||||
|
const auTour = (participant, table, tour) => ({ participant, table, portee: 'tour', tour });
|
||||||
|
|
||||||
|
// Une table sans violation.
|
||||||
|
const sansConflit = (table) => ({ table, nombre: 0, regles: [] });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance et plan indexé d'un plan écrit par identifiants. ids[i] porte
|
||||||
|
* appartenance[i] (par défaut, l'identifiant i + 1) ; idsTables[i] est
|
||||||
|
* l'identifiant de la table d'index i (par défaut i + 1), de la capacité
|
||||||
|
* donnée ; tours[r][i] liste les participants assis à la table d'index i au
|
||||||
|
* tour r + 1.
|
||||||
|
*/
|
||||||
|
function preparer({
|
||||||
|
appartenance,
|
||||||
|
ids = appartenance.map((_, i) => i + 1),
|
||||||
|
capacite,
|
||||||
|
idsTables,
|
||||||
|
tours,
|
||||||
|
reservations = [],
|
||||||
|
contraintes = TOUTES,
|
||||||
|
}) {
|
||||||
|
const participants = appartenance.map((libelle, i) => ({
|
||||||
|
id: ids[i],
|
||||||
|
nom: `P${ids[i]}`,
|
||||||
|
appartenance: libelle,
|
||||||
|
}));
|
||||||
|
const tables = (idsTables ?? tours[0].map((_, i) => i + 1)).map((id, i) => ({
|
||||||
|
id,
|
||||||
|
numero: i + 1,
|
||||||
|
capacite,
|
||||||
|
}));
|
||||||
|
const instance = normaliser({
|
||||||
|
participants,
|
||||||
|
tables,
|
||||||
|
tours: tours.length,
|
||||||
|
reservations,
|
||||||
|
contraintes: { ...contraintes },
|
||||||
|
});
|
||||||
|
const plan = { tables: tables.map(({ id }) => id), tours, reserves: tours.map(() => []) };
|
||||||
|
return { instance, tableDe: indexerPlan(instance, plan) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function conflits(description) {
|
||||||
|
const { instance, tableDe } = preparer(description);
|
||||||
|
return conflitsParTable(instance, tableDe);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mesures(description) {
|
||||||
|
const { instance, tableDe } = preparer(description);
|
||||||
|
return mesurer(instance, tableDe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Six personnes, 1 et 2 du Club des Merles, trois tables de 2, deux tours.
|
||||||
|
const SIX = [MERLES, MERLES, null, null, null, null];
|
||||||
|
const TOUR_1 = [[3, 4], [1, 2], [5, 6]];
|
||||||
|
|
||||||
|
describe('conflitsParTable', () => {
|
||||||
|
test('séparer : la table 2 réunit 1 et 2 au tour 1, une violation ; aucune autre table, à aucun tour', () => {
|
||||||
|
// Au tour 2, (2, 6), (3, 5) et (1, 4) se rencontrent pour la première
|
||||||
|
// fois, et chacun change de table.
|
||||||
|
const description = { appartenance: SIX, capacite: 2, tours: [TOUR_1, [[2, 6], [3, 5], [1, 4]]] };
|
||||||
|
assert.deepEqual(conflits(description), [
|
||||||
|
[
|
||||||
|
sansConflit(1),
|
||||||
|
{ table: 2, nombre: 1, regles: [{ regle: 'separerAppartenances', paires: [[1, 2]] }] },
|
||||||
|
sansConflit(3),
|
||||||
|
],
|
||||||
|
[sansConflit(1), sansConflit(2), sansConflit(3)],
|
||||||
|
]);
|
||||||
|
const sansSeparer = conflits({
|
||||||
|
...description,
|
||||||
|
contraintes: { ...TOUTES, separerAppartenances: false },
|
||||||
|
});
|
||||||
|
assert.deepEqual(sansSeparer, [
|
||||||
|
[sansConflit(1), sansConflit(2), sansConflit(3)],
|
||||||
|
[sansConflit(1), sansConflit(2), sansConflit(3)],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nouvelle table : 3 revient à la table 1, 2 à la table 2 ; une visite réservée deux fois ne se signale pas', () => {
|
||||||
|
// Tour 2 : [3, 6], [2, 5], [1, 4]. 3 était à la table 1 au tour 1, 2 à
|
||||||
|
// la table 2 ; 6, 5, 1 et 4 découvrent la leur.
|
||||||
|
const description = { appartenance: SIX, capacite: 2, tours: [TOUR_1, [[3, 6], [2, 5], [1, 4]]] };
|
||||||
|
const auTour2 = (resultat) => resultat[1];
|
||||||
|
assert.deepEqual(auTour2(conflits(description)), [
|
||||||
|
{ table: 1, nombre: 1, regles: [{ regle: 'nouvelleTable', personnes: [3] }] },
|
||||||
|
{ table: 2, nombre: 1, regles: [{ regle: 'nouvelleTable', personnes: [2] }] },
|
||||||
|
sansConflit(3),
|
||||||
|
]);
|
||||||
|
// 3 ancré à la table 1 : réservé au tour 2 et à sa visite du tour 1, son
|
||||||
|
// retour est imposé.
|
||||||
|
const ancre = { ...description, reservations: [tous(3, 1)] };
|
||||||
|
assert.deepEqual(auTour2(conflits(ancre)), [
|
||||||
|
sansConflit(1),
|
||||||
|
{ table: 2, nombre: 1, regles: [{ regle: 'nouvelleTable', personnes: [2] }] },
|
||||||
|
sansConflit(3),
|
||||||
|
]);
|
||||||
|
assert.deepEqual(mesures(ancre).retoursImposes, [0, 0, 1, 0, 0, 0]);
|
||||||
|
// 3 réservé à la table 1 au seul tour 2 : sa visite du tour 1 était
|
||||||
|
// libre, le retour est choisi, et la mesure le compte choisi.
|
||||||
|
const tour2 = { ...description, reservations: [auTour(3, 1, 2)] };
|
||||||
|
assert.deepEqual(auTour2(conflits(tour2))[0], {
|
||||||
|
table: 1,
|
||||||
|
nombre: 1,
|
||||||
|
regles: [{ regle: 'nouvelleTable', personnes: [3] }],
|
||||||
|
});
|
||||||
|
assert.deepEqual(mesures(tour2).retoursChoisis, [0, 1, 1, 0, 0, 0]);
|
||||||
|
assert.deepEqual(mesures(tour2).retoursImposes, [0, 0, 0, 0, 0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nouveaux voisins : une paire réunie librement puis réservée ensemble reste signalée ; réservée aux deux rencontres, elle ne l’est plus', () => {
|
||||||
|
// Quatre personnes sans appartenance, deux tables de 2. Tour 1 : [1, 2],
|
||||||
|
// [3, 4] ; tour 2 : [3, 4], [1, 2]. Personne ne revient à sa table.
|
||||||
|
const description = {
|
||||||
|
appartenance: [null, null, null, null],
|
||||||
|
capacite: 2,
|
||||||
|
tours: [
|
||||||
|
[[1, 2], [3, 4]],
|
||||||
|
[[3, 4], [1, 2]],
|
||||||
|
],
|
||||||
|
reservations: [auTour(3, 1, 2), auTour(4, 1, 2)],
|
||||||
|
};
|
||||||
|
assert.deepEqual(conflits(description)[1], [
|
||||||
|
{ table: 1, nombre: 1, regles: [{ regle: 'nouveauxVoisins', paires: [[3, 4]] }] },
|
||||||
|
{ table: 2, nombre: 1, regles: [{ regle: 'nouveauxVoisins', paires: [[1, 2]] }] },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(mesures(description).rencontresRepetees, { choisies: 2, imposees: 0 });
|
||||||
|
// 3 et 4 réservés aussi à la table 2 au tour 1 : chacune de leurs
|
||||||
|
// rencontres est réservée pour les deux.
|
||||||
|
const imposee = {
|
||||||
|
...description,
|
||||||
|
reservations: [...description.reservations, auTour(3, 2, 1), auTour(4, 2, 1)],
|
||||||
|
};
|
||||||
|
assert.deepEqual(conflits(imposee)[1], [
|
||||||
|
sansConflit(1),
|
||||||
|
{ table: 2, nombre: 1, regles: [{ regle: 'nouveauxVoisins', paires: [[1, 2]] }] },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(mesures(imposee).rencontresRepetees, { choisies: 1, imposees: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('les trois règles à une même table, dans leur ordre, comptées ensemble, en identifiants', () => {
|
||||||
|
// Participants 5, 9, 14 et 20 ; 5 et 9 de Grisaille. Tables d'identifiants
|
||||||
|
// 40 puis 10, dans cet ordre de configuration. Les deux mêmes paires aux
|
||||||
|
// deux tours : au tour 2, la table 40 porte (5, 9) collègues et revus, 5
|
||||||
|
// et 9 revenus, quatre violations ; la table 10, (14, 20) revus, 14 et
|
||||||
|
// 20 revenus, trois.
|
||||||
|
const description = {
|
||||||
|
appartenance: ['Grisaille', 'Grisaille', null, null],
|
||||||
|
ids: [5, 9, 14, 20],
|
||||||
|
idsTables: [40, 10],
|
||||||
|
capacite: 2,
|
||||||
|
tours: [
|
||||||
|
[[5, 9], [14, 20]],
|
||||||
|
[[5, 9], [14, 20]],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert.deepEqual(conflits(description), [
|
||||||
|
[
|
||||||
|
{ table: 40, nombre: 1, regles: [{ regle: 'separerAppartenances', paires: [[5, 9]] }] },
|
||||||
|
sansConflit(10),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
table: 40,
|
||||||
|
nombre: 4,
|
||||||
|
regles: [
|
||||||
|
{ regle: 'separerAppartenances', paires: [[5, 9]] },
|
||||||
|
{ regle: 'nouveauxVoisins', paires: [[5, 9]] },
|
||||||
|
{ regle: 'nouvelleTable', personnes: [5, 9] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: 10,
|
||||||
|
nombre: 3,
|
||||||
|
regles: [
|
||||||
|
{ regle: 'nouveauxVoisins', paires: [[14, 20]] },
|
||||||
|
{ regle: 'nouvelleTable', personnes: [14, 20] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
// Une règle inactive est omise, les autres gardent leur ordre.
|
||||||
|
const sansVoisins = conflits({ ...description, contraintes: { ...TOUTES, nouveauxVoisins: false } });
|
||||||
|
assert.deepEqual(sansVoisins[1][0], {
|
||||||
|
table: 40,
|
||||||
|
nombre: 3,
|
||||||
|
regles: [
|
||||||
|
{ regle: 'separerAppartenances', paires: [[5, 9]] },
|
||||||
|
{ regle: 'nouvelleTable', personnes: [5, 9] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const sansTable = conflits({ ...description, contraintes: { ...TOUTES, nouvelleTable: false } });
|
||||||
|
assert.deepEqual(sansTable[1][1], {
|
||||||
|
table: 10,
|
||||||
|
nombre: 1,
|
||||||
|
regles: [{ regle: 'nouveauxVoisins', paires: [[14, 20]] }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une table de plusieurs collègues : chaque paire, identifiants croissants, paires dans l’ordre lexicographique', () => {
|
||||||
|
// 2, 4, 6 et 8 du Club des Merles, assis tous les quatre à une table de 5
|
||||||
|
// avec 3, donné avant eux dans la liste : C(4, 2) = 6 paires.
|
||||||
|
const resultat = conflits({
|
||||||
|
appartenance: [null, MERLES, null, MERLES, MERLES, MERLES],
|
||||||
|
ids: [1, 2, 3, 4, 6, 8],
|
||||||
|
capacite: 5,
|
||||||
|
tours: [[[8, 6, 3, 4, 2], [1]]],
|
||||||
|
});
|
||||||
|
assert.deepEqual(resultat[0][0], {
|
||||||
|
table: 1,
|
||||||
|
nombre: 6,
|
||||||
|
regles: [
|
||||||
|
{
|
||||||
|
regle: 'separerAppartenances',
|
||||||
|
paires: [[2, 4], [2, 6], [2, 8], [4, 6], [4, 8], [6, 8]],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une seule table, un seul tour : aucune violation, des répétitions à zéro, sans lever (§ 15.4)', () => {
|
||||||
|
// Trois personnes, 3 seule du Club des Merles. 1 et 2 rencontrent un
|
||||||
|
// affilié d'une appartenance : A = 1, d = 1 ; 3 n'en rencontre aucun :
|
||||||
|
// A = 0, et il sort de l'effectif de la diversité.
|
||||||
|
const description = {
|
||||||
|
appartenance: [null, null, MERLES],
|
||||||
|
capacite: 3,
|
||||||
|
tours: [[[1, 2, 3]]],
|
||||||
|
};
|
||||||
|
assert.deepEqual(conflits(description), [[sansConflit(1)]]);
|
||||||
|
const { rencontresRepetees, aggAppartenancesVues, aggDiversite } = mesures(description);
|
||||||
|
assert.deepEqual(rencontresRepetees, { choisies: 0, imposees: 0 });
|
||||||
|
assert.deepEqual(aggAppartenancesVues.tous, { min: 0, moyenne: 2 / 3, effectif: 3 });
|
||||||
|
assert.deepEqual(aggDiversite.tous, { min: 1, moyenne: 1, effectif: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('aucun présent : une table, deux tours, chacun à zéro, sans lever', () => {
|
||||||
|
const instance = normaliser({
|
||||||
|
participants: [{ id: 1, nom: 'P1', appartenance: MERLES, exclu: true }],
|
||||||
|
tables: [{ id: 1, numero: 1, capacite: 2 }],
|
||||||
|
tours: 2,
|
||||||
|
reservations: [],
|
||||||
|
contraintes: { ...TOUTES },
|
||||||
|
});
|
||||||
|
assert.deepEqual(conflitsParTable(instance, new Int32Array(0)), [[sansConflit(1)], [sansConflit(1)]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plan parfait de la petite démonstration : aucune violation, à chaque table de chaque tour', () => {
|
||||||
|
const configuration = CATALOGUE.find(({ cle }) => cle === 'petite').construire();
|
||||||
|
const instance = normaliser({ ...configuration, contraintes: { ...TOUTES } });
|
||||||
|
const resultat = conflitsParTable(instance, indexerPlan(instance, PLAN_PARFAIT_PETITE));
|
||||||
|
const cases = resultat.flat();
|
||||||
|
assert.equal(cases.length, 16);
|
||||||
|
for (const [k, { nombre, regles }] of cases.entries()) {
|
||||||
|
assert.equal(nombre, 0, `case ${k}`);
|
||||||
|
assert.deepEqual(regles, [], `case ${k}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lève ce que lève exigerPlanIndexe, et ne modifie ni l'instance ni le plan", () => {
|
||||||
|
const { instance, tableDe } = preparer({
|
||||||
|
appartenance: SIX,
|
||||||
|
capacite: 2,
|
||||||
|
tours: [TOUR_1, [[3, 6], [2, 5], [1, 4]]],
|
||||||
|
});
|
||||||
|
assert.throws(() => conflitsParTable(instance, tableDe.subarray(1)), RangeError);
|
||||||
|
const avant = { tableDe: tableDe.slice(), fixe: instance.fixe.slice(), groupe: instance.groupe.slice() };
|
||||||
|
conflitsParTable(instance, tableDe);
|
||||||
|
assert.deepEqual({ tableDe, fixe: instance.fixe, groupe: instance.groupe }, avant);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -14,6 +14,11 @@
|
||||||
// configuration.js, plafond a priori de plafond.js, retours imposés de la
|
// 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
|
// mesure d'indicateurs.js. Le diagnostic ne modifie pas la configuration et
|
||||||
// lève ce que normaliser lève.
|
// lève ce que normaliser lève.
|
||||||
|
//
|
||||||
|
// Un remède se chiffre en sollicitant la même arithmétique sur une
|
||||||
|
// configuration modifiée — plus de tables, moins de tours —, jamais par une
|
||||||
|
// forme close de plus (§ 13.2) : la sonde passe par normaliser, puis par la
|
||||||
|
// fonction qui chiffre la quantité sur la configuration elle-même.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} PlancherCollisions
|
* @typedef {Object} PlancherCollisions
|
||||||
|
|
@ -23,21 +28,39 @@
|
||||||
* @property {number} plancherPairesDistinctes
|
* @property {number} plancherPairesDistinctes
|
||||||
* @property {number|null} plancherExcedent null quand il n'est pas
|
* @property {number|null} plancherExcedent null quand il n'est pas
|
||||||
* strictement positif
|
* strictement positif
|
||||||
|
* @property {number|null} tablesManquantes plus petit nombre de tables, de
|
||||||
|
* la plus grande capacité présente, dont l'ajout ramène ce plancher
|
||||||
|
* à 0 ; null quand l'effectif du groupe n'y suffit pas : les
|
||||||
|
* réservations assoient alors deux membres à une même table d'un tour
|
||||||
*
|
*
|
||||||
* @typedef {Object} Diagnostic
|
* @typedef {Object} Diagnostic
|
||||||
* @property {number} placesManquantes max(0, N − Σ c_t), par tour
|
* @property {number} placesManquantes max(0, N − Σ c_t), par tour
|
||||||
* @property {{min: number|null, max: number|null, parPersonne: number[]}} plafondAPriori
|
* @property {{min: number|null, max: number|null, parPersonne: number[],
|
||||||
|
* repartition: Array<{valeur: number, ancres: number, mobiles: number}>}} plafondAPriori
|
||||||
* parPersonne dans l'ordre de instance.ids ; min et max null pour
|
* parPersonne dans l'ordre de instance.ids ; min et max null pour
|
||||||
* une population vide
|
* une population vide ; repartition, une ligne par valeur
|
||||||
|
* présente, valeurs croissantes, les partiellement fixés parmi les
|
||||||
|
* mobiles
|
||||||
* @property {PlancherCollisions[]} collisions groupes dont le plancher de
|
* @property {PlancherCollisions[]} collisions groupes dont le plancher de
|
||||||
* collisions cumulées est > 0, dans l'ordre de instance.groupes
|
* collisions cumulées est > 0, dans l'ordre de instance.groupes
|
||||||
* @property {number|null} redondanceMinimale null sans affilié
|
* @property {number|null} redondanceMinimale null sans affilié
|
||||||
|
* @property {{toursSansRedondance: number|null}} remedes
|
||||||
|
* toursSansRedondance : plus grand R′ < R où redondanceMinimale vaut
|
||||||
|
* 0 ; null quand elle vaut déjà 0 ou null, ou qu'aucun R′ ne l'annule
|
||||||
* @property {boolean} animateursMemeAppartenance
|
* @property {boolean} animateursMemeAppartenance
|
||||||
* @property {number} redondanceImposeeParAnimateurs min(R, T) − 1 ou 0
|
* @property {number} redondanceImposeeParAnimateurs min(R, T) − 1 ou 0
|
||||||
* @property {Array<{table: number, ancres: number, capacite: number, gelee: boolean}>} ancrage
|
* @property {boolean} uneSeuleAppartenance séparation active, une seule
|
||||||
|
* appartenance, portée par chaque présent (§ 15.4)
|
||||||
|
* @property {boolean} collisionPartout tout plan qui assied chacun
|
||||||
|
* porte une collision à chaque table de chaque tour (§ 7.5)
|
||||||
|
* @property {Array<{table: number, ancres: number, capacite: number, gelee: boolean,
|
||||||
|
* moitie: boolean}>} ancrage
|
||||||
* chaque table, par identifiant, dans l'ordre de la configuration ;
|
* chaque table, par identifiant, dans l'ordre de la configuration ;
|
||||||
* gelee quand ses ancrés occupent tous ses sièges
|
* gelee quand ses ancrés occupent tous ses sièges ; moitie quand ils
|
||||||
|
* en occupent la moitié au moins, sans la geler
|
||||||
* @property {number} retoursImposes
|
* @property {number} retoursImposes
|
||||||
|
* @property {number} ancragesImposantRetours personnes dont les réservations
|
||||||
|
* imposent un retour
|
||||||
* @property {{plancher: number, mobilesAuMoins: number}|null} ecartItineraire
|
* @property {{plancher: number, mobilesAuMoins: number}|null} ecartItineraire
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
|
@ -154,14 +177,110 @@ function animateursMemeAppartenance({ N, statut, groupe }) {
|
||||||
|
|
||||||
// Ancrés et capacité de chaque table, dans l'ordre de l'instance, qui est
|
// Ancrés et capacité de chaque table, dans l'ordre de l'instance, qui est
|
||||||
// celui de la configuration ; gelee quand les ancrés occupent tous les sièges
|
// celui de la configuration ; gelee quand les ancrés occupent tous les sièges
|
||||||
// (§ 5.9).
|
// (§ 5.9) ; moitie quand ils en occupent la moitié au moins, 2 × a_t ≥ c_t,
|
||||||
|
// sur une table qui tourne encore.
|
||||||
function ancrage({ T, idsTables, capacite, ancresParTable }) {
|
function ancrage({ T, idsTables, capacite, ancresParTable }) {
|
||||||
return Array.from({ length: T }, (_, t) => ({
|
return Array.from({ length: T }, (_, t) => {
|
||||||
table: idsTables[t],
|
const gelee = ancresParTable[t] === capacite[t];
|
||||||
ancres: ancresParTable[t],
|
return {
|
||||||
capacite: capacite[t],
|
table: idsTables[t],
|
||||||
gelee: ancresParTable[t] === capacite[t],
|
ancres: ancresParTable[t],
|
||||||
}));
|
capacite: capacite[t],
|
||||||
|
gelee,
|
||||||
|
moitie: !gelee && 2 * ancresParTable[t] >= capacite[t],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plafonds a priori regroupés par valeur, valeurs croissantes : à chacune,
|
||||||
|
// les ancrés et les mobiles, partiellement fixés compris (§ 5.4), qui la
|
||||||
|
// portent. Les personnes se parcourent par plafond, puis par index.
|
||||||
|
function repartition(aPriori, statut) {
|
||||||
|
const ordre = Array.from({ length: aPriori.length }, (_, p) => p);
|
||||||
|
ordre.sort((p, q) => aPriori[p] - aPriori[q] || p - q);
|
||||||
|
const lignes = [];
|
||||||
|
for (const p of ordre) {
|
||||||
|
let ligne = lignes[lignes.length - 1];
|
||||||
|
if (ligne === undefined || ligne.valeur !== aPriori[p]) {
|
||||||
|
ligne = { valeur: aPriori[p], ancres: 0, mobiles: 0 };
|
||||||
|
lignes.push(ligne);
|
||||||
|
}
|
||||||
|
if (statut[p] === STATUT.ANCRE) ligne.ancres += 1;
|
||||||
|
else ligne.mobiles += 1;
|
||||||
|
}
|
||||||
|
return lignes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chaque plancher de collisions, complété de tablesManquantes. La sonde à m
|
||||||
|
// tables ajoute m tables de la plus grande capacité présente, d'identifiants
|
||||||
|
// au-delà du plus grand, et rechiffre les planchers par planchersCollisions :
|
||||||
|
// le premier m où le groupe n'y figure plus est son remède. Un groupe de s
|
||||||
|
// membres se sépare dès que s tables de plus s'offrent à ses membres libres,
|
||||||
|
// sauf quand des réservations en assoient deux à une même table d'un tour :
|
||||||
|
// au-delà de s, il garde null. Les capacités, relâchées dans le plancher, ne
|
||||||
|
// changent pas le compte ; elles disent quelles tables ajouter. Un plancher
|
||||||
|
// suppose une table au moins.
|
||||||
|
function avecTablesManquantes(configuration, instance, planchers) {
|
||||||
|
const lignes = planchers.map((plancher) => ({ ...plancher, tablesManquantes: null }));
|
||||||
|
if (lignes.length === 0) return lignes;
|
||||||
|
const enAttente = (m) => lignes.some((l) => l.tablesManquantes === null && m <= l.effectif);
|
||||||
|
const capacite = Math.max(...instance.capacite);
|
||||||
|
const premierId = Math.max(...instance.idsTables) + 1;
|
||||||
|
const ajoutees = [];
|
||||||
|
for (let m = 1; enAttente(m); m += 1) {
|
||||||
|
ajoutees.push({ id: premierId + m - 1, numero: configuration.tables.length + m, capacite });
|
||||||
|
const sonde = normaliser({ ...configuration, tables: [...configuration.tables, ...ajoutees] });
|
||||||
|
const restants = new Set(planchersCollisions(sonde).map(({ groupe }) => groupe));
|
||||||
|
for (const ligne of lignes) {
|
||||||
|
if (ligne.tablesManquantes === null && m <= ligne.effectif && !restants.has(ligne.groupe)) {
|
||||||
|
ligne.tablesManquantes = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lignes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plus grand R′ < R où la redondance minimale s'annule, null quand elle vaut
|
||||||
|
// déjà 0 ou null, ou qu'aucun R′ ne l'annule. La sonde à R′ tours garde les
|
||||||
|
// réservations de portée « tous », qui couvrent alors ses R′ tours, et retire
|
||||||
|
// celles d'un tour au-delà de R′, exclus compris : normaliser les refuserait
|
||||||
|
// (RESERVATION_TOUR). Les R′ se sondent du plus grand au plus petit.
|
||||||
|
function toursSansRedondance(configuration, R, minimale) {
|
||||||
|
if (minimale === null || minimale === 0) return null;
|
||||||
|
for (let tours = R - 1; tours >= 1; tours -= 1) {
|
||||||
|
const reservations = configuration.reservations.filter(
|
||||||
|
({ portee, tour }) => portee !== 'tour' || tour <= tours,
|
||||||
|
);
|
||||||
|
const sonde = normaliser({ ...configuration, tours, reservations });
|
||||||
|
if (redondanceMinimale(sonde, plafondsAPriori(sonde)) === 0) return tours;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vrai quand la séparation est active et que les présents portent tous une
|
||||||
|
// même appartenance, la seule (§ 15.4) ; faux dès qu'un présent est sans
|
||||||
|
// appartenance : la séparation y garde un sens.
|
||||||
|
function uneSeuleAppartenance({ groupe, groupes, contraintes }) {
|
||||||
|
return contraintes.separerAppartenances && groupes.length === 1 && !groupe.includes(SANS_GROUPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vrai quand tout plan qui assied chacun porte une collision à chaque table
|
||||||
|
// de chaque tour (§ 7.5). Les autres tables tiennent au plus Σ c − c_t
|
||||||
|
// personnes : la table t en reçoit au moins N − (Σ c − c_t). Sans collision,
|
||||||
|
// elle n'assied qu'un membre de chacune des G appartenances et les U présents
|
||||||
|
// sans appartenance : au-delà de G + U, la collision est forcée. Faux quand
|
||||||
|
// la séparation est inactive, qu'une place manque — aucun plan n'assied
|
||||||
|
// chacun —, ou sans table : rien ne le prouve.
|
||||||
|
function collisionPartout(instance, placesManquantes) {
|
||||||
|
const { N, T, capacite, groupe, groupes, contraintes } = instance;
|
||||||
|
if (!contraintes.separerAppartenances || placesManquantes > 0 || T === 0) return false;
|
||||||
|
let sansAppartenance = 0;
|
||||||
|
for (let p = 0; p < N; p += 1) if (groupe[p] === SANS_GROUPE) sansAppartenance += 1;
|
||||||
|
const places = nombrePlaces(instance);
|
||||||
|
for (let t = 0; t < T; t += 1) {
|
||||||
|
if (N - (places - capacite[t]) <= groupes.length + sansAppartenance) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plafond du meilleur itinéraire d'un mobile p qui passe par la table t : le
|
// Plafond du meilleur itinéraire d'un mobile p qui passe par la table t : le
|
||||||
|
|
@ -249,19 +368,23 @@ function etendue(valeurs) {
|
||||||
*
|
*
|
||||||
* - placesManquantes : nombrePlacesManquantes ; la recherche refuse une
|
* - placesManquantes : nombrePlacesManquantes ; la recherche refuse une
|
||||||
* configuration où elles sont positives, le diagnostic les chiffre (§ 5.9).
|
* configuration où elles sont positives, le diagnostic les chiffre (§ 5.9).
|
||||||
* - plafondAPriori : plafondsAPriori, et son étendue.
|
* - plafondAPriori : plafondsAPriori, son étendue et sa répartition par
|
||||||
|
* valeur.
|
||||||
* - collisions : planchers de collisions par groupe, posés sur les tours
|
* - collisions : planchers de collisions par groupe, posés sur les tours
|
||||||
* fixés, capacités relâchées.
|
* fixés, capacités relâchées ; chacun avec les tables qui l'annulent.
|
||||||
* - redondanceMinimale : max_p max(0, min(a priori(p), affiliés autres que
|
* - redondanceMinimale : max_p max(0, min(a priori(p), affiliés autres que
|
||||||
* p) − G).
|
* p) − G) ; remedes.toursSansRedondance, le nombre de tours qui l'annule.
|
||||||
* - animateursMemeAppartenance, et redondanceImposeeParAnimateurs : la forme
|
* - animateursMemeAppartenance, et redondanceImposeeParAnimateurs : la forme
|
||||||
* close min(R, T) − 1 du § 5.6, qui compte un animateur à chaque table
|
* close min(R, T) − 1 du § 5.6, qui compte un animateur à chaque table
|
||||||
* visitée ; 0 quand les animateurs ne partagent pas une appartenance.
|
* visitée ; 0 quand les animateurs ne partagent pas une appartenance.
|
||||||
* - ancrage : ancrés, capacité et gel de chaque table.
|
* - uneSeuleAppartenance et collisionPartout : une séparation vide de sens,
|
||||||
|
* une collision forcée à chaque table.
|
||||||
|
* - ancrage : ancrés, capacité, gel et demi-ancrage de chaque table.
|
||||||
* - retoursImposes : la rangée de instance.fixe se lit comme le plan qui
|
* - retoursImposes : la rangée de instance.fixe se lit comme le plan qui
|
||||||
* assied chacun aux seuls tours fixés, en réserve ailleurs ; sa mesure
|
* assied chacun aux seuls tours fixés, en réserve ailleurs ; sa mesure
|
||||||
* compte max(0, f − 1) retours pour f tours réservés à une même table, ce
|
* compte max(0, f − 1) retours pour f tours réservés à une même table, ce
|
||||||
* que mesure tout plan qui honore les réservations (§ 5.4).
|
* que mesure tout plan qui honore les réservations (§ 5.4) ;
|
||||||
|
* ancragesImposantRetours, les personnes qui en portent un au moins.
|
||||||
* - ecartItineraire : plancher de l'écart d'itinéraire maximal, ou null.
|
* - ecartItineraire : plancher de l'écart d'itinéraire maximal, ou null.
|
||||||
*
|
*
|
||||||
* @param {import('./types.js').Configuration} configuration
|
* @param {import('./types.js').Configuration} configuration
|
||||||
|
|
@ -271,15 +394,26 @@ export function diagnostiquer(configuration) {
|
||||||
const instance = normaliser(configuration);
|
const instance = normaliser(configuration);
|
||||||
const aPriori = plafondsAPriori(instance);
|
const aPriori = plafondsAPriori(instance);
|
||||||
const memeAppartenance = animateursMemeAppartenance(instance);
|
const memeAppartenance = animateursMemeAppartenance(instance);
|
||||||
|
const placesManquantes = nombrePlacesManquantes(instance);
|
||||||
|
const minimale = redondanceMinimale(instance, aPriori);
|
||||||
|
const imposes = mesurer(instance, instance.fixe);
|
||||||
return {
|
return {
|
||||||
placesManquantes: nombrePlacesManquantes(instance),
|
placesManquantes,
|
||||||
plafondAPriori: { ...etendue(aPriori), parPersonne: aPriori },
|
plafondAPriori: {
|
||||||
collisions: planchersCollisions(instance),
|
...etendue(aPriori),
|
||||||
redondanceMinimale: redondanceMinimale(instance, aPriori),
|
parPersonne: aPriori,
|
||||||
|
repartition: repartition(aPriori, instance.statut),
|
||||||
|
},
|
||||||
|
collisions: avecTablesManquantes(configuration, instance, planchersCollisions(instance)),
|
||||||
|
redondanceMinimale: minimale,
|
||||||
|
remedes: { toursSansRedondance: toursSansRedondance(configuration, instance.R, minimale) },
|
||||||
animateursMemeAppartenance: memeAppartenance,
|
animateursMemeAppartenance: memeAppartenance,
|
||||||
redondanceImposeeParAnimateurs: memeAppartenance ? Math.min(instance.R, instance.T) - 1 : 0,
|
redondanceImposeeParAnimateurs: memeAppartenance ? Math.min(instance.R, instance.T) - 1 : 0,
|
||||||
|
uneSeuleAppartenance: uneSeuleAppartenance(instance),
|
||||||
|
collisionPartout: collisionPartout(instance, placesManquantes),
|
||||||
ancrage: ancrage(instance),
|
ancrage: ancrage(instance),
|
||||||
retoursImposes: mesurer(instance, instance.fixe).totalRetoursImposes,
|
retoursImposes: imposes.totalRetoursImposes,
|
||||||
|
ancragesImposantRetours: imposes.retoursImposes.filter((retours) => retours > 0).length,
|
||||||
ecartItineraire: ecartItineraire(configuration, instance, aPriori),
|
ecartItineraire: ecartItineraire(configuration, instance, aPriori),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { describe, test } from '../../test/lanceur.js';
|
import { describe, test } from '../../test/lanceur.js';
|
||||||
import { CATALOGUE, PLAN_PARFAIT_PETITE } from '../demo/catalogue.js';
|
import { CATALOGUE, PLAN_PARFAIT_PETITE } from '../demo/catalogue.js';
|
||||||
|
import { conflitsParTable } from './conflits.js';
|
||||||
import { LIBRE, STATUT, indexerPlan, normaliser } from './configuration.js';
|
import { LIBRE, STATUT, indexerPlan, normaliser } from './configuration.js';
|
||||||
import { diagnostiquer } from './diagnostic.js';
|
import { diagnostiquer } from './diagnostic.js';
|
||||||
import { ErreurConfiguration } from './erreurs.js';
|
import { ErreurConfiguration } from './erreurs.js';
|
||||||
|
|
@ -23,15 +24,31 @@ const SANS_CONTRAINTE = {
|
||||||
nouvelleTable: false,
|
nouvelleTable: false,
|
||||||
varierAppartenances: false,
|
varierAppartenances: false,
|
||||||
};
|
};
|
||||||
|
const TOUTES = {
|
||||||
|
separerAppartenances: true,
|
||||||
|
nouveauxVoisins: true,
|
||||||
|
nouvelleTable: true,
|
||||||
|
varierAppartenances: true,
|
||||||
|
};
|
||||||
|
const SEPARER = { ...SANS_CONTRAINTE, separerAppartenances: true };
|
||||||
|
const TOUTES_SAUF_SEPARER = { ...TOUTES, separerAppartenances: false };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration écrite à la main. tables porte une paire [id, capacité] par
|
* Configuration écrite à la main. tables porte une paire [id, capacité] par
|
||||||
* table, dans l'ordre de la configuration, le numéro suivant la position ;
|
* table, dans l'ordre de la configuration, le numéro suivant la position ;
|
||||||
* appartenances[j] est celle du participant d'id j + 1 ; exclus nomme des ids.
|
* appartenances[j] est celle du participant d'id j + 1 ; exclus nomme des ids ;
|
||||||
|
* contraintes, aucune par défaut.
|
||||||
* Les identifiants de table diffèrent des index et des numéros : un index lu
|
* Les identifiants de table diffèrent des index et des numéros : un index lu
|
||||||
* comme un identifiant désigne une table absente.
|
* comme un identifiant désigne une table absente.
|
||||||
*/
|
*/
|
||||||
function salle({ tables, appartenances, tours, reservations = [], exclus = [] }) {
|
function salle({
|
||||||
|
tables,
|
||||||
|
appartenances,
|
||||||
|
tours,
|
||||||
|
reservations = [],
|
||||||
|
exclus = [],
|
||||||
|
contraintes = SANS_CONTRAINTE,
|
||||||
|
}) {
|
||||||
return {
|
return {
|
||||||
participants: appartenances.map((appartenance, j) => ({
|
participants: appartenances.map((appartenance, j) => ({
|
||||||
id: j + 1,
|
id: j + 1,
|
||||||
|
|
@ -42,7 +59,7 @@ function salle({ tables, appartenances, tours, reservations = [], exclus = [] })
|
||||||
tables: tables.map(([id, capacite], i) => ({ id, numero: i + 1, capacite })),
|
tables: tables.map(([id, capacite], i) => ({ id, numero: i + 1, capacite })),
|
||||||
tours,
|
tours,
|
||||||
reservations,
|
reservations,
|
||||||
contraintes: SANS_CONTRAINTE,
|
contraintes: { ...contraintes },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,8 +234,20 @@ describe('diagnostic : le catalogue (§ 15)', () => {
|
||||||
ancres: 1,
|
ancres: 1,
|
||||||
capacite: t < 29 ? 8 : 7,
|
capacite: t < 29 ? 8 : 7,
|
||||||
gelee: false,
|
gelee: false,
|
||||||
|
moitie: false,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
// 24 aux quatre animateurs des tables de 7, 28 à chacun des autres ; les
|
||||||
|
// 227 mobiles sont tous à 28.
|
||||||
|
assert.deepEqual(diagnostic.plafondAPriori.repartition, [
|
||||||
|
{ valeur: 24, ancres: 4, mobiles: 0 },
|
||||||
|
{ valeur: 28, ancres: 29, mobiles: 227 },
|
||||||
|
]);
|
||||||
|
// Chacun des 33 animateurs revient trois fois à sa table.
|
||||||
|
assert.equal(diagnostic.ancragesImposantRetours, 33);
|
||||||
|
assert.equal(diagnostic.uneSeuleAppartenance, false);
|
||||||
|
assert.equal(diagnostic.collisionPartout, false);
|
||||||
|
assert.deepEqual(diagnostic.remedes, { toursSansRedondance: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 264 sièges pour 260 membres : la configuration n'est pas tendue.
|
// 264 sièges pour 260 membres : la configuration n'est pas tendue.
|
||||||
|
|
@ -250,6 +279,7 @@ describe('diagnostic : le catalogue (§ 15)', () => {
|
||||||
plancherCumulees: 4,
|
plancherCumulees: 4,
|
||||||
plancherPairesDistinctes: 1,
|
plancherPairesDistinctes: 1,
|
||||||
plancherExcedent: null,
|
plancherExcedent: null,
|
||||||
|
tablesManquantes: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -403,6 +433,7 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
plancherCumulees: 5,
|
plancherCumulees: 5,
|
||||||
plancherPairesDistinctes: 1,
|
plancherPairesDistinctes: 1,
|
||||||
plancherExcedent: 2,
|
plancherExcedent: 2,
|
||||||
|
tablesManquantes: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(minimumsParGroupe(configuration).get('X'), {
|
assert.deepEqual(minimumsParGroupe(configuration).get('X'), {
|
||||||
|
|
@ -430,7 +461,14 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
tours: 4,
|
tours: 4,
|
||||||
});
|
});
|
||||||
assert.deepEqual(diagnostiquer(configuration).collisions, [
|
assert.deepEqual(diagnostiquer(configuration).collisions, [
|
||||||
{ groupe: 'X', effectif: 6, plancherCumulees: 8, plancherPairesDistinctes: 2, plancherExcedent: null },
|
{
|
||||||
|
groupe: 'X',
|
||||||
|
effectif: 6,
|
||||||
|
plancherCumulees: 8,
|
||||||
|
plancherPairesDistinctes: 2,
|
||||||
|
plancherExcedent: null,
|
||||||
|
tablesManquantes: 2,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -441,7 +479,14 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
tours: 2,
|
tours: 2,
|
||||||
});
|
});
|
||||||
assert.deepEqual(diagnostiquer(configuration).collisions, [
|
assert.deepEqual(diagnostiquer(configuration).collisions, [
|
||||||
{ groupe: 'X', effectif: 6, plancherCumulees: 12, plancherPairesDistinctes: 6, plancherExcedent: null },
|
{
|
||||||
|
groupe: 'X',
|
||||||
|
effectif: 6,
|
||||||
|
plancherCumulees: 12,
|
||||||
|
plancherPairesDistinctes: 6,
|
||||||
|
plancherExcedent: null,
|
||||||
|
tablesManquantes: 4,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
const exacts = minimumsParGroupe(configuration).get('X');
|
const exacts = minimumsParGroupe(configuration).get('X');
|
||||||
assert.equal(exacts.cumulees, 12);
|
assert.equal(exacts.cumulees, 12);
|
||||||
|
|
@ -455,7 +500,14 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
tours: 3,
|
tours: 3,
|
||||||
});
|
});
|
||||||
assert.deepEqual(diagnostiquer(configuration).collisions, [
|
assert.deepEqual(diagnostiquer(configuration).collisions, [
|
||||||
{ groupe: 'X', effectif: 5, plancherCumulees: 12, plancherPairesDistinctes: 4, plancherExcedent: 2 },
|
{
|
||||||
|
groupe: 'X',
|
||||||
|
effectif: 5,
|
||||||
|
plancherCumulees: 12,
|
||||||
|
plancherPairesDistinctes: 4,
|
||||||
|
plancherExcedent: 2,
|
||||||
|
tablesManquantes: 3,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(minimumsParGroupe(configuration).get('X'), { cumulees: 12, distinctes: 4, excedent: 2 });
|
assert.deepEqual(minimumsParGroupe(configuration).get('X'), { cumulees: 12, distinctes: 4, excedent: 2 });
|
||||||
});
|
});
|
||||||
|
|
@ -473,6 +525,7 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
plancherCumulees: 4,
|
plancherCumulees: 4,
|
||||||
plancherPairesDistinctes: 1,
|
plancherPairesDistinctes: 1,
|
||||||
plancherExcedent: 1,
|
plancherExcedent: 1,
|
||||||
|
tablesManquantes: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(minimumsParGroupe(configuration).get('X'), {
|
assert.deepEqual(minimumsParGroupe(configuration).get('X'), {
|
||||||
|
|
@ -502,6 +555,7 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
plancherCumulees: 3,
|
plancherCumulees: 3,
|
||||||
plancherPairesDistinctes: 1,
|
plancherPairesDistinctes: 1,
|
||||||
plancherExcedent: null,
|
plancherExcedent: null,
|
||||||
|
tablesManquantes: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
groupe: 'Y',
|
groupe: 'Y',
|
||||||
|
|
@ -509,6 +563,7 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
plancherCumulees: 1,
|
plancherCumulees: 1,
|
||||||
plancherPairesDistinctes: 1,
|
plancherPairesDistinctes: 1,
|
||||||
plancherExcedent: null,
|
plancherExcedent: null,
|
||||||
|
tablesManquantes: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const minimums = minimumsParGroupe(configuration);
|
const minimums = minimumsParGroupe(configuration);
|
||||||
|
|
@ -532,6 +587,7 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
plancherCumulees: 3,
|
plancherCumulees: 3,
|
||||||
plancherPairesDistinctes: 1,
|
plancherPairesDistinctes: 1,
|
||||||
plancherExcedent: 2,
|
plancherExcedent: 2,
|
||||||
|
tablesManquantes: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -552,6 +608,7 @@ describe('diagnostic : planchers de collisions (§ 5.6)', () => {
|
||||||
plancherCumulees: 2,
|
plancherCumulees: 2,
|
||||||
plancherPairesDistinctes: 1,
|
plancherPairesDistinctes: 1,
|
||||||
plancherExcedent: null,
|
plancherExcedent: null,
|
||||||
|
tablesManquantes: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
@ -933,9 +990,9 @@ describe('diagnostic : ancrage et retours imposés (§ 5.4, § 5.6)', () => {
|
||||||
reservations: [ancrer(1, 30), ancrer(2, 30), ancrer(3, 10), ancrer(4, 10)],
|
reservations: [ancrer(1, 30), ancrer(2, 30), ancrer(3, 10), ancrer(4, 10)],
|
||||||
});
|
});
|
||||||
assert.deepEqual(diagnostiquer(configuration).ancrage, [
|
assert.deepEqual(diagnostiquer(configuration).ancrage, [
|
||||||
{ table: 30, ancres: 2, capacite: 2, gelee: true },
|
{ table: 30, ancres: 2, capacite: 2, gelee: true, moitie: false },
|
||||||
{ table: 10, ancres: 2, capacite: 3, gelee: false },
|
{ table: 10, ancres: 2, capacite: 3, gelee: false, moitie: true },
|
||||||
{ table: 20, ancres: 0, capacite: 2, gelee: false },
|
{ table: 20, ancres: 0, capacite: 2, gelee: false, moitie: false },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -964,6 +1021,8 @@ describe('diagnostic : ancrage et retours imposés (§ 5.4, § 5.6)', () => {
|
||||||
});
|
});
|
||||||
const diagnostic = diagnostiquer(configuration);
|
const diagnostic = diagnostiquer(configuration);
|
||||||
assert.equal(diagnostic.retoursImposes, 7);
|
assert.equal(diagnostic.retoursImposes, 7);
|
||||||
|
// 1, 2 et 5 portent des retours imposés ; 3 n'en porte aucun.
|
||||||
|
assert.equal(diagnostic.ancragesImposantRetours, 3);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
diagnostic.ancrage.map(({ ancres }) => ancres),
|
diagnostic.ancrage.map(({ ancres }) => ancres),
|
||||||
[2, 0],
|
[2, 0],
|
||||||
|
|
@ -998,25 +1057,34 @@ describe('diagnostic : population vide, forme, refus', () => {
|
||||||
test('une population vide rend null, jamais 0, là où rien ne se mesure', () => {
|
test('une population vide rend null, jamais 0, là où rien ne se mesure', () => {
|
||||||
const attendu = {
|
const attendu = {
|
||||||
placesManquantes: 0,
|
placesManquantes: 0,
|
||||||
plafondAPriori: { min: null, max: null, parPersonne: [] },
|
plafondAPriori: { min: null, max: null, parPersonne: [], repartition: [] },
|
||||||
collisions: [],
|
collisions: [],
|
||||||
redondanceMinimale: null,
|
redondanceMinimale: null,
|
||||||
|
remedes: { toursSansRedondance: null },
|
||||||
animateursMemeAppartenance: false,
|
animateursMemeAppartenance: false,
|
||||||
redondanceImposeeParAnimateurs: 0,
|
redondanceImposeeParAnimateurs: 0,
|
||||||
ancrage: [{ table: 10, ancres: 0, capacite: 4, gelee: false }],
|
uneSeuleAppartenance: false,
|
||||||
|
collisionPartout: false,
|
||||||
|
ancrage: [{ table: 10, ancres: 0, capacite: 4, gelee: false, moitie: false }],
|
||||||
retoursImposes: 0,
|
retoursImposes: 0,
|
||||||
|
ancragesImposantRetours: 0,
|
||||||
ecartItineraire: null,
|
ecartItineraire: null,
|
||||||
};
|
};
|
||||||
const vide = salle({ tables: [[10, 4]], appartenances: [], tours: 2 });
|
// Les mêmes chiffres, séparation active ou non : sans présent, aucune
|
||||||
assert.deepEqual(diagnostiquer(vide), attendu);
|
// appartenance n'est portée par chacun, et aucune table n'est forcée.
|
||||||
const tousExclus = salle({
|
for (const contraintes of [SANS_CONTRAINTE, TOUTES]) {
|
||||||
tables: [[10, 4]],
|
const vide = salle({ tables: [[10, 4]], appartenances: [], tours: 2, contraintes });
|
||||||
appartenances: ['X', 'X', 'X'],
|
assert.deepEqual(diagnostiquer(vide), attendu);
|
||||||
tours: 2,
|
const tousExclus = salle({
|
||||||
reservations: [ancrer(1, 10), ancrer(2, 10)],
|
tables: [[10, 4]],
|
||||||
exclus: [1, 2, 3],
|
appartenances: ['X', 'X', 'X'],
|
||||||
});
|
tours: 2,
|
||||||
assert.deepEqual(diagnostiquer(tousExclus), attendu);
|
reservations: [ancrer(1, 10), ancrer(2, 10)],
|
||||||
|
exclus: [1, 2, 3],
|
||||||
|
contraintes,
|
||||||
|
});
|
||||||
|
assert.deepEqual(diagnostiquer(tousExclus), attendu);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('le diagnostic ne modifie pas la configuration', () => {
|
test('le diagnostic ne modifie pas la configuration', () => {
|
||||||
|
|
@ -1041,3 +1109,310 @@ describe('diagnostic : population vide, forme, refus', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('diagnostic : répartition du plafond a priori (§ 5.6)', () => {
|
||||||
|
// 1 et 2, sans appartenance, ancrés à la table 10 de 3 sièges ; quatre
|
||||||
|
// mobiles de X, deux tours : les ancrés plafonnent à 3, les mobiles à 4
|
||||||
|
// (voir « un affilié ne se compte pas parmi les affiliés qu'il
|
||||||
|
// rencontre »).
|
||||||
|
test('une ligne par valeur, croissante, ancrés et mobiles comptés à part', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[10, 3], [20, 3]],
|
||||||
|
appartenances: [null, null, ...fois(4, 'X')],
|
||||||
|
tours: 2,
|
||||||
|
reservations: [ancrer(1, 10), ancrer(2, 10)],
|
||||||
|
});
|
||||||
|
assert.deepEqual(diagnostiquer(configuration).plafondAPriori.repartition, [
|
||||||
|
{ valeur: 3, ancres: 2, mobiles: 0 },
|
||||||
|
{ valeur: 4, ancres: 0, mobiles: 4 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Un partiellement fixé compte parmi les mobiles (§ 5.4) : 1, réservé à la
|
||||||
|
// table 10 au seul tour 1, plafonne comme les autres, la table 10 restant
|
||||||
|
// libre à l'autre tour. Six personnes sur deux tables de 3, deux tours :
|
||||||
|
// chacun rencontre 2 voisins par tour, 4 en tout.
|
||||||
|
test('un partiellement fixé se compte parmi les mobiles', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[10, 3], [20, 3]],
|
||||||
|
appartenances: fois(6, null),
|
||||||
|
tours: 2,
|
||||||
|
reservations: [fixer(1, 10, 1)],
|
||||||
|
});
|
||||||
|
assert.deepEqual(diagnostiquer(configuration).plafondAPriori.repartition, [
|
||||||
|
{ valeur: 4, ancres: 0, mobiles: 6 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('diagnostic : tables manquantes (§ 5.6, § 15.3)', () => {
|
||||||
|
// Le remède compte des tables de la plus grande capacité présente ; le
|
||||||
|
// plancher, capacités relâchées, n'en dépend pas. Tables de 2 et de 6 :
|
||||||
|
// quatre membres de X pour deux tables, deux de plus les séparent.
|
||||||
|
test('des tables de capacités inégales : le compte ne lit que le plancher', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[10, 2], [20, 6]],
|
||||||
|
appartenances: [...fois(4, 'X'), ...fois(4, null)],
|
||||||
|
tours: 2,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
diagnostiquer(configuration).collisions.map(({ tablesManquantes }) => tablesManquantes),
|
||||||
|
[2],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deux groupes : Y, réservé ensemble à la table 10 au tour 2, n'a pas de
|
||||||
|
// remède ; X, cinq membres libres sur trois tables, en a un, 2 tables.
|
||||||
|
test('un groupe sans remède n’empêche pas le remède d’un autre', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[10, 4], [20, 4], [30, 4]],
|
||||||
|
appartenances: [...fois(5, 'X'), 'Y', 'Y', null],
|
||||||
|
tours: 2,
|
||||||
|
reservations: [fixer(6, 10, 2), fixer(7, 10, 2)],
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
diagnostiquer(configuration).collisions.map(({ groupe, tablesManquantes }) => [groupe, tablesManquantes]),
|
||||||
|
[
|
||||||
|
['X', 2],
|
||||||
|
['Y', null],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('diagnostic : remède en tours (§ 5.6)', () => {
|
||||||
|
// 28 personnes, dix appartenances de 2 et huit de 1, G = 18 ; quatre
|
||||||
|
// tables de 7. Un participant rencontre au plus 6 personnes par tour. En
|
||||||
|
// 4 tours : min(24, 27 affiliés autres que soi) − 18 = 6. En 3 tours :
|
||||||
|
// 18 − 18 = 0.
|
||||||
|
const DIX_PAIRES_HUIT_SEULS = [
|
||||||
|
...Array.from({ length: 10 }, (_, g) => fois(2, `Grisaille ${g + 1}`)).flat(),
|
||||||
|
...Array.from({ length: 8 }, (_, g) => `Club des Merles ${g + 1}`),
|
||||||
|
];
|
||||||
|
const QUATRE_TABLES_DE_SEPT = [[10, 7], [20, 7], [30, 7], [40, 7]];
|
||||||
|
|
||||||
|
test('28 personnes en 18 appartenances, quatre tours : 6 redondances, aucune en 3 tours', () => {
|
||||||
|
assert.equal(DIX_PAIRES_HUIT_SEULS.length, 28);
|
||||||
|
const configuration = salle({
|
||||||
|
tables: QUATRE_TABLES_DE_SEPT,
|
||||||
|
appartenances: DIX_PAIRES_HUIT_SEULS,
|
||||||
|
tours: 4,
|
||||||
|
});
|
||||||
|
const diagnostic = diagnostiquer(configuration);
|
||||||
|
assert.equal(diagnostic.redondanceMinimale, 6);
|
||||||
|
assert.deepEqual(diagnostic.remedes, { toursSansRedondance: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// La réservation du tour 4 ne tient pas dans une configuration de 3
|
||||||
|
// tours : la sonde la retire, et celle d'un exclu aussi, au lieu de lever
|
||||||
|
// RESERVATION_TOUR. La réservation de portée « tous » couvre les trois.
|
||||||
|
test('une réservation d’un tour au-delà de R′ se retire de la sonde, sans lever', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: QUATRE_TABLES_DE_SEPT,
|
||||||
|
appartenances: [...DIX_PAIRES_HUIT_SEULS, 'Club des Merles 1'],
|
||||||
|
tours: 4,
|
||||||
|
reservations: [fixer(1, 10, 4), ancrer(2, 20), fixer(29, 30, 4)],
|
||||||
|
exclus: [29],
|
||||||
|
});
|
||||||
|
assert.deepEqual(diagnostiquer(configuration).remedes, { toursSansRedondance: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trois tables de 2, P1 sans appartenance, P2 et P3 de la même. En 3
|
||||||
|
// tours, P1, fixé à la table 3 à chaque tour, y retrouve P3 au tour 2 et
|
||||||
|
// P2 au tour 3 : 2 − 1 = 1. En 2 tours, P2 et P3 occupent la table 2 au
|
||||||
|
// tour 1 et P1 n'y retrouve que P3 au tour 2 : 1 − 1 = 0. Le remède, 2,
|
||||||
|
// tient aux réservations des tours 1 et 2, que la sonde garde.
|
||||||
|
test('les réservations d’un tour jusqu’à R′ restent dans la sonde', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[1, 2], [2, 2], [3, 2]],
|
||||||
|
appartenances: [null, 'Grisaille', 'Grisaille'],
|
||||||
|
tours: 3,
|
||||||
|
reservations: [
|
||||||
|
fixer(1, 3, 1), fixer(1, 3, 2), fixer(1, 3, 3),
|
||||||
|
fixer(2, 2, 1), fixer(2, 2, 2), fixer(2, 3, 3),
|
||||||
|
fixer(3, 2, 1), fixer(3, 3, 2), fixer(3, 2, 3),
|
||||||
|
],
|
||||||
|
contraintes: TOUTES,
|
||||||
|
});
|
||||||
|
const diagnostic = diagnostiquer(configuration);
|
||||||
|
assert.equal(diagnostic.redondanceMinimale, 1);
|
||||||
|
assert.deepEqual(diagnostic.remedes, { toursSansRedondance: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deux tables de 3, P1 sans appartenance ancré à la table 10, P2 et P3 de
|
||||||
|
// la même appartenance, fixés aux tables 10 et 20 aux tours 1 et 2. En 3
|
||||||
|
// tours, le tour 3 libre réunit P1 et P3 : 2 − 1 = 1. En 2 tours, P1 ne
|
||||||
|
// rencontre que P2 : 0. Le remède, 2, tient à l'ancrage, qui couvre les
|
||||||
|
// tours de la sonde, et aux réservations du tour 2, R′ lui-même.
|
||||||
|
test('l’ancrage et les réservations du tour R′ lui-même restent dans la sonde', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[10, 3], [20, 3]],
|
||||||
|
appartenances: [null, 'Grisaille', 'Grisaille'],
|
||||||
|
tours: 3,
|
||||||
|
reservations: [ancrer(1, 10), fixer(2, 10, 1), fixer(3, 20, 1), fixer(2, 10, 2), fixer(3, 20, 2)],
|
||||||
|
});
|
||||||
|
const diagnostic = diagnostiquer(configuration);
|
||||||
|
assert.equal(diagnostic.redondanceMinimale, 1);
|
||||||
|
assert.deepEqual(diagnostic.remedes, { toursSansRedondance: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deux appartenances de 14 : en un seul tour, 6 − 2 = 4 ; aucun R′ ne
|
||||||
|
// l'annule.
|
||||||
|
test('deux appartenances de 14 : aucun nombre de tours n’annule la redondance', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: QUATRE_TABLES_DE_SEPT,
|
||||||
|
appartenances: [...fois(14, 'Grisaille'), ...fois(14, 'Club des Merles')],
|
||||||
|
tours: 4,
|
||||||
|
});
|
||||||
|
const diagnostic = diagnostiquer(configuration);
|
||||||
|
assert.equal(diagnostic.redondanceMinimale, 22);
|
||||||
|
assert.deepEqual(diagnostic.remedes, { toursSansRedondance: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sans affilié, ou à redondance déjà nulle : aucun remède', () => {
|
||||||
|
const sansAffilie = salle({ tables: QUATRE_TABLES_DE_SEPT, appartenances: fois(28, null), tours: 4 });
|
||||||
|
assert.deepEqual(diagnostiquer(sansAffilie).remedes, { toursSansRedondance: null });
|
||||||
|
const dejaNulle = salle({ tables: QUATRE_TABLES_DE_SEPT, appartenances: DIX_PAIRES_HUIT_SEULS, tours: 3 });
|
||||||
|
assert.equal(diagnostiquer(dejaNulle).redondanceMinimale, 0);
|
||||||
|
assert.deepEqual(diagnostiquer(dejaNulle).remedes, { toursSansRedondance: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('diagnostic : une seule appartenance (§ 15.4)', () => {
|
||||||
|
const tables = [[10, 3], [20, 3], [30, 3], [40, 3]];
|
||||||
|
|
||||||
|
test('douze personnes d’une appartenance, séparation active : annoncée ; désactivée : non', () => {
|
||||||
|
const active = salle({ tables, appartenances: fois(12, 'Grisaille'), tours: 4, contraintes: SEPARER });
|
||||||
|
assert.equal(diagnostiquer(active).uneSeuleAppartenance, true);
|
||||||
|
const inactive = salle({ tables, appartenances: fois(12, 'Grisaille'), tours: 4, contraintes: TOUTES_SAUF_SEPARER });
|
||||||
|
assert.equal(diagnostiquer(inactive).uneSeuleAppartenance, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Les présents sans appartenance se séparent des trois membres : la
|
||||||
|
// séparation y garde un sens.
|
||||||
|
test('la même appartenance portée par 3 des 12 : faux', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables,
|
||||||
|
appartenances: [...fois(3, 'Grisaille'), ...fois(9, null)],
|
||||||
|
tours: 4,
|
||||||
|
contraintes: SEPARER,
|
||||||
|
});
|
||||||
|
assert.equal(diagnostiquer(configuration).uneSeuleAppartenance, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// L'appartenance du seul membre exclu ne compte pas : les présents n'en
|
||||||
|
// portent qu'une.
|
||||||
|
test('un exclu d’une autre appartenance ne compte pas', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables,
|
||||||
|
appartenances: [...fois(11, 'Grisaille'), 'Club des Merles'],
|
||||||
|
tours: 4,
|
||||||
|
contraintes: SEPARER,
|
||||||
|
exclus: [12],
|
||||||
|
});
|
||||||
|
assert.equal(diagnostiquer(configuration).uneSeuleAppartenance, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('diagnostic : collision à chaque table (§ 7.5)', () => {
|
||||||
|
// Douze personnes en deux appartenances de six, quatre tables de 3 : une
|
||||||
|
// table reçoit au moins 12 − 9 = 3 personnes, plus que G + U = 2.
|
||||||
|
const DEUX_FOIS_SIX = [...fois(6, 'Grisaille'), ...fois(6, 'Club des Merles')];
|
||||||
|
const QUATRE_DE_TROIS = [[10, 3], [20, 3], [30, 3], [40, 3]];
|
||||||
|
|
||||||
|
test('deux appartenances de six sur quatre tables de 3 : vrai ; désactivée, faux ; une cinquième table, faux', () => {
|
||||||
|
const active = salle({ tables: QUATRE_DE_TROIS, appartenances: DEUX_FOIS_SIX, tours: 4, contraintes: SEPARER });
|
||||||
|
assert.equal(diagnostiquer(active).collisionPartout, true);
|
||||||
|
const inactive = salle({
|
||||||
|
tables: QUATRE_DE_TROIS,
|
||||||
|
appartenances: DEUX_FOIS_SIX,
|
||||||
|
tours: 4,
|
||||||
|
contraintes: TOUTES_SAUF_SEPARER,
|
||||||
|
});
|
||||||
|
assert.equal(diagnostiquer(inactive).collisionPartout, false);
|
||||||
|
// Une table de 3 de plus : chaque table peut rester vide, 12 − 12 = 0.
|
||||||
|
const cinqTables = salle({
|
||||||
|
tables: [...QUATRE_DE_TROIS, [50, 3]],
|
||||||
|
appartenances: DEUX_FOIS_SIX,
|
||||||
|
tours: 4,
|
||||||
|
contraintes: SEPARER,
|
||||||
|
});
|
||||||
|
assert.equal(diagnostiquer(cinqTables).collisionPartout, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trois membres de chacune des trois appartenances par table de 3 : un
|
||||||
|
// plan sans collision existe, le plan parfait.
|
||||||
|
test('la petite : faux', () => {
|
||||||
|
assert.equal(diagnostiquer(construire('petite')).collisionPartout, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Un présent sans appartenance : 6 − 3 = 3 places au moins par table, et
|
||||||
|
// G + U = 3 personnes s'y assoient sans collision.
|
||||||
|
test('un présent sans appartenance peut rompre la preuve : faux', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[10, 3], [20, 3]],
|
||||||
|
appartenances: ['Grisaille', 'Grisaille', 'Grisaille', 'Club des Merles', 'Club des Merles', null],
|
||||||
|
tours: 2,
|
||||||
|
contraintes: SEPARER,
|
||||||
|
});
|
||||||
|
assert.equal(diagnostiquer(configuration).collisionPartout, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sept personnes pour six sièges : aucun plan n'assied chacun, rien ne se
|
||||||
|
// prouve. Sans table, rien non plus.
|
||||||
|
test('des places manquantes, ou aucune table : faux', () => {
|
||||||
|
const tropPetite = salle({
|
||||||
|
tables: [[10, 3], [20, 3]],
|
||||||
|
appartenances: [...fois(4, 'Grisaille'), ...fois(3, 'Club des Merles')],
|
||||||
|
tours: 2,
|
||||||
|
contraintes: SEPARER,
|
||||||
|
});
|
||||||
|
assert.equal(diagnostiquer(tropPetite).placesManquantes, 1);
|
||||||
|
assert.equal(diagnostiquer(tropPetite).collisionPartout, false);
|
||||||
|
const sansTable = salle({ tables: [], appartenances: [], tours: 2, contraintes: SEPARER });
|
||||||
|
assert.equal(diagnostiquer(sansTable).collisionPartout, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deux appartenances de trois, deux tables de 3, deux tours : 6 − 3 = 3
|
||||||
|
// > 2. L'énumération le confirme sans lire le diagnostic : chacun des
|
||||||
|
// C(6, 3)² = 400 plans porte une violation de séparation à chaque table de
|
||||||
|
// chaque tour.
|
||||||
|
test('deux appartenances de trois sur deux tables de 3 : vrai, et chacun des 400 plans le confirme', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[10, 3], [20, 3]],
|
||||||
|
appartenances: [...fois(3, 'Grisaille'), ...fois(3, 'Club des Merles')],
|
||||||
|
tours: 2,
|
||||||
|
contraintes: SEPARER,
|
||||||
|
});
|
||||||
|
assert.equal(diagnostiquer(configuration).collisionPartout, true);
|
||||||
|
const instance = normaliser(configuration);
|
||||||
|
const fautes = [];
|
||||||
|
const plans = enumererPlans(instance, (tableDe) => {
|
||||||
|
const cases = conflitsParTable(instance, tableDe).flat();
|
||||||
|
if (cases.length !== 4) fautes.push(`${cases.length} cases`);
|
||||||
|
for (const { table, regles } of cases) {
|
||||||
|
if (regles.every(({ regle }) => regle !== 'separerAppartenances')) {
|
||||||
|
fautes.push(`table ${table} sans collision : ${Array.from(tableDe)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(plans, 400);
|
||||||
|
assert.deepEqual(fautes, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('diagnostic : ancrage par table (§ 5.6)', () => {
|
||||||
|
test('trois ancrés pour trois sièges : gelée, non à moitié ; deux pour quatre : à moitié', () => {
|
||||||
|
const configuration = salle({
|
||||||
|
tables: [[10, 3], [20, 4], [30, 4]],
|
||||||
|
appartenances: fois(9, null),
|
||||||
|
tours: 2,
|
||||||
|
reservations: [ancrer(1, 10), ancrer(2, 10), ancrer(3, 10), ancrer(4, 20), ancrer(5, 20), ancrer(6, 30)],
|
||||||
|
});
|
||||||
|
assert.deepEqual(diagnostiquer(configuration).ancrage, [
|
||||||
|
{ table: 10, ancres: 3, capacite: 3, gelee: true, moitie: false },
|
||||||
|
{ table: 20, ancres: 2, capacite: 4, gelee: false, moitie: true },
|
||||||
|
{ table: 30, ancres: 1, capacite: 4, gelee: false, moitie: false },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@
|
||||||
// sans appartenance ne sont pas collègues. Un ensemble vide rend null, jamais
|
// 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
|
// 0 : le taux de diversité de qui ne rencontre aucun affilié, le minimum et la
|
||||||
// moyenne d'une population sans membre.
|
// moyenne d'une population sans membre.
|
||||||
|
//
|
||||||
|
// Une seule lecture du plan décide qu'une paire est réunie, collègue ou revue,
|
||||||
|
// et qu'une personne revient : parcourirTours. mesurer en tire chacun de ses
|
||||||
|
// chiffres, et conflitsParTable (conflits.js) le détail par table, si bien
|
||||||
|
// que la somme des violations par table égale le compte de la mesure
|
||||||
|
// (§ 13.2).
|
||||||
import {
|
import {
|
||||||
RESERVE,
|
RESERVE,
|
||||||
SANS_GROUPE,
|
SANS_GROUPE,
|
||||||
|
|
@ -41,10 +47,15 @@ import {
|
||||||
* @property {number[]} redondance r(p) = |F(p)| − A(p)
|
* @property {number[]} redondance r(p) = |F(p)| − A(p)
|
||||||
* @property {(number|null)[]} diversite d(p) = A(p) / |F(p)|, null si |F(p)| = 0
|
* @property {(number|null)[]} diversite d(p) = A(p) / |F(p)|, null si |F(p)| = 0
|
||||||
* @property {number[]} toursAssis
|
* @property {number[]} toursAssis
|
||||||
* @property {number[]} retoursChoisis par personne
|
* @property {number[]} retoursChoisis par personne : ses occurrences
|
||||||
* @property {number[]} retoursImposes par personne
|
* parmi les revenus de parcourirTours
|
||||||
|
* @property {number[]} retoursImposes par personne : ses retours moins
|
||||||
|
* ses retours choisis
|
||||||
* @property {TroisAgregats} aggRencontres
|
* @property {TroisAgregats} aggRencontres
|
||||||
* @property {TroisAgregats} aggRedondance
|
* @property {TroisAgregats} aggRedondance
|
||||||
|
* @property {TroisAgregats} aggAppartenancesVues A(p)
|
||||||
|
* @property {TroisAgregats} aggDiversite d(p), sur les seules personnes qui
|
||||||
|
* rencontrent un affilié
|
||||||
* @property {number} totalRedondance Σ_p r(p), le seul total de la
|
* @property {number} totalRedondance Σ_p r(p), le seul total de la
|
||||||
* redondance : la recherche et le
|
* redondance : la recherche et le
|
||||||
* classement le lisent
|
* classement le lisent
|
||||||
|
|
@ -53,7 +64,8 @@ import {
|
||||||
* @property {number} excedentCollisions cumulées − distinctes
|
* @property {number} excedentCollisions cumulées − distinctes
|
||||||
* @property {{choisies: number, imposees: number}} rencontresRepetees
|
* @property {{choisies: number, imposees: number}} rencontresRepetees
|
||||||
* paires réunies ≥ 2 fois ; « imposée » quand chaque rencontre de la
|
* paires réunies ≥ 2 fois ; « imposée » quand chaque rencontre de la
|
||||||
* paire a lieu à une table où les deux sont réservés ce tour-là
|
* paire a lieu à une table où les deux sont réservés ce tour-là ;
|
||||||
|
* choisies, les paires distinctes des revues de parcourirTours
|
||||||
* @property {number} maxRencontresPaire sur toutes les paires ; 0 si aucune
|
* @property {number} maxRencontresPaire sur toutes les paires ; 0 si aucune
|
||||||
* @property {number} totalRetoursChoisis
|
* @property {number} totalRetoursChoisis
|
||||||
* @property {number} totalRetoursImposes
|
* @property {number} totalRetoursImposes
|
||||||
|
|
@ -70,57 +82,122 @@ const UINT16_MAX = 0xffff;
|
||||||
// (N − 1) + (N − 2) + … + (N − a) paires des lignes qui la précèdent.
|
// (N − 1) + (N − 2) + … + (N − a) paires des lignes qui la précèdent.
|
||||||
const rangPaire = (N, a, b) => (a * (2 * N - a - 1)) / 2 + (b - a - 1);
|
const rangPaire = (N, a, b) => (a * (2 * N - a - 1)) / 2 + (b - a - 1);
|
||||||
|
|
||||||
// Rencontres de chaque paire, rangées par rangPaire, en parcourant chaque
|
/**
|
||||||
// table de chaque tour. choisie vaut 1 pour une paire dont une rencontre au
|
* Le plan lu tour par tour, puis table par table, par index croissants : le
|
||||||
// moins a lieu hors d'une table où les deux sont réservés ce tour-là. Une
|
* seul code qui décide qu'une paire est réunie, de même appartenance ou
|
||||||
// paire se réunit au plus une fois par tour : R borne chaque compte, qu'un
|
* revue, et qu'une personne revient. mesurer en tire chacun de ses chiffres,
|
||||||
// Uint16Array tient tant que R ≤ 65 535.
|
* conflitsParTable (conflits.js) le détail par table.
|
||||||
function compterRencontres({ N, T, R, fixe }, tableDe) {
|
*
|
||||||
const paires = (N * (N - 1)) / 2;
|
* parTour[r][t], en index de personnes, chaque paire [a, b] avec a < b,
|
||||||
const compte = R <= UINT16_MAX ? new Uint16Array(paires) : new Uint32Array(paires);
|
* chaque liste croissante — paires dans l'ordre de a, puis de b :
|
||||||
const choisie = new Uint8Array(paires);
|
* - collegues : paires de même appartenance assises à t au tour r ;
|
||||||
|
* - revues : paires assises à t au tour r et réunies à un tour antérieur,
|
||||||
|
* sauf quand chacune de leurs rencontres jusqu'à r a lieu à une table où
|
||||||
|
* les deux sont réservés ce tour-là ;
|
||||||
|
* - revenus : personnes assises à t au tour r et à un tour antérieur, sauf
|
||||||
|
* quand elles y sont réservées au tour r et lors d'une visite antérieure
|
||||||
|
* à t.
|
||||||
|
* parPaire : les rencontres de chaque paire, rangées par rangPaire ; une
|
||||||
|
* paire se réunit au plus une fois par tour, R borne chaque compte, qu'un
|
||||||
|
* Uint16Array tient tant que R ≤ 65 535. toursAssis, et retours — visites
|
||||||
|
* d'une table déjà visitée, imposées comprises —, par personne.
|
||||||
|
*
|
||||||
|
* Ne modifie ni l'instance ni le plan ; lève ce que lève exigerPlanIndexe.
|
||||||
|
*
|
||||||
|
* @param {import('./types.js').Instance} instance
|
||||||
|
* @param {ArrayLike<number>} tableDe
|
||||||
|
* @returns {{parTour: Array<Array<{collegues: number[][], revues: number[][], revenus: number[]}>>,
|
||||||
|
* parPaire: Uint16Array|Uint32Array, toursAssis: number[], retours: number[]}}
|
||||||
|
*/
|
||||||
|
export function parcourirTours(instance, tableDe) {
|
||||||
|
exigerPlanIndexe(instance, tableDe);
|
||||||
|
const { N, T, R, fixe, groupe } = instance;
|
||||||
|
|
||||||
|
// Personne par personne, ses tours dans l'ordre : visites[t] compte ses
|
||||||
|
// passages à la table t, reserveeAvant[t] note qu'un passage antérieur y
|
||||||
|
// était réservé. revenu[p × R + r] marque la visite répétée que la
|
||||||
|
// personne choisit. Le second parcours de ses tours remet à zéro les
|
||||||
|
// tables qu'elle a visitées : la personne suivante les trouve nulles.
|
||||||
|
const toursAssis = new Array(N).fill(0);
|
||||||
|
const retours = new Array(N).fill(0);
|
||||||
|
const revenu = new Uint8Array(N * R);
|
||||||
|
const visites = new Int32Array(T);
|
||||||
|
const reserveeAvant = new Uint8Array(T);
|
||||||
|
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) continue;
|
||||||
|
toursAssis[p] += 1;
|
||||||
|
const reservee = fixe[p * R + r] === t;
|
||||||
|
if (visites[t] > 0) {
|
||||||
|
retours[p] += 1;
|
||||||
|
if (!reservee || reserveeAvant[t] === 0) revenu[p * R + r] = 1;
|
||||||
|
}
|
||||||
|
visites[t] += 1;
|
||||||
|
if (reservee) reserveeAvant[t] = 1;
|
||||||
|
}
|
||||||
|
for (let r = 0; r < R; r += 1) {
|
||||||
|
const t = tableDe[p * R + r];
|
||||||
|
if (t === RESERVE) continue;
|
||||||
|
visites[t] = 0;
|
||||||
|
reserveeAvant[t] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tour par tour : occupants de chaque table, par index croissants, a < b
|
||||||
|
// pour toute paire (liste[i], liste[j]) avec i < j. La boucle intérieure
|
||||||
|
// part du voisin suivant : une liste vide ou d'un seul occupant ne forme
|
||||||
|
// aucune paire, et nul n'est apparié à lui-même. libre[k] note qu'une
|
||||||
|
// rencontre de la paire, jusqu'au tour courant compris, a eu lieu hors
|
||||||
|
// d'une table où les deux sont réservés.
|
||||||
|
const nombrePaires = (N * (N - 1)) / 2;
|
||||||
|
const parPaire = R <= UINT16_MAX ? new Uint16Array(nombrePaires) : new Uint32Array(nombrePaires);
|
||||||
|
const libre = new Uint8Array(nombrePaires);
|
||||||
|
const parTour = [];
|
||||||
for (let r = 0; r < R; r += 1) {
|
for (let r = 0; r < R; r += 1) {
|
||||||
// Occupants de chaque table, par index croissants : a < b pour toute
|
|
||||||
// paire (liste[i], liste[j]) avec i < j. La boucle intérieure part du
|
|
||||||
// voisin suivant : une liste vide ou d'un seul occupant ne forme aucune
|
|
||||||
// paire, et nul n'est apparié à lui-même.
|
|
||||||
const assis = Array.from({ length: T }, () => []);
|
const assis = Array.from({ length: T }, () => []);
|
||||||
for (let p = 0; p < N; p += 1) {
|
for (let p = 0; p < N; p += 1) {
|
||||||
const t = tableDe[p * R + r];
|
const t = tableDe[p * R + r];
|
||||||
if (t !== RESERVE) assis[t].push(p);
|
if (t !== RESERVE) assis[t].push(p);
|
||||||
}
|
}
|
||||||
|
const tables = [];
|
||||||
for (let t = 0; t < T; t += 1) {
|
for (let t = 0; t < T; t += 1) {
|
||||||
const liste = assis[t];
|
const liste = assis[t];
|
||||||
|
const collegues = [];
|
||||||
|
const revues = [];
|
||||||
|
const revenus = [];
|
||||||
for (let i = 0; i < liste.length; i += 1) {
|
for (let i = 0; i < liste.length; i += 1) {
|
||||||
const a = liste[i];
|
const a = liste[i];
|
||||||
|
if (revenu[a * R + r] === 1) revenus.push(a);
|
||||||
const aReserve = fixe[a * R + r] === t;
|
const aReserve = fixe[a * R + r] === t;
|
||||||
|
const g = groupe[a];
|
||||||
for (let j = i + 1; j < liste.length; j += 1) {
|
for (let j = i + 1; j < liste.length; j += 1) {
|
||||||
const b = liste[j];
|
const b = liste[j];
|
||||||
const k = rangPaire(N, a, b);
|
const k = rangPaire(N, a, b);
|
||||||
compte[k] += 1;
|
parPaire[k] += 1;
|
||||||
if (!aReserve || fixe[b * R + r] !== t) choisie[k] = 1;
|
if (!aReserve || fixe[b * R + r] !== t) libre[k] = 1;
|
||||||
|
if (g !== SANS_GROUPE && g === groupe[b]) collegues.push([a, b]);
|
||||||
|
if (parPaire[k] >= 2 && libre[k] === 1) revues.push([a, b]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tables.push({ collegues, revues, revenus });
|
||||||
}
|
}
|
||||||
|
parTour.push(tables);
|
||||||
}
|
}
|
||||||
return { compte, choisie };
|
return { parTour, parPaire, toursAssis, retours };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lecture du triangle des paires, dans l'ordre de rangPaire. Par personne :
|
// Lecture du triangle des paires, dans l'ordre de rangPaire. Par personne :
|
||||||
// |met(p)|, |F(p)| et A(p), vu[p * G + g] notant que p a déjà croisé un
|
// |met(p)|, |F(p)| et A(p), vu[p * G + g] notant que p a déjà croisé un
|
||||||
// affilié du groupe g. Par groupe : une paire de collègues réunie n fois
|
// affilié du groupe g. Sur toutes les paires : les paires réunies deux fois
|
||||||
// porte n collisions cumulées et une paire distincte. Sur toutes les paires :
|
// au moins, et le plus grand compte.
|
||||||
// les rencontres répétées, choisies ou imposées, et le plus grand compte.
|
function lirePaires({ N, groupe, groupes }, parPaire) {
|
||||||
function lirePaires({ N, groupe, groupes }, { compte, choisie }) {
|
|
||||||
const G = groupes.length;
|
const G = groupes.length;
|
||||||
const rencontres = new Int32Array(N);
|
const rencontres = new Int32Array(N);
|
||||||
const affilies = new Int32Array(N);
|
const affilies = new Int32Array(N);
|
||||||
const vues = new Int32Array(N);
|
const vues = new Int32Array(N);
|
||||||
const vu = new Uint8Array(N * G);
|
const vu = new Uint8Array(N * G);
|
||||||
const collisions = new Array(G).fill(0);
|
let repetees = 0;
|
||||||
const distinctes = new Array(G).fill(0);
|
|
||||||
let choisies = 0;
|
|
||||||
let imposees = 0;
|
|
||||||
let maxRencontresPaire = 0;
|
let maxRencontresPaire = 0;
|
||||||
|
|
||||||
// p rencontre q : q compte dans met(p) ; un affilié compte aussi dans F(p),
|
// p rencontre q : q compte dans met(p) ; un affilié compte aussi dans F(p),
|
||||||
|
|
@ -140,70 +217,52 @@ function lirePaires({ N, groupe, groupes }, { compte, choisie }) {
|
||||||
let k = 0;
|
let k = 0;
|
||||||
for (let a = 0; a < N; a += 1) {
|
for (let a = 0; a < N; a += 1) {
|
||||||
for (let b = a + 1; b < N; b += 1, k += 1) {
|
for (let b = a + 1; b < N; b += 1, k += 1) {
|
||||||
const n = compte[k];
|
const n = parPaire[k];
|
||||||
if (n === 0) continue;
|
if (n === 0) continue;
|
||||||
rencontrer(a, b);
|
rencontrer(a, b);
|
||||||
rencontrer(b, a);
|
rencontrer(b, a);
|
||||||
if (n > maxRencontresPaire) maxRencontresPaire = n;
|
if (n > maxRencontresPaire) maxRencontresPaire = n;
|
||||||
if (n >= 2) {
|
if (n >= 2) repetees += 1;
|
||||||
if (choisie[k] === 1) choisies += 1;
|
|
||||||
else imposees += 1;
|
|
||||||
}
|
|
||||||
const g = groupe[a];
|
|
||||||
if (g !== SANS_GROUPE && g === groupe[b]) {
|
|
||||||
collisions[g] += n;
|
|
||||||
distinctes[g] += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return { rencontres, affilies, vues, repetees, maxRencontresPaire };
|
||||||
rencontres,
|
|
||||||
affilies,
|
|
||||||
vues,
|
|
||||||
collisions,
|
|
||||||
distinctes,
|
|
||||||
rencontresRepetees: { choisies, imposees },
|
|
||||||
maxRencontresPaire,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tours assis et retours de chaque personne (§ 5.4). Pour une table visitée
|
// Lecture des listes de parcourirTours. Une paire de collègues réunie n fois
|
||||||
// m fois, dont f à un tour où la personne y est réservée : max(0, f − 1)
|
// porte n collisions cumulées et compte une fois parmi les paires
|
||||||
// retours imposés et (m − 1) − imposés retours choisis. visites et reservees
|
// distinctes de son groupe ; une paire revue compte une fois parmi les
|
||||||
// comptent par table les tours de la personne en cours ; le second parcours
|
// rencontres répétées choisies, à quelque tour qu'elle le soit ; chaque
|
||||||
// de ses tours solde chaque table à sa première occurrence et remet ses
|
// occurrence d'une personne revenue est un retour choisi.
|
||||||
// compteurs à zéro, si bien que la personne suivante les trouve nuls.
|
function lireTours({ N, groupe, groupes }, parTour) {
|
||||||
function compterRetours({ N, T, R, fixe }, tableDe) {
|
const nombrePaires = (N * (N - 1)) / 2;
|
||||||
const toursAssis = [];
|
const collisions = new Array(groupes.length).fill(0);
|
||||||
const retoursChoisis = [];
|
const distinctes = new Array(groupes.length).fill(0);
|
||||||
const retoursImposes = [];
|
const collegueVu = new Uint8Array(nombrePaires);
|
||||||
const visites = new Int32Array(T);
|
const revueVue = new Uint8Array(nombrePaires);
|
||||||
const reservees = new Int32Array(T);
|
const retoursChoisis = new Array(N).fill(0);
|
||||||
for (let p = 0; p < N; p += 1) {
|
let choisies = 0;
|
||||||
let assis = 0;
|
for (const tables of parTour) {
|
||||||
for (let r = 0; r < R; r += 1) {
|
for (const { collegues, revues, revenus } of tables) {
|
||||||
const t = tableDe[p * R + r];
|
for (const [a, b] of collegues) {
|
||||||
if (t === RESERVE) continue;
|
const g = groupe[a];
|
||||||
assis += 1;
|
const k = rangPaire(N, a, b);
|
||||||
visites[t] += 1;
|
collisions[g] += 1;
|
||||||
if (fixe[p * R + r] === t) reservees[t] += 1;
|
if (collegueVu[k] === 0) {
|
||||||
|
collegueVu[k] = 1;
|
||||||
|
distinctes[g] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [a, b] of revues) {
|
||||||
|
const k = rangPaire(N, a, b);
|
||||||
|
if (revueVue[k] === 0) {
|
||||||
|
revueVue[k] = 1;
|
||||||
|
choisies += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const p of revenus) retoursChoisis[p] += 1;
|
||||||
}
|
}
|
||||||
let choisis = 0;
|
|
||||||
let imposes = 0;
|
|
||||||
for (let r = 0; r < R; r += 1) {
|
|
||||||
const t = tableDe[p * R + r];
|
|
||||||
if (t === RESERVE || visites[t] === 0) continue;
|
|
||||||
const imposesTable = Math.max(0, reservees[t] - 1);
|
|
||||||
imposes += imposesTable;
|
|
||||||
choisis += visites[t] - 1 - imposesTable;
|
|
||||||
visites[t] = 0;
|
|
||||||
reservees[t] = 0;
|
|
||||||
}
|
|
||||||
toursAssis.push(assis);
|
|
||||||
retoursChoisis.push(choisis);
|
|
||||||
retoursImposes.push(imposes);
|
|
||||||
}
|
}
|
||||||
return { toursAssis, retoursChoisis, retoursImposes };
|
return { collisions, distinctes, choisies, retoursChoisis };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Population sans membre. min reste null tant qu'aucune valeur n'y entre :
|
// Population sans membre. min reste null tant qu'aucune valeur n'y entre :
|
||||||
|
|
@ -224,10 +283,14 @@ const conclure = ({ min, somme, effectif }) => ({
|
||||||
effectif,
|
effectif,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// La population, une valeur de plus quand elle en est une : null n'y entre
|
||||||
|
// pas, et la personne qui le porte sort de l'effectif.
|
||||||
|
const ajouterSiDefinie = (cumul, valeur) => (valeur === null ? cumul : ajouter(cumul, valeur));
|
||||||
|
|
||||||
// Minimum, moyenne et effectif de valeurs[p] sur les trois populations du
|
// Minimum, moyenne et effectif de valeurs[p] sur les trois populations du
|
||||||
// § 5.4, que replierParPopulation forme.
|
// § 5.4, que replierParPopulation forme ; replier y range chaque valeur.
|
||||||
function troisAgregats(valeurs, statut) {
|
function troisAgregats(valeurs, statut, replier = ajouter) {
|
||||||
const { tous, mobiles, ancres } = replierParPopulation(valeurs, statut, VIDE, ajouter);
|
const { tous, mobiles, ancres } = replierParPopulation(valeurs, statut, VIDE, replier);
|
||||||
return { tous: conclure(tous), mobiles: conclure(mobiles), ancres: conclure(ancres) };
|
return { tous: conclure(tous), mobiles: conclure(mobiles), ancres: conclure(ancres) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,16 +308,17 @@ const somme = (valeurs) => valeurs.reduce((total, valeur) => total + valeur, 0);
|
||||||
* @returns {Mesures}
|
* @returns {Mesures}
|
||||||
*/
|
*/
|
||||||
export function mesurer(instance, tableDe) {
|
export function mesurer(instance, tableDe) {
|
||||||
exigerPlanIndexe(instance, tableDe);
|
|
||||||
const { N, groupe, groupes, statut } = instance;
|
const { N, groupe, groupes, statut } = instance;
|
||||||
const paires = lirePaires(instance, compterRencontres(instance, tableDe));
|
const { parTour, parPaire, toursAssis, retours } = parcourirTours(instance, tableDe);
|
||||||
const { collisions, distinctes } = paires;
|
const paires = lirePaires(instance, parPaire);
|
||||||
|
const { collisions, distinctes, choisies, retoursChoisis } = lireTours(instance, parTour);
|
||||||
|
|
||||||
const rencontres = Array.from(paires.rencontres);
|
const rencontres = Array.from(paires.rencontres);
|
||||||
const affilies = Array.from(paires.affilies);
|
const affilies = Array.from(paires.affilies);
|
||||||
const appartenancesVues = Array.from(paires.vues);
|
const appartenancesVues = Array.from(paires.vues);
|
||||||
const redondance = affilies.map((f, p) => f - appartenancesVues[p]);
|
const redondance = affilies.map((f, p) => f - appartenancesVues[p]);
|
||||||
const diversite = affilies.map((f, p) => (f === 0 ? null : appartenancesVues[p] / f));
|
const diversite = affilies.map((f, p) => (f === 0 ? null : appartenancesVues[p] / f));
|
||||||
|
const retoursImposes = retours.map((total, p) => total - retoursChoisis[p]);
|
||||||
|
|
||||||
const effectifs = new Array(groupes.length).fill(0);
|
const effectifs = new Array(groupes.length).fill(0);
|
||||||
for (let p = 0; p < N; p += 1) {
|
for (let p = 0; p < N; p += 1) {
|
||||||
|
|
@ -270,8 +334,6 @@ export function mesurer(instance, tableDe) {
|
||||||
const collisionsCumulees = somme(collisions);
|
const collisionsCumulees = somme(collisions);
|
||||||
const pairesDistinctes = somme(distinctes);
|
const pairesDistinctes = somme(distinctes);
|
||||||
|
|
||||||
const { toursAssis, retoursChoisis, retoursImposes } = compterRetours(instance, tableDe);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rencontres,
|
rencontres,
|
||||||
affilies,
|
affilies,
|
||||||
|
|
@ -283,11 +345,13 @@ export function mesurer(instance, tableDe) {
|
||||||
retoursImposes,
|
retoursImposes,
|
||||||
aggRencontres: troisAgregats(rencontres, statut),
|
aggRencontres: troisAgregats(rencontres, statut),
|
||||||
aggRedondance: troisAgregats(redondance, statut),
|
aggRedondance: troisAgregats(redondance, statut),
|
||||||
|
aggAppartenancesVues: troisAgregats(appartenancesVues, statut),
|
||||||
|
aggDiversite: troisAgregats(diversite, statut, ajouterSiDefinie),
|
||||||
totalRedondance: somme(redondance),
|
totalRedondance: somme(redondance),
|
||||||
collisionsCumulees,
|
collisionsCumulees,
|
||||||
pairesDistinctes,
|
pairesDistinctes,
|
||||||
excedentCollisions: collisionsCumulees - pairesDistinctes,
|
excedentCollisions: collisionsCumulees - pairesDistinctes,
|
||||||
rencontresRepetees: paires.rencontresRepetees,
|
rencontresRepetees: { choisies, imposees: paires.repetees - choisies },
|
||||||
maxRencontresPaire: paires.maxRencontresPaire,
|
maxRencontresPaire: paires.maxRencontresPaire,
|
||||||
totalRetoursChoisis: somme(retoursChoisis),
|
totalRetoursChoisis: somme(retoursChoisis),
|
||||||
totalRetoursImposes: somme(retoursImposes),
|
totalRetoursImposes: somme(retoursImposes),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { describe, test } from '../../test/lanceur.js';
|
import { describe, test } from '../../test/lanceur.js';
|
||||||
import { indexerPlan, normaliser } from './configuration.js';
|
import { indexerPlan, normaliser } from './configuration.js';
|
||||||
import { mesurer } from './indicateurs.js';
|
import { mesurer, parcourirTours } from './indicateurs.js';
|
||||||
|
|
||||||
const SANS_CONTRAINTE = {
|
const SANS_CONTRAINTE = {
|
||||||
separerAppartenances: false,
|
separerAppartenances: false,
|
||||||
|
|
@ -135,6 +135,18 @@ describe('mesurer', () => {
|
||||||
mobiles: { min: 6, moyenne: 6, effectif: 12 },
|
mobiles: { min: 6, moyenne: 6, effectif: 12 },
|
||||||
ancres: AUCUN,
|
ancres: AUCUN,
|
||||||
},
|
},
|
||||||
|
// Chacun croise deux appartenances, la sienne exclue : A(p) = 2 pour
|
||||||
|
// huit affiliés rencontrés, d(p) = 2 / 8.
|
||||||
|
aggAppartenancesVues: {
|
||||||
|
tous: { min: 2, moyenne: 2, effectif: 12 },
|
||||||
|
mobiles: { min: 2, moyenne: 2, effectif: 12 },
|
||||||
|
ancres: AUCUN,
|
||||||
|
},
|
||||||
|
aggDiversite: {
|
||||||
|
tous: { min: 0.25, moyenne: 0.25, effectif: 12 },
|
||||||
|
mobiles: { min: 0.25, moyenne: 0.25, effectif: 12 },
|
||||||
|
ancres: AUCUN,
|
||||||
|
},
|
||||||
totalRedondance: 72,
|
totalRedondance: 72,
|
||||||
collisionsCumulees: 0,
|
collisionsCumulees: 0,
|
||||||
pairesDistinctes: 0,
|
pairesDistinctes: 0,
|
||||||
|
|
@ -268,6 +280,46 @@ describe('mesurer', () => {
|
||||||
mobiles: { min: 0, moyenne: 0, effectif: 3 },
|
mobiles: { min: 0, moyenne: 0, effectif: 3 },
|
||||||
ancres: AUCUN,
|
ancres: AUCUN,
|
||||||
});
|
});
|
||||||
|
// A(p) se replie sur chacun ; d(p) sur les seuls qui rencontrent un
|
||||||
|
// affilié : 3 sort de l'effectif, et son null n'entre ni dans le minimum
|
||||||
|
// ni dans la moyenne.
|
||||||
|
assert.deepEqual(mesures.aggAppartenancesVues, {
|
||||||
|
tous: { min: 0, moyenne: 2 / 3, effectif: 3 },
|
||||||
|
mobiles: { min: 0, moyenne: 2 / 3, effectif: 3 },
|
||||||
|
ancres: AUCUN,
|
||||||
|
});
|
||||||
|
assert.deepEqual(mesures.aggDiversite, {
|
||||||
|
tous: { min: 1, moyenne: 1, effectif: 2 },
|
||||||
|
mobiles: { min: 1, moyenne: 1, effectif: 2 },
|
||||||
|
ancres: AUCUN,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("le taux de diversité se replie par population : des mobiles qui ne croisent aucun affilié laissent leur ligne vide", () => {
|
||||||
|
// 1 et 2, sans appartenance, sont ancrés à la table 1 ; 3 et 4, de X et
|
||||||
|
// de Y, mobiles, la visitent tour à tour. 1 et 2 rencontrent chacun 3 et
|
||||||
|
// 4 : d = 2 / 2. 3 rencontre 1 et 2, sans appartenance, au tour 1, puis
|
||||||
|
// personne : il sort de l'effectif ; de même 4.
|
||||||
|
const mesures = mesurerPlan({
|
||||||
|
appartenance: [null, null, 'X', 'Y'],
|
||||||
|
capacite: 3,
|
||||||
|
tours: [
|
||||||
|
[[1, 2, 3], [4]],
|
||||||
|
[[1, 2, 4], [3]],
|
||||||
|
],
|
||||||
|
reservations: [tous(1, 1), tous(2, 1)],
|
||||||
|
});
|
||||||
|
assert.deepEqual(mesures.diversite, [1, 1, null, null]);
|
||||||
|
assert.deepEqual(mesures.aggDiversite, {
|
||||||
|
tous: { min: 1, moyenne: 1, effectif: 2 },
|
||||||
|
mobiles: AUCUN,
|
||||||
|
ancres: { min: 1, moyenne: 1, effectif: 2 },
|
||||||
|
});
|
||||||
|
assert.deepEqual(mesures.aggAppartenancesVues, {
|
||||||
|
tous: { min: 0, moyenne: 1, effectif: 4 },
|
||||||
|
mobiles: { min: 0, moyenne: 0, effectif: 2 },
|
||||||
|
ancres: { min: 2, moyenne: 2, effectif: 2 },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("la réserve n'est pas une table : seuls les tours assis comptent", () => {
|
test("la réserve n'est pas une table : seuls les tours assis comptent", () => {
|
||||||
|
|
@ -521,6 +573,8 @@ describe('mesurer', () => {
|
||||||
retoursImposes: [],
|
retoursImposes: [],
|
||||||
aggRencontres: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
aggRencontres: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
||||||
aggRedondance: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
aggRedondance: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
||||||
|
aggAppartenancesVues: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
||||||
|
aggDiversite: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
||||||
totalRedondance: 0,
|
totalRedondance: 0,
|
||||||
collisionsCumulees: 0,
|
collisionsCumulees: 0,
|
||||||
pairesDistinctes: 0,
|
pairesDistinctes: 0,
|
||||||
|
|
@ -577,3 +631,84 @@ describe('mesurer', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Quatre personnes, 1 et 2 de X, deux tables de 3, trois tours ; 4 attend en
|
||||||
|
// réserve au tour 3. Les paires se rangent par rangPaire : (0, 1), (0, 2),
|
||||||
|
// (0, 3), (1, 2), (1, 3), (2, 3), en index de personnes.
|
||||||
|
const PARCOURS = {
|
||||||
|
appartenance: ['X', 'X', null, null],
|
||||||
|
capacite: 3,
|
||||||
|
tours: [
|
||||||
|
[[1, 2, 3], [4]],
|
||||||
|
[[1, 2], [3, 4]],
|
||||||
|
[[1, 3], [2]],
|
||||||
|
],
|
||||||
|
reserves: [[], [], [4]],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('parcourirTours', () => {
|
||||||
|
test('chaque table de chaque tour : collègues, paires revues, personnes revenues, en index croissants', () => {
|
||||||
|
// Tour 1 : (0, 1) collègues, rien de revu, personne ne revient. Tour 2 :
|
||||||
|
// (0, 1) collègues et revus ; 0 et 1 reviennent à la table d'index 0, 3
|
||||||
|
// à celle d'index 1, que 2 découvre. Tour 3 : (0, 2) revus, réunis
|
||||||
|
// librement au tour 1 ; 0 revient une deuxième fois, 2 revient ; 1
|
||||||
|
// découvre la table d'index 1.
|
||||||
|
const { instance, tableDe } = preparer(PARCOURS);
|
||||||
|
const vide = { collegues: [], revues: [], revenus: [] };
|
||||||
|
assert.deepEqual(parcourirTours(instance, tableDe), {
|
||||||
|
parTour: [
|
||||||
|
[{ collegues: [[0, 1]], revues: [], revenus: [] }, vide],
|
||||||
|
[
|
||||||
|
{ collegues: [[0, 1]], revues: [[0, 1]], revenus: [0, 1] },
|
||||||
|
{ collegues: [], revues: [], revenus: [3] },
|
||||||
|
],
|
||||||
|
[{ collegues: [], revues: [[0, 2]], revenus: [0, 2] }, vide],
|
||||||
|
],
|
||||||
|
parPaire: Uint16Array.of(2, 2, 0, 1, 0, 1),
|
||||||
|
toursAssis: [3, 3, 3, 2],
|
||||||
|
retours: [2, 1, 1, 1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une rencontre et une visite que les réservations imposent ne se signalent pas ; elles restent comptées', () => {
|
||||||
|
// 1 et 2 sont réservés à la table 1 aux tours 1 et 2 : leur rencontre du
|
||||||
|
// tour 2 ne répète qu'une rencontre réservée, et chacun y revient
|
||||||
|
// réservé, comme à sa visite du tour 1. Au tour 3, 1 y revient sans
|
||||||
|
// réservation : il est revenu. parPaire et retours comptent tout,
|
||||||
|
// imposé compris.
|
||||||
|
const { instance, tableDe } = preparer({
|
||||||
|
...PARCOURS,
|
||||||
|
reservations: [auTour(1, 1, 1), auTour(2, 1, 1), auTour(1, 1, 2), auTour(2, 1, 2)],
|
||||||
|
});
|
||||||
|
const { parTour, parPaire, retours } = parcourirTours(instance, tableDe);
|
||||||
|
assert.deepEqual(parTour[1][0], { collegues: [[0, 1]], revues: [], revenus: [] });
|
||||||
|
assert.deepEqual(parTour[2][0], { collegues: [], revues: [[0, 2]], revenus: [0, 2] });
|
||||||
|
assert.deepEqual(Array.from(parPaire), [2, 2, 0, 1, 0, 1]);
|
||||||
|
assert.deepEqual(retours, [2, 1, 1, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('une table, ses tours, et aucune personne présente : chaque case est vide', () => {
|
||||||
|
const instance = normaliser({
|
||||||
|
participants: [{ id: 1, nom: 'P1', appartenance: 'X', exclu: true }],
|
||||||
|
tables: [{ id: 1, numero: 1, capacite: 2 }],
|
||||||
|
tours: 2,
|
||||||
|
reservations: [],
|
||||||
|
contraintes: SANS_CONTRAINTE,
|
||||||
|
});
|
||||||
|
const vide = { collegues: [], revues: [], revenus: [] };
|
||||||
|
assert.deepEqual(parcourirTours(instance, new Int32Array(0)), {
|
||||||
|
parTour: [[vide], [vide]],
|
||||||
|
parPaire: new Uint16Array(0),
|
||||||
|
toursAssis: [],
|
||||||
|
retours: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lève ce que lève exigerPlanIndexe, et ne modifie ni l'instance ni le plan", () => {
|
||||||
|
const { instance, tableDe } = preparer(PARCOURS);
|
||||||
|
assert.throws(() => parcourirTours(instance, tableDe.subarray(1)), RangeError);
|
||||||
|
const avant = { tableDe: tableDe.slice(), fixe: instance.fixe.slice(), groupe: instance.groupe.slice() };
|
||||||
|
parcourirTours(instance, tableDe);
|
||||||
|
assert.deepEqual({ tableDe, fixe: instance.fixe, groupe: instance.groupe }, avant);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue