momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Împachetarea alocării pe ore a lecțiilor zilei (II)

limbajul R | orar şcolar
2025 apr

În [3] am demarat pachetul hours2lessons: am constituit întâi scheletul pachetului, specificând în fișierul 'DESCRIPTION' pachetele care trebuie importate (de exemplu, dplyr și igraph) și în 'NAMESPACE' funcțiile necesare din fiecare; apoi am introdus funcția scale_prof_cls() care asociază setului de lecții prof|cls indicat, anumiți coeficienți de ierarhizare ("betweenness" pe graful profesorilor și respectiv, al claselor — după numărul de clase, respectiv profesori, în comun).
Apoi, am introdus funcția mount_hours() prin care se obține un orar prof|cls|ora — dar, simplificând lucrurile pentru o primă tentativă de împachetare, am omis cazul când avem lecții cuplate, precum și cazul când există tuplaje…

Pentru a ține seama de existența unor cuplaje (eventual și de tuplaje, asociate suplimentar lecțiilor zilei), am avea de extins funcția mountHtoCls() — care apare (v. [3]) în interiorul funcției mount_hours() — adăugând condiția ca lecțiile celor angajați în câte un cuplaj să nu se suprapună (iar pentru tuplaje… dimpotrivă: lecțiile dintr-un același tuplaj trebuie să cadă în câte o aceeași oră).

Constatarea și modelarea dependențelor induse de cuplaje

Cum am convenit anterior (v. [3]), un cod prof de lungime 6 reprezintă un cuplaj; avem de filtrat prof după lungimea codurilor și dacă este cazul, de furnizat o listă care să indice dependențele de asigurat la alocarea pe ore a lecțiilor celor angajați în cuplaje.

Lansăm R din directorul sursă al pachetului nostru, restaurăm pachetul în memorie și instituim funcția necesară:

> library(devtools)
> load_all()  # directorul curent este cel care conține sursa pachetului
> use_r("get_twins")
#' Depistează cuplajele și dependențele de alocare pe ore induse de acestea
#' @param LSS data.frame cu lecțiile prof|cls
#' @return NULL dacă nu există cuplaje; altfel, lista care indică bilateral,
#'     pe fiecare membru al unui cuplaj și pe fiecare cuplaj, profesorii
#'     și cuplajele de care depinde alocarea pe ore a lecțiilor sale
#'
get_twins <- function(LSS) {
    if(all(nchar(LSS$prof) == 3)) return(NULL)  # nu există cuplaje
    P36 <- split(LSS, ~ nchar(LSS$prof)) %>%
           map(function(LS) LS %>% pull(.data$prof) %>% unique() %>% sort())
    P3 <- P36[[1]]  # vectorul profesorilor angajați în cuplaje
    P6 <- P36[[2]]  # vectorul cuplajelor
    Tw1 <- map(P3, function(P) P6[grepl(P, P6)]) %>%
           setNames(P3) %>% purrr::compact()
    Tw2 <- map(P6, function(PP) {
               p1 <- substr(PP, 1, 3)
               p2 <- substr(PP, 4, 6)
               setdiff(c(p1, p2, Tw1[[p1]], Tw1[[p2]]), PP) %>%
               unique() 
           }) %>% setNames(P6) %>% purrr::compact()
    list(Tw1, Tw2)
}

Chiar dacă este vorba de o funcție internă pachetului (a observa că lipsește specificația "@extern"), o putem invoca (având sursele în memorie) și o putem testa direct (fără a instala pachetul):

> check()  # OK! (dar nu înseamnă totdeauna, că lucrurile sunt în regulă)
> LSS <- readRDS("~/24dec/R12_ldz.RDS")
> TW <- get_twins(LSS[["Jo"]])  # lecțiile prof|cls din ziua "Jo"
> TW[[1]] 
    $Ds1  "Ds1Mz1"
    $Fr1  "Fr1Fr2" "Fr1Gr1"
    $Fr2  "Fr1Fr2"
    $Gr1  "Fr1Gr1"
    $Mz1  "Ds1Mz1"
> TW[[2]]
    $Ds1Mz1  "Ds1" "Mz1"
    $Fr1Fr2  "Fr1"    "Fr2"    "Fr1Gr1"
    $Fr1Gr1  "Fr1"    "Gr1"    "Fr1Fr2"

Va trebui să adăugăm două condiții: dacă P este o cheie din TW[[1]], lecția (P, cls) nu va putea fi într-o aceeași oră cu niciuna dintre lecțiile (TW[[1]][P], cls); analog, pentru lecțiile cu cheile în TW[[2]].

Specificarea așteptată pentru tuplaje

Ne așteptăm ca tuplajele, dacă există, să fie specificate pe câte o linie a unui set suplimentar prof|cls, unde prof înregistrează profesorii care vor trebui să se afle (câte unul, sau eventual câte doi) într-o aceeași oră, la câte una dintre "clasele" înregistrate în cls — de exemplu:

                 prof            cls
    1         Ds1 Mz1        10E 11E
    2     Fr2 Fr3 Gr1    09A 09C 09D
    3 Fr1 Fr2 Fr3 Gr2    10B 10C 10E
    4     Fr1 Fr2 Gr1        11C 11E

Ds1 și Mz1 trebuie să intre într-o aceeași oră, la clasele 10E și respectiv 11E (și eventual invers, în săptămâna următoare — dacă orele de "Desen" și "Muzică" sunt alocate la clasele respective "pe câte o jumătate de oră/săptămână").
Lecțiile de "Franceză" și "Germană" pot angaja într-o aceeași oră, mai mulți profesori și mai multe clase; adesea, elevii reuniți ai claselor respective sunt regrupați în noi "clase" (pentru care păstrăm numele de clasă, chiar dacă acum "09A" cuprinde "începători" din două sau mai multe dintre clasele respective); în exemplul de mai sus (v. linia 2), Fr2 va trebui să intre la "clasa" 09A într-o aceeași oră în care Fr3 intră la 09C și Gr1 intră la 09D.
Dacă într-un tuplaj, numărul de profesori este mai mare decât cel de clase (v. liniile 3 și 4), atunci va trebui să înființăm un cuplaj de doi profesori pentru una dintre clasele respective; de exemplu, pentru linia 3: înființăm cuplajul Fr1Fr2 pentru "clasa" 10B (deci Fr1 și Fr2 vor intra în ora respectivă la câte o grupă de elevi, din clasa constituită ad-hoc sub numele 10B) și în aceeași oră, Fr3 va intra la 10C și Gr2 va intra la 10E.

Subliniem că ordinea în care se specifică profesorii și clasele din fiecare tuplaj nu este (așa de) importantă; poate că Fr1Fr2 trebuie să intre nu la grupe din 10B, ci la grupe din 10C (și atunci clasele trebuiau specificate prin 10C 10B 10E, sau după caz într-o altă ordine). De fapt, clasele de pe linia respectivă desemnează acum (eventual) niște noi "clase", constituite ad_hoc (după reguli convenite de către profesorii respectivi) din elevii reuniți ai claselor inițiale; "10B" ar desemna acea clasă care trebuie să facă "Franceză" pe grupe, iar "10E" pe aceea, nou constituită, care are de făcut "Germană".

Subliniem că nu vom lua în seamă cazuri nefirești, precum vreun tuplaj de 5 profesori cu 3 clase (când ar trebui înființate, nefiresc, două cuplaje)… Deasemenea, nu prea are sens să ne gândim la tuplaje de genul doi profesori pe trei clase — "noile" clase ar avea fiecare, prea mulți elevi (față de normativele uzuale).

Observație. Va fi nevoie să separăm profesorii (și respectiv clasele), de pe o linie de tuplaje (de exemplu, din vectorul de lungime 1 "10B 10C 10E" să obținem cei 3 vectori de lungime 1, reprezentând câte o clasă: "10B", "10C" și "10E"); dar pentru a determina numărul de profesori din tuplaj, nu este necesar să folosim strsplit() (urmat de unlist()) — avem o soluție mai simplă (folosind nchar()), știind că profesorii din setul tuplajelor sunt codificați pe câte 3 caractere.
De exemplu, din faptul că șirul din coloana prof, linia 4 (v. exemplul redat mai sus), are lungimea 15 — deducem că în tuplajul respectiv sunt $(15 + 1)/4$ = 4 profesori; am adăugat 1 lungimii șirului, fiindcă numărul de spații separatoare este cu 1 mai mic, decât cel de profesori (nu mai avem spațiu, după ultimul profesor din șir) și am împărțit la 4 fiindcă după fiecare cod de 3 caractere (exceptându-l pe ultimul) urmează un spațiu.
Pentru clase, am putea conveni reprezentarea pe câte 3 caractere (cum se vede mai sus, cu observația subiectivă că era mai frumos "i9A" decât "09A") și atunci, putem găsi după același principiu (plecând de la lungimea șirului din coloana cls) și numărul claselor din tuplaj. Dar n-am vrut să încărcăm cu încă o convenție de notare — acceptând în mod tacit notația standard a claselor, pe două sau trei caractere ("9A", "12A").

Pregătirea alocării pe ore a tuplajelor

Introducem o funcție internă (ne-exportată) care primind setul tuplajelor, îi adaugă o coloană ora (cu valoarea inițială 0L), returnându-l astfel într-o listă, împreună cu un vector conținând clasele implicate în tuplaje și un altul, conținând profesorii respectivi (între care — dacă este cazul — și cuplajul de înființat, când numărul de profesori este (cu unu) mai mare ca al claselor din tuplajul respectiv):

#' Adaptează setul tuplajelor în vederea alocării în câte o aceeași oră
#'     a lecțiilor dintr-un același tuplaj
#' @param TPL data.frame conținând tuplajele prof|cls (cu separare la spațiu
#'     în cadrul fiecăruia dintre cele două câmpuri)
#' @return NULL dacă setul TPL este defectuos; altfel, o listă conținând
#'     setul prof|cls|ora ('ora' fiind inițializată cu 0), împreună cu
#'     doi vectori: profesorii și respectiv clasele, din tuplaje
#'
#'     În unele cazuri (4 profesori pe 3 clase) se înființează un cuplaj
#'     pentru primii doi din tuplajul respectiv.

on_tuples <- function(TPL) {
    Lp <- nchar(TPL$prof)  # lungimile șirurilor din coloana 'prof'
    if(any(Lp > 15)) 
        return(NULL)  # este nefiresc un tuplaj cu peste 4 profesori
    Cls <- gsub("( |\\b)([0-9]{1}[A-Z]{1})( |\\b)", "\\1i\\2\\3", 
                TPL$cls)  # prefixează cu 'i' numele 'cls' de lungime 2  
    Lc <- nchar(Cls)
    if(any(Lp - Lc) > 4) 
        return(NULL)  # un tuplaj ar putea avea cel mult, un cuplaj
    for(i in which(Lp - Lc == 4))  # înființează un cuplaj, la primii doi
        TPL$prof[i] <- sub(" ", "", TPL$prof[i])
    t_prof <- lapply(TPL$prof, function(V) strsplit(V, " ")[[1]]) %>% 
              unlist() %>% as.vector() %>% unique()
    t_cls <- lapply(TPL$cls, function(V) strsplit(V, " ")[[1]]) %>% 
             unlist() %>% as.vector() %>% unique()
    list(TPL %>% mutate(ora = 0L), t_prof, t_cls)
}

Testăm iarăși în maniera ad-hoc, pe un set de tuplaje adus dintr-un program de care ne-am ocupat mai demult în [1] (set care conținea tuplajele corespunzătoare fiecăreia dintre zilele de lucru; am ales pe cele din ziua "Mi"):

> check()  # OK (!)
> LT <- readRDS("~/24nov/tuplaje_b.RDS") %>% 
        filter(zl == "Mi") %>% on_tuples()
> LT
[[1]] # setul tuplajelor (adaptat)
              cls           prof zl ora
    1    9A 9C 9D    Fr2 Fr3 Gr1 Mi   0
    2 10B 10C 10E Fr1Fr2 Fr3 Gr2 Mi   0
    3     11C 11E     Fr1Fr2 Gr1 Mi   0
[[2]] # profesorii din tuplaje; s-a înființat un cuplaj, membru în două tuplaje
[1] "Fr2"    "Fr3"    "Gr1"    "Fr1Fr2" "Gr2"
[[3]] # clasele din tuplaje
 [1] "9A" "9C" "9D" "10B" "10C" "10E" "11C" "11E"

Va trebui să căutăm cuplajele înființate (aici, Fr1Fr2) în vectorii returnați de get_twins() și eventual, să le adăugăm acestora — urmând să respectăm condiția (de cuplare) ca lecțiile lui Fr1Fr2 la 10B, respectiv 11C, să nu se suprapună cu vreo lecție la care Fr1, respectiv Fr2, intră singur și deasemenea, condiția (de tuplare) ca profesorii din tuplajul din linia 2 (inclusiv, cel nou Fr1Fr2), respectiv din linia 3, să intre în câte o aceeași oră 1:7, la clasele din tuplajul respectiv.
Vom rezolva condiția de tuplare folosind dinamic coloana ora — setând sau resetând (după situația curentă a alocării lecțiilor) valorile acesteia pe o linie sau alta.

Montarea orelor pe setul lecțiilor (posibil cuplate sau tuplate)

Dacă, pe lângă setul LSS al lecțiilor (incluzând și lecțiile "pe grupe" de clasă, înregistrate la profesori "fictivi"), este furnizat și un set TPL de tuplaje — atunci extragem prin on_tuples() vectorul profesorilor și pe cel al claselor care apar în tuplaje și verificăm dacă on_tuples() a înființat noi cuplaje (când va fi constatat în vreun tuplaj un număr de profesori mai mare cu 1 decât cel de clase); pentru fiecare nou cuplaj, adăugăm în LSS lecțiile acestuia (din toate tuplajele unde a apărut).
De exemplu, pentru tuplajele returnate mai sus de on_tuples(), înscriem în LSS liniile Fr1Fr2|10C și Fr1Fr2|11C (în LSS vom avea astfel toate lecțiile care trebuie desfășurate "pe grupe" de clasă). Deasemenea… trebuie adăugate în LSS și lecțiile individuale din tuplaje, Fr2|9A, Fr3|9C, Gr1|9D (care trebuie desfășurate într-o aceeași oră), etc.

Apoi, prin get_twins() constituim vectorii care indică pentru fiecare profesor angajat în vreun cuplaj, respectiv pentru fiecare cuplaj, profesorii și cuplajele de care depinde alocarea pe ore a lecțiilor acestuia.
De exemplu, lecția lui Fr1Fr2 la 10B sau la 11C nu se poate suprapune cu vreo lecție a lui Fr1, sau a lui Fr2 și nici cu vreuna a cuplajului Fr1Gr1, existent din start în LSS (iar Fr1Fr2 trebuie adăugat în vectorul dependențelor lui Fr1Gr1).

Apoi, obținem prin scale_prof_cls(LSS), coeficienții de ierarhizare a claselor (și respectiv profesorilor) — folosiți ulterior ca ponderi pentru alegerea aleatorie a câte unei clase, în scopul alocării pe ore a lecțiilor acesteia. Experimentele anterioare ne-au arătat că parcurgerea claselor într-o ordine aleatoare apropiată de cea crescătoare a coeficienților "betweenness" pe graful după numărul de profesori comuni fiecărei perechi de clase, are cele mai bune șanse de a trece consecutiv prin toate clasele, repartizând pe ore lecțiile fiecăreia.

Pentru a urmări și a confrunta alocările făcute pe rând profesorilor și claselor, folosim un vector cu nume (un "dicționar") prin care fiecărui profesor/cuplaj îi corespunde câte un octet în care vom seta bitul de rang i=0:6 pentru a semnala alocarea unei lecții a profesorului respectiv în ora (i+1).

#' Adaugă 'ora' încât oricare două lecții prof|cls|ora să nu se suprapună
#' @param LSS data.frame cu lecțiile prof|cls, unde 'prof' este un 
#'     profesor propriu-zis, sau unul fictiv (cuplaj de doi/clasă)
#' @param TPL data.frame pentru tuplaje, dacă este cazul
#'     Un tuplaj conține 3 sau 4 profesori, pe 2, 3 sau 4 clase
#'     (numărul de profesori fiind cel mult cu 1 mai mare, ca al claselor)
#' @return Un orar prof|cls|ora pentru ziua respectivă
#' @export
#'
mount_hours <- function(LSS, TPL = NULL) {
    ### ... (150 linii)
}

Corpul funcției mount_hours() este totuși prea lung, încât aici se cuvine să-l omitem (pe de altă parte… sperăm că la un moment viitor, pachetul hours2lessons va fi accesibil de pe CRAN).

vezi Cărţile mele (de programare)

docerpro | Prev |