[IMP] engine perf: facilitator tracking on demand, one normalisation
The default search ran 4.4 % slower than before tags and goals, and a cold evaluation of 200 proposals with goals up to 18.7 % slower. The search now tracks facilitator meetings only when the compiled order counts that criterion; an evaluation normalises its configuration once for both the table and the diagnostic, through a cache keyed on the frozen copy it builds; met pairs are read from the pairs that met. Results are byte-identical. Measured against the previous revision, alternating runs: search +1.3 %, evaluation with goals +4.3 to +9.1 %. Checked: node 4594, navigateur 963, node-long 124, fingerprints intact. --- FR --- [IMP] perf du moteur : animateurs suivis à la demande, une normalisation La recherche par défaut tournait 4,4 % plus lentement qu'avant étiquettes et objectifs, et une évaluation à froid de 200 propositions à objectifs jusqu'à 18,7 % plus lentement. La recherche ne suit les rencontres d'animateurs que si l'ordre compilé compte ce critère ; une évaluation normalise sa configuration une fois pour la table et le diagnostic, par un cache sur la copie figée qu'elle bâtit ; les paires se lisent parmi celles réunies. Résultats identiques à l'octet. Mesuré contre la révision précédente, en alternance : recherche +1,3 %, évaluation +4,3 à +9,1 %. Vérifié : node 4594, navigateur 963, longues 124, empreintes intactes. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
d17e2931a7
commit
0f8f4663b8
8 changed files with 287 additions and 80 deletions
|
|
@ -17,6 +17,10 @@
|
|||
// seul appel à fautes qui l'arrête. resumeDuRetenu passe par derive et
|
||||
// planDepuisFichier, qui la refont et lèvent sur une faute.
|
||||
//
|
||||
// La configuration de la charge est figée (figerConfiguration) : le
|
||||
// diagnostic et la mesure en lisent la même instance, normalisée une fois.
|
||||
// configurationDe la bâtit neuve, et figer ne touche pas la charge.
|
||||
//
|
||||
// Les imposés viennent du diagnostic, les mêmes pour toute ligne ; les
|
||||
// rencontres répétées imposées dépendent du plan, et chaque ligne les lit
|
||||
// dans ses mesures, à part des choisies.
|
||||
|
|
@ -35,7 +39,7 @@
|
|||
// aussi l'état de chaque critère tel que normaliser le donne — coché, sans
|
||||
// objet, ses valeurs —, que l'écran lit au lieu de le recalculer.
|
||||
import { classer } from '../moteur/classement.js';
|
||||
import { indexerPlan, normaliser } from '../moteur/configuration.js';
|
||||
import { figerConfiguration, indexerPlan, normaliser } from '../moteur/configuration.js';
|
||||
import { compilationDe } from '../moteur/criteres.js';
|
||||
import { diagnostiquer } from '../moteur/diagnostic.js';
|
||||
import { mesurer } from '../moteur/indicateurs.js';
|
||||
|
|
@ -132,7 +136,7 @@ export function evaluer(charge, { plafondsAPriori = 'calcul' } = {}) {
|
|||
if (plafondsAPriori !== 'calcul' && plafondsAPriori !== null) {
|
||||
throw new TypeError(`evaluer : plafondsAPriori 'calcul' ou null attendu, reçu ${JSON.stringify(plafondsAPriori)}`);
|
||||
}
|
||||
const configuration = configurationDe(charge);
|
||||
const configuration = figerConfiguration(configurationDe(charge));
|
||||
const diagnostic = diagnostiquer(configuration);
|
||||
const instance = normaliser(configuration);
|
||||
const aPriori = plafondsAPriori === 'calcul' ? diagnostic.plafondAPriori.parPersonne : null;
|
||||
|
|
|
|||
|
|
@ -202,6 +202,19 @@ describe("évaluation : la mesure qui fait foi (§ 5.10, § 13.2)", () => {
|
|||
const charge = figer(generee(chargeDemo('petite'), { graine: 2, nombre: 2, arret: 300 }));
|
||||
assert.deepEqual(evaluer(charge), evaluer(charge));
|
||||
});
|
||||
|
||||
test('evaluer fige sa configuration, jamais la charge : rien de ce que la charge atteint ne devient figé', () => {
|
||||
const charge = generee(chargeDemo('grande'), { graine: 2, nombre: 1, arret: 300 });
|
||||
evaluer(charge);
|
||||
const figes = [];
|
||||
const parcourir = (valeur, chemin) => {
|
||||
if (valeur === null || typeof valeur !== 'object') return;
|
||||
if (Object.isFrozen(valeur)) figes.push(chemin);
|
||||
for (const [cle, element] of Object.entries(valeur)) parcourir(element, `${chemin}.${cle}`);
|
||||
};
|
||||
parcourir(charge, 'charge');
|
||||
assert.deepEqual(figes, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('évaluation : une réservation que le plan ne tient pas (§ 5.8, § 12.6)', () => {
|
||||
|
|
|
|||
|
|
@ -379,12 +379,23 @@ function numeroterObjectifs(objectifs, { T, R, idsTables, indexTableDe }) {
|
|||
// (§ 4.5) : (p, r) l'est quand la table fixée de p à ce tour lui correspond,
|
||||
// ou, p libre à ce tour, quand une table qui garde un siège libre lui
|
||||
// correspond. Les objectifs qu'offre une table à siège libre se marquent une
|
||||
// fois par tour.
|
||||
// fois par tour. Deux personnes aux mêmes étiquettes, dans le même ordre, ont
|
||||
// la même rangée : correspondA ne se calcule qu'à la première, que les
|
||||
// suivantes recopient ; la clé, la liste en JSON, ne confond pas deux listes
|
||||
// différentes.
|
||||
function couplesPossibles(presents, distincts, { N, T, R, fixe, siegesLibres, objectifDe }) {
|
||||
const O = distincts.length;
|
||||
const correspond = new Uint8Array(N * O);
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const premiereAvec = new Map();
|
||||
for (let p = 0; O > 0 && p < N; p += 1) {
|
||||
const etiquettes = presents[p].etiquettes ?? [];
|
||||
const cle = JSON.stringify(etiquettes);
|
||||
const premiere = premiereAvec.get(cle);
|
||||
if (premiere !== undefined) {
|
||||
correspond.copyWithin(p * O, premiere * O, premiere * O + O);
|
||||
continue;
|
||||
}
|
||||
premiereAvec.set(cle, p);
|
||||
for (let o = 0; o < O; o += 1) correspond[p * O + o] = correspondA(etiquettes, distincts[o]) ? 1 : 0;
|
||||
}
|
||||
const possible = new Uint8Array(N * R);
|
||||
|
|
@ -481,28 +492,31 @@ function bornerAnimateurs({ N, T, R, fixe, siegesLibres }, { A, rangAnimateur, d
|
|||
}
|
||||
libresDuTour.push(rangs);
|
||||
}
|
||||
// marque[f] vaut p quand p a déjà rejoint l'animateur de rang f.
|
||||
const marque = new Int32Array(A).fill(-1);
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const soi = rangAnimateur[p];
|
||||
let rejoints = 0;
|
||||
let somme = 0;
|
||||
const rejoindre = (f) => {
|
||||
if (f === soi || marque[f] === p) return;
|
||||
marque[f] = p;
|
||||
rejoints += 1;
|
||||
};
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
const t = fixe[p * R + r];
|
||||
if (t === LIBRE) {
|
||||
libresDuTour[r].forEach(rejoindre);
|
||||
for (const f of libresDuTour[r]) {
|
||||
if (f === soi || marque[f] === p) continue;
|
||||
marque[f] = p;
|
||||
rejoints += 1;
|
||||
}
|
||||
somme += plusGrandLibre[r];
|
||||
continue;
|
||||
}
|
||||
let ici = 0;
|
||||
for (let k = debutAnimes[t * R + r]; k < debutAnimes[t * R + r + 1]; k += 1) {
|
||||
if (animes[k] === soi) continue;
|
||||
rejoindre(animes[k]);
|
||||
const f = animes[k];
|
||||
if (f === soi) continue;
|
||||
ici += 1;
|
||||
if (marque[f] === p) continue;
|
||||
marque[f] = p;
|
||||
rejoints += 1;
|
||||
}
|
||||
somme += ici;
|
||||
}
|
||||
|
|
@ -584,10 +598,78 @@ function etatDesCriteres(contraintes, mode, possibles, borne) {
|
|||
* deux cases facultatives restent absentes de instance.contraintes, où
|
||||
* estCoche (criteres.js) leur donne leur valeur par défaut.
|
||||
*
|
||||
* Une configuration passée par figerConfiguration ne se normalise qu'une
|
||||
* fois : les appels suivants rendent la même instance, que l'appelant lit
|
||||
* sans la modifier, comme tout le moteur.
|
||||
*
|
||||
* @param {import('./types.js').Configuration} configuration
|
||||
* @returns {import('./types.js').Instance}
|
||||
*/
|
||||
export function normaliser(configuration) {
|
||||
const connue = figees.get(configuration);
|
||||
if (connue) return connue;
|
||||
const instance = instanceDe(configuration);
|
||||
if (connue === null) figees.set(configuration, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Les configurations que figerConfiguration a figées, chacune avec son
|
||||
// instance, null tant qu'aucune normalisation n'a réussi : une
|
||||
// configuration refusée lève à chaque appel. La clé est l'objet même ; ce
|
||||
// que normaliser en lit, figé, ne change plus sous la même identité, et
|
||||
// l'instance tirée au premier appel vaut pour les suivants. Une
|
||||
// configuration que plus rien ne retient quitte la table avec son instance.
|
||||
const figees = new WeakMap();
|
||||
|
||||
// Fige liste, quand c'en est une, après chacun de ses éléments par
|
||||
// figerElement.
|
||||
function figerListe(liste, figerElement) {
|
||||
if (!Array.isArray(liste)) return;
|
||||
for (const element of liste) figerElement(element);
|
||||
Object.freeze(liste);
|
||||
}
|
||||
|
||||
// Fige valeur quand c'est un objet ; une autre valeur reste telle quelle.
|
||||
function figerObjet(valeur) {
|
||||
if (valeur !== null && typeof valeur === 'object') Object.freeze(valeur);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fige ce que normaliser lit de la configuration et la rend : l'objet, ses
|
||||
* participants avec leurs étiquettes et leurs tours animés, ses tables, ses
|
||||
* réservations, ses contraintes, ses objectifs avec leurs étiquettes, ses
|
||||
* priorités, palier par palier. normaliser ne la calcule alors plus qu'une
|
||||
* fois, et chaque appel suivant rend la même instance : un appelant qui la
|
||||
* passe à plusieurs fonctions du moteur — diagnostiquer, puis normaliser
|
||||
* pour mesurer — ne paie qu'une normalisation. Ce qu'elle atteint devient
|
||||
* immuable : elle ne doit rien partager avec une valeur que son
|
||||
* propriétaire modifie encore. Les champs sont nommés, et non parcourus :
|
||||
* l'instance ne dépend que d'eux.
|
||||
*
|
||||
* @param {import('./types.js').Configuration} configuration
|
||||
* @returns {import('./types.js').Configuration} la même, figée
|
||||
*/
|
||||
export function figerConfiguration(configuration) {
|
||||
figerListe(configuration.participants, (participant) => {
|
||||
figerObjet(participant);
|
||||
figerListe(participant?.etiquettes, figerObjet);
|
||||
figerListe(participant?.anime, figerObjet);
|
||||
});
|
||||
figerListe(configuration.tables, figerObjet);
|
||||
figerListe(configuration.reservations, figerObjet);
|
||||
figerObjet(configuration.contraintes);
|
||||
figerListe(configuration.objectifs, (objectif) => {
|
||||
figerObjet(objectif);
|
||||
figerListe(objectif?.etiquettes, figerObjet);
|
||||
});
|
||||
figerListe(configuration.priorites, (palier) => figerListe(palier, figerObjet));
|
||||
Object.freeze(configuration);
|
||||
if (!figees.has(configuration)) figees.set(configuration, null);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
// L'instance d'une configuration, calculée à neuf (normaliser).
|
||||
function instanceDe(configuration) {
|
||||
const { participants, tables, tours: R, reservations } = configuration;
|
||||
exigerListe(participants, 'participants');
|
||||
exigerListe(tables, 'tables');
|
||||
|
|
@ -861,9 +943,10 @@ export function nombrePlacesManquantes(instance) {
|
|||
* range une personne dans sa population : les agrégats des indicateurs et
|
||||
* les chiffres du manque le lisent tous deux.
|
||||
*
|
||||
* replier(cumul, valeur) rend le cumul suivant sans modifier celui qu'il
|
||||
* reçoit : les trois populations partent du même initial, et une population
|
||||
* sans membre le garde.
|
||||
* replier(cumul, valeur) rend le cumul suivant sans jamais modifier
|
||||
* initial : les trois populations partent de lui, et une population sans
|
||||
* membre le garde. Un cumul que replier a lui-même rendu n'appartient qu'à
|
||||
* sa population, et replier peut le compléter en place.
|
||||
*
|
||||
* @template C
|
||||
* @param {ArrayLike<*>} valeurs une par personne, dans l'ordre de instance.ids
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
SANS_OBJECTIF,
|
||||
STATUT,
|
||||
exigerPlanIndexe,
|
||||
figerConfiguration,
|
||||
indexerPlan,
|
||||
nombrePlaces,
|
||||
nombrePlacesManquantes,
|
||||
|
|
@ -851,6 +852,48 @@ describe('plans : forme par identifiants et forme indexée (§ 8.9)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('figerConfiguration : une normalisation par configuration figée', () => {
|
||||
// 2 anime aux deux tours, réservé à la table 10 ; 4 porte « finance », que
|
||||
// vise la table 30 pour la soirée.
|
||||
const recue = () => ({
|
||||
...configuration({
|
||||
participants: [
|
||||
...personnes([9]),
|
||||
{ id: 2, nom: 'P2', appartenance: null, anime: [1, 2] },
|
||||
{ id: 4, nom: 'P4', appartenance: 'X', etiquettes: ['finance'] },
|
||||
],
|
||||
tables: tablesDe([30, 10], 4),
|
||||
tours: 2,
|
||||
reservations: [tous(9, 30), tous(2, 10)],
|
||||
}),
|
||||
objectifs: [{ table: 30, tour: null, regle: 'ou', etiquettes: ['finance'] }],
|
||||
priorites: PRIORITES_PAR_DEFAUT.map((palier) => palier.map((entree) => ({ ...entree }))),
|
||||
});
|
||||
|
||||
test('fige ce que normaliser lit et rend la configuration ; normaliser rend alors la même instance à chaque appel, celle d’une copie non figée', () => {
|
||||
const figee = recue();
|
||||
assert.equal(figerConfiguration(figee), figee);
|
||||
const lues = [
|
||||
figee, figee.participants, figee.participants[0], figee.participants[1].anime, figee.participants[2].etiquettes,
|
||||
figee.tables, figee.tables[1], figee.reservations, figee.reservations[1], figee.contraintes,
|
||||
figee.objectifs, figee.objectifs[0], figee.objectifs[0].etiquettes, figee.priorites, figee.priorites[3], figee.priorites[3][0],
|
||||
];
|
||||
assert.deepEqual(lues.filter((valeur) => !Object.isFrozen(valeur)), []);
|
||||
const premiere = normaliser(figee);
|
||||
assert.equal(normaliser(figee), premiere);
|
||||
assert.deepEqual(premiere, normaliser(recue()));
|
||||
const libre = recue();
|
||||
assert.notEqual(normaliser(libre), normaliser(libre));
|
||||
});
|
||||
|
||||
test('une configuration figée que normaliser refuse lève à chaque appel', () => {
|
||||
const figee = figerConfiguration({ ...recue(), tours: 0 });
|
||||
for (let appel = 0; appel < 2; appel += 1) {
|
||||
assert.throws(() => normaliser(figee), (erreur) => erreur instanceof ErreurConfiguration && erreur.code === 'TOURS');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('replierParPopulation (§ 5.4)', () => {
|
||||
test('chacun compte dans tous, puis parmi les ancrés ou parmi les mobiles, partiellement fixés compris, dans l’ordre canonique', () => {
|
||||
const statut = Uint8Array.from([ANCRE, MOBILE, PARTIELLEMENT_FIXE, ANCRE, MOBILE, PARTIELLEMENT_FIXE]);
|
||||
|
|
|
|||
|
|
@ -129,7 +129,10 @@ const rangPaire = (N, a, b) => (a * (2 * N - a - 1)) / 2 + (b - a - 1);
|
|||
* à 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
|
||||
* Uint16Array tient tant que R ≤ 65 535. reunies : chaque paire réunie au
|
||||
* moins une fois, une seule fois, a puis b à plat, dans l'ordre de sa
|
||||
* première rencontre ; lirePaires n'en parcourt pas d'autre, au lieu du
|
||||
* triangle entier. toursAssis, et retours — visites
|
||||
* d'une table déjà visitée, imposées comprises —, par personne. servis,
|
||||
* s(p) : les tours où p est assis, à une table qu'il peut occuper, sous un
|
||||
* objectif en vigueur qui lui correspond. rencontresAnimateurs, m(p) : les
|
||||
|
|
@ -141,7 +144,7 @@ const rangPaire = (N, a, b) => (a * (2 * N - a - 1)) / 2 + (b - a - 1);
|
|||
* @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[],
|
||||
* parPaire: Uint16Array|Uint32Array, reunies: number[], toursAssis: number[], retours: number[],
|
||||
* servis: number[], rencontresAnimateurs: number[]}}
|
||||
*/
|
||||
export function parcourirTours(instance, tableDe) {
|
||||
|
|
@ -169,17 +172,19 @@ export function parcourirTours(instance, tableDe) {
|
|||
const reserveeAvant = new Uint8Array(T);
|
||||
const rencontre = new Int32Array(A).fill(-1);
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const soi = rangAnimateur[p];
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
const t = tableDe[p * R + r];
|
||||
if (t === RESERVE) continue;
|
||||
toursAssis[p] += 1;
|
||||
const imposee = fixe[p * R + r];
|
||||
if (imposee === LIBRE ? siegesLibres[t * R + r] > 0 : imposee === t) {
|
||||
const o = objectifDe[t * R + r];
|
||||
const ligne = t * R + r;
|
||||
if (imposee === LIBRE ? siegesLibres[ligne] > 0 : imposee === t) {
|
||||
const o = objectifDe[ligne];
|
||||
if (o !== SANS_OBJECTIF && correspond[p * O + o] === 1) servis[p] += 1;
|
||||
for (let k = debutAnimes[t * R + r]; k < debutAnimes[t * R + r + 1]; k += 1) {
|
||||
for (let k = debutAnimes[ligne], fin = debutAnimes[ligne + 1]; k < fin; k += 1) {
|
||||
const f = animes[k];
|
||||
if (f === rangAnimateur[p] || rencontre[f] === p || tableDe[animateurs[f] * R + r] !== t) continue;
|
||||
if (f === soi || rencontre[f] === p || tableDe[animateurs[f] * R + r] !== t) continue;
|
||||
rencontre[f] = p;
|
||||
rencontresAnimateurs[p] += 1;
|
||||
}
|
||||
|
|
@ -209,6 +214,7 @@ export function parcourirTours(instance, tableDe) {
|
|||
const nombrePaires = (N * (N - 1)) / 2;
|
||||
const parPaire = R <= UINT16_MAX ? new Uint16Array(nombrePaires) : new Uint32Array(nombrePaires);
|
||||
const libre = new Uint8Array(nombrePaires);
|
||||
const reunies = [];
|
||||
const parTour = [];
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
const assis = Array.from({ length: T }, () => []);
|
||||
|
|
@ -231,6 +237,7 @@ export function parcourirTours(instance, tableDe) {
|
|||
const b = liste[j];
|
||||
const k = rangPaire(N, a, b);
|
||||
parPaire[k] += 1;
|
||||
if (parPaire[k] === 1) reunies.push(a, b);
|
||||
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]);
|
||||
|
|
@ -240,14 +247,16 @@ export function parcourirTours(instance, tableDe) {
|
|||
}
|
||||
parTour.push(tables);
|
||||
}
|
||||
return { parTour, parPaire, toursAssis, retours, servis, rencontresAnimateurs };
|
||||
return { parTour, parPaire, reunies, toursAssis, retours, servis, rencontresAnimateurs };
|
||||
}
|
||||
|
||||
// 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
|
||||
// affilié du groupe g. Sur toutes les paires : les paires réunies deux fois
|
||||
// au moins, et le plus grand compte.
|
||||
function lirePaires({ N, groupe, groupes }, parPaire) {
|
||||
// Lecture des paires réunies (reunies de parcourirTours), leurs comptes dans
|
||||
// parPaire : une paire jamais réunie ne change aucun chiffre, et l'ordre de
|
||||
// lecture n'en change aucun non plus. Par personne : |met(p)|, |F(p)| et
|
||||
// A(p), vu[p * G + g] notant que p a déjà croisé un affilié du groupe g. Sur
|
||||
// toutes les paires : les paires réunies deux fois au moins, et le plus
|
||||
// grand compte. O(paires réunies), et non O(N²).
|
||||
function lirePaires({ N, groupe, groupes }, parPaire, reunies) {
|
||||
const G = groupes.length;
|
||||
const rencontres = new Int32Array(N);
|
||||
const affilies = new Int32Array(N);
|
||||
|
|
@ -270,16 +279,14 @@ function lirePaires({ N, groupe, groupes }, parPaire) {
|
|||
}
|
||||
};
|
||||
|
||||
let k = 0;
|
||||
for (let a = 0; a < N; a += 1) {
|
||||
for (let b = a + 1; b < N; b += 1, k += 1) {
|
||||
const n = parPaire[k];
|
||||
if (n === 0) continue;
|
||||
rencontrer(a, b);
|
||||
rencontrer(b, a);
|
||||
if (n > maxRencontresPaire) maxRencontresPaire = n;
|
||||
if (n >= 2) repetees += 1;
|
||||
}
|
||||
for (let i = 0; i < reunies.length; i += 2) {
|
||||
const a = reunies[i];
|
||||
const b = reunies[i + 1];
|
||||
const n = parPaire[rangPaire(N, a, b)];
|
||||
rencontrer(a, b);
|
||||
rencontrer(b, a);
|
||||
if (n > maxRencontresPaire) maxRencontresPaire = n;
|
||||
if (n >= 2) repetees += 1;
|
||||
}
|
||||
return { rencontres, affilies, vues, repetees, maxRencontresPaire };
|
||||
}
|
||||
|
|
@ -325,12 +332,16 @@ function lireTours({ N, groupe, groupes }, parTour) {
|
|||
// aucun Infinity ne sert de minimum initial.
|
||||
const VIDE = Object.freeze({ min: null, somme: 0, effectif: 0 });
|
||||
|
||||
// La population, une valeur de plus ; celle qu'elle reçoit reste intacte.
|
||||
const ajouter = ({ min, somme, effectif }, valeur) => ({
|
||||
min: min === null || valeur < min ? valeur : min,
|
||||
somme: somme + valeur,
|
||||
effectif: effectif + 1,
|
||||
});
|
||||
// La population, une valeur de plus. VIDE, que les trois populations
|
||||
// partagent au départ, reste intact : la première valeur ouvre un cumul
|
||||
// neuf, que les suivantes complètent en place, sans un objet par personne.
|
||||
function ajouter(cumul, valeur) {
|
||||
if (cumul === VIDE) return { min: valeur, somme: valeur, effectif: 1 };
|
||||
if (valeur < cumul.min) cumul.min = valeur;
|
||||
cumul.somme += valeur;
|
||||
cumul.effectif += 1;
|
||||
return cumul;
|
||||
}
|
||||
|
||||
// Agrégat d'une population ; la moyenne d'une population vide vaut null.
|
||||
const conclure = ({ min, somme, effectif }) => ({
|
||||
|
|
@ -355,55 +366,71 @@ const somme = (valeurs) => valeurs.reduce((total, valeur) => total + valeur, 0);
|
|||
// Les objectifs des tables mesurés (A9), sur les personnes concernées, celles
|
||||
// dont P(p) > 0 : null sans aucune. Pour chacune, P(p) − s(p) entre dans H ;
|
||||
// s(p) nul, elle est jamais servie ; E et le nombre de personnes à E se
|
||||
// tiennent en un passage, ce nombre ramené à 0 quand E l'est.
|
||||
function lireObjectifs({ N, possibles, statut }, servis) {
|
||||
const tours = [...servis];
|
||||
const parPersonne = Array.from(possibles);
|
||||
// tiennent en un passage, ce nombre ramené à 0 quand E l'est. servis est le
|
||||
// tableau neuf de parcourirTours, que la mesure rend tel quel sous tours.
|
||||
function lireObjectifs({ N, possibles: P, statut }, servis) {
|
||||
let concernes = 0;
|
||||
for (let p = 0; p < N; p += 1) if (P[p] > 0) concernes += 1;
|
||||
if (concernes === 0) return null;
|
||||
const possibles = new Array(N);
|
||||
// s(p) des concernés, null pour les autres : l'agrégat n'en compte aucun.
|
||||
const selonObjectif = new Array(N);
|
||||
let horsObjectif = 0;
|
||||
let jamaisServies = 0;
|
||||
let horsObjectifMax = 0;
|
||||
let auMax = 0;
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
if (parPersonne[p] === 0) continue;
|
||||
concernes += 1;
|
||||
const hors = parPersonne[p] - tours[p];
|
||||
possibles[p] = P[p];
|
||||
if (P[p] === 0) {
|
||||
selonObjectif[p] = null;
|
||||
continue;
|
||||
}
|
||||
selonObjectif[p] = servis[p];
|
||||
const hors = P[p] - servis[p];
|
||||
horsObjectif += hors;
|
||||
if (tours[p] === 0) jamaisServies += 1;
|
||||
if (servis[p] === 0) jamaisServies += 1;
|
||||
if (hors > horsObjectifMax) {
|
||||
horsObjectifMax = hors;
|
||||
auMax = 0;
|
||||
}
|
||||
if (hors === horsObjectifMax) auMax += 1;
|
||||
}
|
||||
if (concernes === 0) return null;
|
||||
return {
|
||||
tours,
|
||||
possibles: parPersonne,
|
||||
tours: servis,
|
||||
possibles,
|
||||
horsObjectif,
|
||||
jamaisServies,
|
||||
horsObjectifMax,
|
||||
personnesAuMax: horsObjectifMax === 0 ? 0 : auMax,
|
||||
concernes,
|
||||
agregat: troisAgregats(tours.map((s, p) => (parPersonne[p] > 0 ? s : null)), statut, ajouterSiDefinie),
|
||||
agregat: troisAgregats(selonObjectif, statut, ajouterSiDefinie),
|
||||
};
|
||||
}
|
||||
|
||||
// Les animateurs rencontrés mesurés (A9), sur les personnes concernées,
|
||||
// celles dont B(p) > 0 : null sans aucune. Les animateurs non rencontrés
|
||||
// somment B(p) − m(p) sur toutes les personnes, une non concernée y valant
|
||||
// zéro.
|
||||
function lireAnimateurs({ borneAnimateurs, statut }, rencontresAnimateurs) {
|
||||
const rencontres = [...rencontresAnimateurs];
|
||||
const bornes = Array.from(borneAnimateurs);
|
||||
const concernes = bornes.filter((borne) => borne > 0).length;
|
||||
// zéro. rencontres est le tableau neuf de parcourirTours, rendu tel quel.
|
||||
function lireAnimateurs({ N, borneAnimateurs, statut }, rencontres) {
|
||||
let concernes = 0;
|
||||
for (let p = 0; p < N; p += 1) if (borneAnimateurs[p] > 0) concernes += 1;
|
||||
if (concernes === 0) return null;
|
||||
const bornes = new Array(N);
|
||||
// m(p) des concernés, null pour les autres : l'agrégat n'en compte aucun.
|
||||
const selonBorne = new Array(N);
|
||||
let nonRencontres = 0;
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const borne = borneAnimateurs[p];
|
||||
bornes[p] = borne;
|
||||
nonRencontres += borne - rencontres[p];
|
||||
selonBorne[p] = borne > 0 ? rencontres[p] : null;
|
||||
}
|
||||
return {
|
||||
rencontres,
|
||||
bornes,
|
||||
nonRencontres: somme(bornes) - somme(rencontres),
|
||||
nonRencontres,
|
||||
concernes,
|
||||
agregat: troisAgregats(rencontres.map((m, p) => (bornes[p] > 0 ? m : null)), statut, ajouterSiDefinie),
|
||||
agregat: troisAgregats(selonBorne, statut, ajouterSiDefinie),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -420,8 +447,8 @@ function lireAnimateurs({ borneAnimateurs, statut }, rencontresAnimateurs) {
|
|||
*/
|
||||
export function mesurer(instance, tableDe) {
|
||||
const { N, groupe, groupes, statut } = instance;
|
||||
const { parTour, parPaire, toursAssis, retours, servis, rencontresAnimateurs } = parcourirTours(instance, tableDe);
|
||||
const paires = lirePaires(instance, parPaire);
|
||||
const { parTour, parPaire, reunies, toursAssis, retours, servis, rencontresAnimateurs } = parcourirTours(instance, tableDe);
|
||||
const paires = lirePaires(instance, parPaire, reunies);
|
||||
const { collisions, distinctes, choisies, retoursChoisis } = lireTours(instance, parTour);
|
||||
|
||||
const rencontres = Array.from(paires.rencontres);
|
||||
|
|
|
|||
|
|
@ -714,7 +714,8 @@ describe('parcourirTours', () => {
|
|||
// (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.
|
||||
// découvre la table d'index 1. Réunies une première fois : (0, 1),
|
||||
// (0, 2) et (1, 2) au tour 1, (2, 3) au tour 2.
|
||||
const { instance, tableDe } = preparer(PARCOURS);
|
||||
const vide = { collegues: [], revues: [], revenus: [] };
|
||||
assert.deepEqual(parcourirTours(instance, tableDe), {
|
||||
|
|
@ -727,6 +728,7 @@ describe('parcourirTours', () => {
|
|||
[{ collegues: [], revues: [[0, 2]], revenus: [0, 2] }, vide],
|
||||
],
|
||||
parPaire: Uint16Array.of(2, 2, 0, 1, 0, 1),
|
||||
reunies: [0, 1, 0, 2, 1, 2, 2, 3],
|
||||
toursAssis: [3, 3, 3, 2],
|
||||
retours: [2, 1, 1, 1],
|
||||
servis: [0, 0, 0, 0],
|
||||
|
|
@ -763,6 +765,7 @@ describe('parcourirTours', () => {
|
|||
assert.deepEqual(parcourirTours(instance, new Int32Array(0)), {
|
||||
parTour: [[vide], [vide]],
|
||||
parPaire: new Uint16Array(0),
|
||||
reunies: [],
|
||||
toursAssis: [],
|
||||
retours: [],
|
||||
servis: [],
|
||||
|
|
|
|||
|
|
@ -79,7 +79,10 @@
|
|||
// rencontre apparaît ou disparaît, et m(p), avec les animateurs non
|
||||
// rencontrés Σ (B(p) − m(p)), suit. Partager la table d'un animateur à un
|
||||
// tour où il n'anime pas n'y compte pas : modifierPaire n'y touche pas.
|
||||
// O(animateurs des deux tables), au plus O(capacité).
|
||||
// O(animateurs des deux tables), au plus O(capacité). L'état ne les tient
|
||||
// que lorsque le n-uplet les compose, le critère coché et non sans objet :
|
||||
// sinon aucune composante ne les lit, et la recherche ne paie rien pour
|
||||
// eux, même quand des personnes animent.
|
||||
//
|
||||
// Ce score sert la boucle de recherche et rien d'autre ; scoreComplet
|
||||
// recompose le même n-uplet à partir de la mesure qui fait foi, dont il lit
|
||||
|
|
@ -169,11 +172,15 @@ import { plafondsAPriori } from './plafond.js';
|
|||
* @property {number} jamaisServies J
|
||||
* @property {number} horsObjectifMax E
|
||||
* @property {number} personnesAuMax concernés à E ; 0 quand E = 0
|
||||
* @property {Int32Array} rencontresAnimateurs m(p)
|
||||
* @property {number} animateursNonRencontres Σ (B(p) − m(p))
|
||||
* @property {boolean} suitAnimateurs vrai quand le n-uplet compose les
|
||||
* animateurs non rencontrés : l'état ne les tient qu'alors
|
||||
* @property {Int32Array|null} rencontresAnimateurs m(p) ; null quand
|
||||
* le n-uplet ne compose pas les animateurs non rencontrés
|
||||
* @property {number|null} animateursNonRencontres Σ (B(p) − m(p)) ; null
|
||||
* de même
|
||||
* @property {Object} composition la forme du n-uplet de l'instance :
|
||||
* taille, termes de chaque composante, ordres de comparaison et
|
||||
* rang du plus grand écart
|
||||
* taille, termes de chaque composante, ordres de comparaison,
|
||||
* rang du plus grand écart et suitAnimateurs
|
||||
*
|
||||
* @typedef {{tour: number, a: number, b: number}} Mouvement
|
||||
* l'échange des occupants des sièges a et b au tour d'index tour
|
||||
|
|
@ -253,7 +260,9 @@ function lire(q, code) {
|
|||
// terme par composante qu'il descend ; un palier partagé, un terme par
|
||||
// critère dans sa composante. Les rencontres répétées posent un terme par
|
||||
// case cochée de « nouveaux voisins » et « nouvelle table », au même poids.
|
||||
// Lève RangeError pour une grandeur que la composition ne sait pas lire.
|
||||
// suitAnimateurs dit qu'un terme lit les animateurs non rencontrés : l'état
|
||||
// ne les tient qu'alors. Lève RangeError pour une grandeur que la
|
||||
// composition ne sait pas lire.
|
||||
function composition(instance) {
|
||||
const { paliers, ordreScore, ordreContraintes, rangEcart, taille } = compilationDe(instance);
|
||||
const { contraintes, modeObjectifs } = instance;
|
||||
|
|
@ -287,6 +296,7 @@ function composition(instance) {
|
|||
ordreScore: Int32Array.from(ordreScore),
|
||||
ordreContraintes: Int32Array.from(ordreContraintes),
|
||||
rangEcart,
|
||||
suitAnimateurs: termes.some(([code]) => code === ANIMATEURS),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -517,13 +527,14 @@ function changerAnimateurs(etat, p, t, r, sens) {
|
|||
}
|
||||
|
||||
// p, libre au tour r, passe de la table depart à la table arrivee : ses tours
|
||||
// servis et ses rencontres d'animateurs suivent (en-tête du module).
|
||||
// servis et, quand l'état les tient, ses rencontres d'animateurs suivent
|
||||
// (en-tête du module).
|
||||
function changerTable(etat, p, depart, arrivee, r) {
|
||||
if (etat.O > 0) {
|
||||
const ecart = sert(etat, p, arrivee, r) - sert(etat, p, depart, r);
|
||||
if (ecart !== 0) changerServis(etat, p, ecart);
|
||||
}
|
||||
if (etat.A > 0) {
|
||||
if (etat.suitAnimateurs) {
|
||||
changerAnimateurs(etat, p, depart, r, -1);
|
||||
changerAnimateurs(etat, p, arrivee, r, 1);
|
||||
}
|
||||
|
|
@ -586,7 +597,8 @@ function echanger(etat, r, a, b) {
|
|||
// sièges mobiles. Les compteurs partent de zéro et suivent chaque paire,
|
||||
// chaque visite et chaque rencontre d'animateur par les mêmes fonctions que
|
||||
// les mouvements ; les écarts partent du plafond a priori, aucune rencontre
|
||||
// n'étant encore comptée, et les animateurs non rencontrés de Σ B(p). Les
|
||||
// n'étant encore comptée, et les animateurs non rencontrés de Σ B(p),
|
||||
// quand l'état les tient (suitAnimateurs de la composition). Les
|
||||
// tours servis se comptent siège par siège, puis l'histogramme des
|
||||
// P(p) − s(p) se forme sur les personnes concernées. Chaque personne assise
|
||||
// l'est à une table qu'elle peut occuper — sa table fixée, ou un siège
|
||||
|
|
@ -595,6 +607,7 @@ function echanger(etat, r, a, b) {
|
|||
function etatInitial(structure, rng) {
|
||||
const { instance, N, T, R, S, mobiles, libres, aPriori, tableDuSiege, debut } = structure;
|
||||
const G = instance.groupes.length;
|
||||
const { suitAnimateurs } = structure.composition;
|
||||
const etat = {
|
||||
instance,
|
||||
contraintes: instance.contraintes,
|
||||
|
|
@ -646,20 +659,23 @@ function etatInitial(structure, rng) {
|
|||
personnesAuMax: 0,
|
||||
// Les animateurs : qui anime à chaque table et chaque tour, de
|
||||
// l'instance ; le compte de chaque couple (personne, animateur), m(p) et
|
||||
// Σ (B(p) − m(p)), de cet état.
|
||||
// Σ (B(p) − m(p)), de cet état, null quand il ne les tient pas.
|
||||
suitAnimateurs,
|
||||
A: instance.A,
|
||||
rangAnimateur: instance.rangAnimateur,
|
||||
debutAnimes: instance.debutAnimes,
|
||||
animes: instance.animes,
|
||||
rencontresAnimees: R <= UINT8_MAX ? new Uint8Array(N * instance.A) : new Uint32Array(N * instance.A),
|
||||
rencontresAnimateurs: new Int32Array(N),
|
||||
animateursNonRencontres: 0,
|
||||
rencontresAnimees: !suitAnimateurs
|
||||
? null
|
||||
: R <= UINT8_MAX ? new Uint8Array(N * instance.A) : new Uint32Array(N * instance.A),
|
||||
rencontresAnimateurs: suitAnimateurs ? new Int32Array(N) : null,
|
||||
animateursNonRencontres: suitAnimateurs ? 0 : null,
|
||||
};
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
etat.histogramme[aPriori[p]] += 1;
|
||||
etat.carresEcartsAuPlafondAPriori += aPriori[p] * aPriori[p];
|
||||
etat.ecartAuPlafondAPrioriMax = Math.max(etat.ecartAuPlafondAPrioriMax, aPriori[p]);
|
||||
etat.animateursNonRencontres += instance.borneAnimateurs[p];
|
||||
if (suitAnimateurs) etat.animateursNonRencontres += instance.borneAnimateurs[p];
|
||||
}
|
||||
const { siege, tableDe } = etat;
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
|
|
@ -679,7 +695,7 @@ function etatInitial(structure, rng) {
|
|||
if (q !== FANTOME) modifierPaire(etat, p, q, 1);
|
||||
}
|
||||
if (etat.O > 0) etat.servis[p] += sert(etat, p, t, r);
|
||||
if (etat.A > 0) changerAnimateurs(etat, p, t, r, 1);
|
||||
if (suitAnimateurs) changerAnimateurs(etat, p, t, r, 1);
|
||||
}
|
||||
}
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
|
|
|
|||
|
|
@ -1347,6 +1347,24 @@ describe('score : objectifs et animateurs, sous chaque mode et sous un ordre dé
|
|||
const directs = animateursRencontres(configuration, instance, etat.tableDe);
|
||||
assert.deepEqual(Array.from(etat.rencontresAnimateurs), directs);
|
||||
});
|
||||
|
||||
test("case des animateurs décochée : l'état ne tient pas leurs rencontres, le score reste celui de la mesure, et la recherche rend les plans d'une soirée sans animateur", () => {
|
||||
const cochee = soireeAObjectifs();
|
||||
const decochee = { ...cochee, contraintes: { ...cochee.contraintes, rencontrerAnimateurs: false } };
|
||||
const sansAnimateur = { ...decochee, participants: decochee.participants.map(({ anime, ...reste }) => reste) };
|
||||
assert.ok(normaliser(decochee).borneAnimateurs.some((B) => B > 0));
|
||||
assert.equal(creerEtat(normaliser(cochee), creerPcg32(5, FLUX.RECHERCHE)).suitAnimateurs, true);
|
||||
const rng = creerPcg32(5, FLUX.RECHERCHE);
|
||||
const etat = creerEtat(normaliser(decochee), rng);
|
||||
assert.deepEqual(
|
||||
[etat.suitAnimateurs, etat.rencontresAnimees, etat.rencontresAnimateurs, etat.animateursNonRencontres],
|
||||
[false, null, null, null],
|
||||
);
|
||||
for (let essai = 0; essai < 300; essai += 1) proposerEtAppliquer(etat, rng);
|
||||
assert.deepStrictEqual(scoreIncremental(etat), scoreComplet(etat));
|
||||
const reglages = { graine: 9, arret: 3_000, nombre: 2 };
|
||||
assert.deepEqual(rechercher(decochee, reglages), rechercher(sansAnimateur, reglages));
|
||||
});
|
||||
});
|
||||
|
||||
describe("recherche : le mode et l'ordre réglé ne touchent pas au hasard (A13, § 15.5)", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue