[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
|
// seul appel à fautes qui l'arrête. resumeDuRetenu passe par derive et
|
||||||
// planDepuisFichier, qui la refont et lèvent sur une faute.
|
// 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
|
// 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
|
// rencontres répétées imposées dépendent du plan, et chaque ligne les lit
|
||||||
// dans ses mesures, à part des choisies.
|
// dans ses mesures, à part des choisies.
|
||||||
|
|
@ -35,7 +39,7 @@
|
||||||
// aussi l'état de chaque critère tel que normaliser le donne — coché, sans
|
// 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.
|
// objet, ses valeurs —, que l'écran lit au lieu de le recalculer.
|
||||||
import { classer } from '../moteur/classement.js';
|
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 { compilationDe } from '../moteur/criteres.js';
|
||||||
import { diagnostiquer } from '../moteur/diagnostic.js';
|
import { diagnostiquer } from '../moteur/diagnostic.js';
|
||||||
import { mesurer } from '../moteur/indicateurs.js';
|
import { mesurer } from '../moteur/indicateurs.js';
|
||||||
|
|
@ -132,7 +136,7 @@ export function evaluer(charge, { plafondsAPriori = 'calcul' } = {}) {
|
||||||
if (plafondsAPriori !== 'calcul' && plafondsAPriori !== null) {
|
if (plafondsAPriori !== 'calcul' && plafondsAPriori !== null) {
|
||||||
throw new TypeError(`evaluer : plafondsAPriori 'calcul' ou null attendu, reçu ${JSON.stringify(plafondsAPriori)}`);
|
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 diagnostic = diagnostiquer(configuration);
|
||||||
const instance = normaliser(configuration);
|
const instance = normaliser(configuration);
|
||||||
const aPriori = plafondsAPriori === 'calcul' ? diagnostic.plafondAPriori.parPersonne : null;
|
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 }));
|
const charge = figer(generee(chargeDemo('petite'), { graine: 2, nombre: 2, arret: 300 }));
|
||||||
assert.deepEqual(evaluer(charge), evaluer(charge));
|
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)', () => {
|
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,
|
// (§ 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
|
// 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
|
// 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 }) {
|
function couplesPossibles(presents, distincts, { N, T, R, fixe, siegesLibres, objectifDe }) {
|
||||||
const O = distincts.length;
|
const O = distincts.length;
|
||||||
const correspond = new Uint8Array(N * O);
|
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 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;
|
for (let o = 0; o < O; o += 1) correspond[p * O + o] = correspondA(etiquettes, distincts[o]) ? 1 : 0;
|
||||||
}
|
}
|
||||||
const possible = new Uint8Array(N * R);
|
const possible = new Uint8Array(N * R);
|
||||||
|
|
@ -481,28 +492,31 @@ function bornerAnimateurs({ N, T, R, fixe, siegesLibres }, { A, rangAnimateur, d
|
||||||
}
|
}
|
||||||
libresDuTour.push(rangs);
|
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);
|
const marque = new Int32Array(A).fill(-1);
|
||||||
for (let p = 0; p < N; p += 1) {
|
for (let p = 0; p < N; p += 1) {
|
||||||
const soi = rangAnimateur[p];
|
const soi = rangAnimateur[p];
|
||||||
let rejoints = 0;
|
let rejoints = 0;
|
||||||
let somme = 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) {
|
for (let r = 0; r < R; r += 1) {
|
||||||
const t = fixe[p * R + r];
|
const t = fixe[p * R + r];
|
||||||
if (t === LIBRE) {
|
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];
|
somme += plusGrandLibre[r];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let ici = 0;
|
let ici = 0;
|
||||||
for (let k = debutAnimes[t * R + r]; k < debutAnimes[t * R + r + 1]; k += 1) {
|
for (let k = debutAnimes[t * R + r]; k < debutAnimes[t * R + r + 1]; k += 1) {
|
||||||
if (animes[k] === soi) continue;
|
const f = animes[k];
|
||||||
rejoindre(animes[k]);
|
if (f === soi) continue;
|
||||||
ici += 1;
|
ici += 1;
|
||||||
|
if (marque[f] === p) continue;
|
||||||
|
marque[f] = p;
|
||||||
|
rejoints += 1;
|
||||||
}
|
}
|
||||||
somme += ici;
|
somme += ici;
|
||||||
}
|
}
|
||||||
|
|
@ -584,10 +598,78 @@ function etatDesCriteres(contraintes, mode, possibles, borne) {
|
||||||
* deux cases facultatives restent absentes de instance.contraintes, où
|
* deux cases facultatives restent absentes de instance.contraintes, où
|
||||||
* estCoche (criteres.js) leur donne leur valeur par défaut.
|
* 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
|
* @param {import('./types.js').Configuration} configuration
|
||||||
* @returns {import('./types.js').Instance}
|
* @returns {import('./types.js').Instance}
|
||||||
*/
|
*/
|
||||||
export function normaliser(configuration) {
|
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;
|
const { participants, tables, tours: R, reservations } = configuration;
|
||||||
exigerListe(participants, 'participants');
|
exigerListe(participants, 'participants');
|
||||||
exigerListe(tables, 'tables');
|
exigerListe(tables, 'tables');
|
||||||
|
|
@ -861,9 +943,10 @@ export function nombrePlacesManquantes(instance) {
|
||||||
* range une personne dans sa population : les agrégats des indicateurs et
|
* range une personne dans sa population : les agrégats des indicateurs et
|
||||||
* les chiffres du manque le lisent tous deux.
|
* les chiffres du manque le lisent tous deux.
|
||||||
*
|
*
|
||||||
* replier(cumul, valeur) rend le cumul suivant sans modifier celui qu'il
|
* replier(cumul, valeur) rend le cumul suivant sans jamais modifier
|
||||||
* reçoit : les trois populations partent du même initial, et une population
|
* initial : les trois populations partent de lui, et une population sans
|
||||||
* sans membre le garde.
|
* 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
|
* @template C
|
||||||
* @param {ArrayLike<*>} valeurs une par personne, dans l'ordre de instance.ids
|
* @param {ArrayLike<*>} valeurs une par personne, dans l'ordre de instance.ids
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
SANS_OBJECTIF,
|
SANS_OBJECTIF,
|
||||||
STATUT,
|
STATUT,
|
||||||
exigerPlanIndexe,
|
exigerPlanIndexe,
|
||||||
|
figerConfiguration,
|
||||||
indexerPlan,
|
indexerPlan,
|
||||||
nombrePlaces,
|
nombrePlaces,
|
||||||
nombrePlacesManquantes,
|
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)', () => {
|
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', () => {
|
test('chacun compte dans tous, puis parmi les ancrés ou parmi les mobiles, partiellement fixés compris, dans l’ordre canonique', () => {
|
||||||
const statut = Uint8Array.from([ANCRE, MOBILE, PARTIELLEMENT_FIXE, ANCRE, MOBILE, PARTIELLEMENT_FIXE]);
|
const 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.
|
* à t.
|
||||||
* parPaire : les rencontres de chaque paire, rangées par rangPaire ; une
|
* 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
|
* 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,
|
* 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
|
* 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
|
* 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 {import('./types.js').Instance} instance
|
||||||
* @param {ArrayLike<number>} tableDe
|
* @param {ArrayLike<number>} tableDe
|
||||||
* @returns {{parTour: Array<Array<{collegues: number[][], revues: number[][], revenus: number[]}>>,
|
* @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[]}}
|
* servis: number[], rencontresAnimateurs: number[]}}
|
||||||
*/
|
*/
|
||||||
export function parcourirTours(instance, tableDe) {
|
export function parcourirTours(instance, tableDe) {
|
||||||
|
|
@ -169,17 +172,19 @@ export function parcourirTours(instance, tableDe) {
|
||||||
const reserveeAvant = new Uint8Array(T);
|
const reserveeAvant = new Uint8Array(T);
|
||||||
const rencontre = new Int32Array(A).fill(-1);
|
const rencontre = new Int32Array(A).fill(-1);
|
||||||
for (let p = 0; p < N; p += 1) {
|
for (let p = 0; p < N; p += 1) {
|
||||||
|
const soi = rangAnimateur[p];
|
||||||
for (let r = 0; r < R; r += 1) {
|
for (let r = 0; r < R; r += 1) {
|
||||||
const t = tableDe[p * R + r];
|
const t = tableDe[p * R + r];
|
||||||
if (t === RESERVE) continue;
|
if (t === RESERVE) continue;
|
||||||
toursAssis[p] += 1;
|
toursAssis[p] += 1;
|
||||||
const imposee = fixe[p * R + r];
|
const imposee = fixe[p * R + r];
|
||||||
if (imposee === LIBRE ? siegesLibres[t * R + r] > 0 : imposee === t) {
|
const ligne = t * R + r;
|
||||||
const o = objectifDe[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;
|
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];
|
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;
|
rencontre[f] = p;
|
||||||
rencontresAnimateurs[p] += 1;
|
rencontresAnimateurs[p] += 1;
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +214,7 @@ export function parcourirTours(instance, tableDe) {
|
||||||
const nombrePaires = (N * (N - 1)) / 2;
|
const nombrePaires = (N * (N - 1)) / 2;
|
||||||
const parPaire = R <= UINT16_MAX ? new Uint16Array(nombrePaires) : new Uint32Array(nombrePaires);
|
const parPaire = R <= UINT16_MAX ? new Uint16Array(nombrePaires) : new Uint32Array(nombrePaires);
|
||||||
const libre = new Uint8Array(nombrePaires);
|
const libre = new Uint8Array(nombrePaires);
|
||||||
|
const reunies = [];
|
||||||
const parTour = [];
|
const parTour = [];
|
||||||
for (let r = 0; r < R; r += 1) {
|
for (let r = 0; r < R; r += 1) {
|
||||||
const assis = Array.from({ length: T }, () => []);
|
const assis = Array.from({ length: T }, () => []);
|
||||||
|
|
@ -231,6 +237,7 @@ export function parcourirTours(instance, tableDe) {
|
||||||
const b = liste[j];
|
const b = liste[j];
|
||||||
const k = rangPaire(N, a, b);
|
const k = rangPaire(N, a, b);
|
||||||
parPaire[k] += 1;
|
parPaire[k] += 1;
|
||||||
|
if (parPaire[k] === 1) reunies.push(a, b);
|
||||||
if (!aReserve || fixe[b * R + r] !== t) libre[k] = 1;
|
if (!aReserve || fixe[b * R + r] !== t) libre[k] = 1;
|
||||||
if (g !== SANS_GROUPE && g === groupe[b]) collegues.push([a, b]);
|
if (g !== SANS_GROUPE && g === groupe[b]) collegues.push([a, b]);
|
||||||
if (parPaire[k] >= 2 && libre[k] === 1) revues.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);
|
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 :
|
// Lecture des paires réunies (reunies de parcourirTours), leurs comptes dans
|
||||||
// |met(p)|, |F(p)| et A(p), vu[p * G + g] notant que p a déjà croisé un
|
// parPaire : une paire jamais réunie ne change aucun chiffre, et l'ordre de
|
||||||
// affilié du groupe g. Sur toutes les paires : les paires réunies deux fois
|
// lecture n'en change aucun non plus. Par personne : |met(p)|, |F(p)| et
|
||||||
// au moins, et le plus grand compte.
|
// A(p), vu[p * G + g] notant que p a déjà croisé un affilié du groupe g. Sur
|
||||||
function lirePaires({ N, groupe, groupes }, parPaire) {
|
// 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 G = groupes.length;
|
||||||
const rencontres = new Int32Array(N);
|
const rencontres = new Int32Array(N);
|
||||||
const affilies = new Int32Array(N);
|
const affilies = new Int32Array(N);
|
||||||
|
|
@ -270,16 +279,14 @@ function lirePaires({ N, groupe, groupes }, parPaire) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let k = 0;
|
for (let i = 0; i < reunies.length; i += 2) {
|
||||||
for (let a = 0; a < N; a += 1) {
|
const a = reunies[i];
|
||||||
for (let b = a + 1; b < N; b += 1, k += 1) {
|
const b = reunies[i + 1];
|
||||||
const n = parPaire[k];
|
const n = parPaire[rangPaire(N, a, b)];
|
||||||
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) repetees += 1;
|
||||||
if (n >= 2) repetees += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { rencontres, affilies, vues, repetees, maxRencontresPaire };
|
return { rencontres, affilies, vues, repetees, maxRencontresPaire };
|
||||||
}
|
}
|
||||||
|
|
@ -325,12 +332,16 @@ function lireTours({ N, groupe, groupes }, parTour) {
|
||||||
// aucun Infinity ne sert de minimum initial.
|
// aucun Infinity ne sert de minimum initial.
|
||||||
const VIDE = Object.freeze({ min: null, somme: 0, effectif: 0 });
|
const VIDE = Object.freeze({ min: null, somme: 0, effectif: 0 });
|
||||||
|
|
||||||
// La population, une valeur de plus ; celle qu'elle reçoit reste intacte.
|
// La population, une valeur de plus. VIDE, que les trois populations
|
||||||
const ajouter = ({ min, somme, effectif }, valeur) => ({
|
// partagent au départ, reste intact : la première valeur ouvre un cumul
|
||||||
min: min === null || valeur < min ? valeur : min,
|
// neuf, que les suivantes complètent en place, sans un objet par personne.
|
||||||
somme: somme + valeur,
|
function ajouter(cumul, valeur) {
|
||||||
effectif: effectif + 1,
|
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.
|
// Agrégat d'une population ; la moyenne d'une population vide vaut null.
|
||||||
const conclure = ({ min, somme, effectif }) => ({
|
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
|
// 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 ;
|
// 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
|
// 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.
|
// tiennent en un passage, ce nombre ramené à 0 quand E l'est. servis est le
|
||||||
function lireObjectifs({ N, possibles, statut }, servis) {
|
// tableau neuf de parcourirTours, que la mesure rend tel quel sous tours.
|
||||||
const tours = [...servis];
|
function lireObjectifs({ N, possibles: P, statut }, servis) {
|
||||||
const parPersonne = Array.from(possibles);
|
|
||||||
let concernes = 0;
|
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 horsObjectif = 0;
|
||||||
let jamaisServies = 0;
|
let jamaisServies = 0;
|
||||||
let horsObjectifMax = 0;
|
let horsObjectifMax = 0;
|
||||||
let auMax = 0;
|
let auMax = 0;
|
||||||
for (let p = 0; p < N; p += 1) {
|
for (let p = 0; p < N; p += 1) {
|
||||||
if (parPersonne[p] === 0) continue;
|
possibles[p] = P[p];
|
||||||
concernes += 1;
|
if (P[p] === 0) {
|
||||||
const hors = parPersonne[p] - tours[p];
|
selonObjectif[p] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
selonObjectif[p] = servis[p];
|
||||||
|
const hors = P[p] - servis[p];
|
||||||
horsObjectif += hors;
|
horsObjectif += hors;
|
||||||
if (tours[p] === 0) jamaisServies += 1;
|
if (servis[p] === 0) jamaisServies += 1;
|
||||||
if (hors > horsObjectifMax) {
|
if (hors > horsObjectifMax) {
|
||||||
horsObjectifMax = hors;
|
horsObjectifMax = hors;
|
||||||
auMax = 0;
|
auMax = 0;
|
||||||
}
|
}
|
||||||
if (hors === horsObjectifMax) auMax += 1;
|
if (hors === horsObjectifMax) auMax += 1;
|
||||||
}
|
}
|
||||||
if (concernes === 0) return null;
|
|
||||||
return {
|
return {
|
||||||
tours,
|
tours: servis,
|
||||||
possibles: parPersonne,
|
possibles,
|
||||||
horsObjectif,
|
horsObjectif,
|
||||||
jamaisServies,
|
jamaisServies,
|
||||||
horsObjectifMax,
|
horsObjectifMax,
|
||||||
personnesAuMax: horsObjectifMax === 0 ? 0 : auMax,
|
personnesAuMax: horsObjectifMax === 0 ? 0 : auMax,
|
||||||
concernes,
|
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,
|
// 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
|
// 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
|
// somment B(p) − m(p) sur toutes les personnes, une non concernée y valant
|
||||||
// zéro.
|
// zéro. rencontres est le tableau neuf de parcourirTours, rendu tel quel.
|
||||||
function lireAnimateurs({ borneAnimateurs, statut }, rencontresAnimateurs) {
|
function lireAnimateurs({ N, borneAnimateurs, statut }, rencontres) {
|
||||||
const rencontres = [...rencontresAnimateurs];
|
let concernes = 0;
|
||||||
const bornes = Array.from(borneAnimateurs);
|
for (let p = 0; p < N; p += 1) if (borneAnimateurs[p] > 0) concernes += 1;
|
||||||
const concernes = bornes.filter((borne) => borne > 0).length;
|
|
||||||
if (concernes === 0) return null;
|
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 {
|
return {
|
||||||
rencontres,
|
rencontres,
|
||||||
bornes,
|
bornes,
|
||||||
nonRencontres: somme(bornes) - somme(rencontres),
|
nonRencontres,
|
||||||
concernes,
|
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) {
|
export function mesurer(instance, tableDe) {
|
||||||
const { N, groupe, groupes, statut } = instance;
|
const { N, groupe, groupes, statut } = instance;
|
||||||
const { parTour, parPaire, toursAssis, retours, servis, rencontresAnimateurs } = parcourirTours(instance, tableDe);
|
const { parTour, parPaire, reunies, toursAssis, retours, servis, rencontresAnimateurs } = parcourirTours(instance, tableDe);
|
||||||
const paires = lirePaires(instance, parPaire);
|
const paires = lirePaires(instance, parPaire, reunies);
|
||||||
const { collisions, distinctes, choisies, retoursChoisis } = lireTours(instance, parTour);
|
const { collisions, distinctes, choisies, retoursChoisis } = lireTours(instance, parTour);
|
||||||
|
|
||||||
const rencontres = Array.from(paires.rencontres);
|
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
|
// (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
|
// à 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
|
// 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 { instance, tableDe } = preparer(PARCOURS);
|
||||||
const vide = { collegues: [], revues: [], revenus: [] };
|
const vide = { collegues: [], revues: [], revenus: [] };
|
||||||
assert.deepEqual(parcourirTours(instance, tableDe), {
|
assert.deepEqual(parcourirTours(instance, tableDe), {
|
||||||
|
|
@ -727,6 +728,7 @@ describe('parcourirTours', () => {
|
||||||
[{ collegues: [], revues: [[0, 2]], revenus: [0, 2] }, vide],
|
[{ collegues: [], revues: [[0, 2]], revenus: [0, 2] }, vide],
|
||||||
],
|
],
|
||||||
parPaire: Uint16Array.of(2, 2, 0, 1, 0, 1),
|
parPaire: Uint16Array.of(2, 2, 0, 1, 0, 1),
|
||||||
|
reunies: [0, 1, 0, 2, 1, 2, 2, 3],
|
||||||
toursAssis: [3, 3, 3, 2],
|
toursAssis: [3, 3, 3, 2],
|
||||||
retours: [2, 1, 1, 1],
|
retours: [2, 1, 1, 1],
|
||||||
servis: [0, 0, 0, 0],
|
servis: [0, 0, 0, 0],
|
||||||
|
|
@ -763,6 +765,7 @@ describe('parcourirTours', () => {
|
||||||
assert.deepEqual(parcourirTours(instance, new Int32Array(0)), {
|
assert.deepEqual(parcourirTours(instance, new Int32Array(0)), {
|
||||||
parTour: [[vide], [vide]],
|
parTour: [[vide], [vide]],
|
||||||
parPaire: new Uint16Array(0),
|
parPaire: new Uint16Array(0),
|
||||||
|
reunies: [],
|
||||||
toursAssis: [],
|
toursAssis: [],
|
||||||
retours: [],
|
retours: [],
|
||||||
servis: [],
|
servis: [],
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,10 @@
|
||||||
// rencontre apparaît ou disparaît, et m(p), avec les animateurs non
|
// 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
|
// 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.
|
// 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
|
// 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
|
// 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} jamaisServies J
|
||||||
* @property {number} horsObjectifMax E
|
* @property {number} horsObjectifMax E
|
||||||
* @property {number} personnesAuMax concernés à E ; 0 quand E = 0
|
* @property {number} personnesAuMax concernés à E ; 0 quand E = 0
|
||||||
* @property {Int32Array} rencontresAnimateurs m(p)
|
* @property {boolean} suitAnimateurs vrai quand le n-uplet compose les
|
||||||
* @property {number} animateursNonRencontres Σ (B(p) − m(p))
|
* 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 :
|
* @property {Object} composition la forme du n-uplet de l'instance :
|
||||||
* taille, termes de chaque composante, ordres de comparaison et
|
* taille, termes de chaque composante, ordres de comparaison,
|
||||||
* rang du plus grand écart
|
* rang du plus grand écart et suitAnimateurs
|
||||||
*
|
*
|
||||||
* @typedef {{tour: number, a: number, b: number}} Mouvement
|
* @typedef {{tour: number, a: number, b: number}} Mouvement
|
||||||
* l'échange des occupants des sièges a et b au tour d'index tour
|
* 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
|
// 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
|
// 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.
|
// 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) {
|
function composition(instance) {
|
||||||
const { paliers, ordreScore, ordreContraintes, rangEcart, taille } = compilationDe(instance);
|
const { paliers, ordreScore, ordreContraintes, rangEcart, taille } = compilationDe(instance);
|
||||||
const { contraintes, modeObjectifs } = instance;
|
const { contraintes, modeObjectifs } = instance;
|
||||||
|
|
@ -287,6 +296,7 @@ function composition(instance) {
|
||||||
ordreScore: Int32Array.from(ordreScore),
|
ordreScore: Int32Array.from(ordreScore),
|
||||||
ordreContraintes: Int32Array.from(ordreContraintes),
|
ordreContraintes: Int32Array.from(ordreContraintes),
|
||||||
rangEcart,
|
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
|
// 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) {
|
function changerTable(etat, p, depart, arrivee, r) {
|
||||||
if (etat.O > 0) {
|
if (etat.O > 0) {
|
||||||
const ecart = sert(etat, p, arrivee, r) - sert(etat, p, depart, r);
|
const ecart = sert(etat, p, arrivee, r) - sert(etat, p, depart, r);
|
||||||
if (ecart !== 0) changerServis(etat, p, ecart);
|
if (ecart !== 0) changerServis(etat, p, ecart);
|
||||||
}
|
}
|
||||||
if (etat.A > 0) {
|
if (etat.suitAnimateurs) {
|
||||||
changerAnimateurs(etat, p, depart, r, -1);
|
changerAnimateurs(etat, p, depart, r, -1);
|
||||||
changerAnimateurs(etat, p, arrivee, 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,
|
// 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
|
// chaque visite et chaque rencontre d'animateur par les mêmes fonctions que
|
||||||
// les mouvements ; les écarts partent du plafond a priori, aucune rencontre
|
// 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
|
// 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
|
// 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
|
// 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) {
|
function etatInitial(structure, rng) {
|
||||||
const { instance, N, T, R, S, mobiles, libres, aPriori, tableDuSiege, debut } = structure;
|
const { instance, N, T, R, S, mobiles, libres, aPriori, tableDuSiege, debut } = structure;
|
||||||
const G = instance.groupes.length;
|
const G = instance.groupes.length;
|
||||||
|
const { suitAnimateurs } = structure.composition;
|
||||||
const etat = {
|
const etat = {
|
||||||
instance,
|
instance,
|
||||||
contraintes: instance.contraintes,
|
contraintes: instance.contraintes,
|
||||||
|
|
@ -646,20 +659,23 @@ function etatInitial(structure, rng) {
|
||||||
personnesAuMax: 0,
|
personnesAuMax: 0,
|
||||||
// Les animateurs : qui anime à chaque table et chaque tour, de
|
// Les animateurs : qui anime à chaque table et chaque tour, de
|
||||||
// l'instance ; le compte de chaque couple (personne, animateur), m(p) et
|
// 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,
|
A: instance.A,
|
||||||
rangAnimateur: instance.rangAnimateur,
|
rangAnimateur: instance.rangAnimateur,
|
||||||
debutAnimes: instance.debutAnimes,
|
debutAnimes: instance.debutAnimes,
|
||||||
animes: instance.animes,
|
animes: instance.animes,
|
||||||
rencontresAnimees: R <= UINT8_MAX ? new Uint8Array(N * instance.A) : new Uint32Array(N * instance.A),
|
rencontresAnimees: !suitAnimateurs
|
||||||
rencontresAnimateurs: new Int32Array(N),
|
? null
|
||||||
animateursNonRencontres: 0,
|
: 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) {
|
for (let p = 0; p < N; p += 1) {
|
||||||
etat.histogramme[aPriori[p]] += 1;
|
etat.histogramme[aPriori[p]] += 1;
|
||||||
etat.carresEcartsAuPlafondAPriori += aPriori[p] * aPriori[p];
|
etat.carresEcartsAuPlafondAPriori += aPriori[p] * aPriori[p];
|
||||||
etat.ecartAuPlafondAPrioriMax = Math.max(etat.ecartAuPlafondAPrioriMax, 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;
|
const { siege, tableDe } = etat;
|
||||||
for (let r = 0; r < R; r += 1) {
|
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 (q !== FANTOME) modifierPaire(etat, p, q, 1);
|
||||||
}
|
}
|
||||||
if (etat.O > 0) etat.servis[p] += sert(etat, p, t, r);
|
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) {
|
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);
|
const directs = animateursRencontres(configuration, instance, etat.tableDe);
|
||||||
assert.deepEqual(Array.from(etat.rencontresAnimateurs), directs);
|
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)", () => {
|
describe("recherche : le mode et l'ordre réglé ne touchent pas au hasard (A13, § 15.5)", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue