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

Defectele pachetului meu de creaţie grafică

limbajul R
2016 dec

În [1] am constituit trei funcţii scurte, care angajează pachetul de bază graphics din R pentru a crea lucrări grafice precum cele redate în computer_art_R. Între timp, am reuşit să constat că mi-au scăpat din vedere "amănunte" instructive de programare, ţinând de natura funcţională a limbajului R (şi pe de altă parte, de specificul ferestrelor grafice).

Pentru a sesiza defectele unui program, în primul rând trebuie să compari rezultatul cu ceea ce ştii că trebuia să obţii. Dar programele de "creaţie grafică" produc de regulă rezultate pe care nu le poţi anticipa; totuşi - "imitarea" unor lucrări cunoscute (devenind astfel clar ce trebuie să obţii) facilitează şi în acest caz, depanarea şi punerea la punct a programului propriu.

Vrând să imit imaginea redată în This is not a bird, am început prin a revedea secvenţa prin care imitasem o piesă asemănătoare: într-o sesiune R "curată" (fără obiecte create în sesiuni de lucru anterioare), am încărcat cele trei funcţii şi am pastat în consolă secvenţa preluată din [1] §4:

În loc de imaginea respectivă (cum credeam eu că obţin!), avem doar un mesaj de eroare; traceback() ne arată cum s-au desfăşurat lucrurile: plotart() a lansat weave(), care a apelat smoothScatter(), care la rândul ei a apelat xy.coords() - iar aceasta n-a putut fi executată din cauză că "object 'x1' not found".

Apelul plotart() a setat "sct_x = x0" şi "sct_y = 5*x1" pentru primii doi parametri aşteptaţi de smoothScatter(); dar… care "x0" şi "x1"? NU cei setaţi chiar prin linia de apel plotart() - fiindcă R utilizează mecanismul denumit "lazy evaluation", prin care o expresie transmisă ca argument este evaluată numai atunci când este necesar; de exemplu, pentru setarea "x0 = 3*sin(a)^3", expresia care defineşte valoarea argumentului "x0" este evaluată (luînd astfel fiinţă şi variabila "x0") numai când în corpul funcţiei respective (sau al unei funcţii care o apelează pe aceasta) se întâlneşte un calcul care angajează "x0" - ori, înaintea apelului smoothScatter() nu există niciun calcul care să fi necesitat valoarea acestei variabile (nici în corpul funcţiei weave(), nici în corpul funcţiei apelante plotart()).

Următorul experiment arată clar că într-adevăr, "x0" şi "x1" nu există, în momentul apelului funcţiei smoothScatter(): ştergem variabila "a" (pe baza căreia ar urma să se calculeze valorile parametrilor "x0", etc.) şi relansăm apoi funcţia plotart(). Într-un limbaj "obişnuit" - fără "lazy evaluation" - ne-am aştepta la mesajul "object 'a' not found", dar nu şi aici:

Iată că avem acelaşi mesaj "object 'x1' not found" ca şi în cazul anterior, când variabila 'a' fusese creată; deci obiectele "x0" şi "x1" nu există, în momentul apelului smoothScatter() - crearea lor necesita existenţa valorii variabilei 'a' şi atunci s-ar fi sesizat "object 'a' not found".

Faptul că în [1] §4 nu am obţinut acest mesaj de eroare şi lucrurile au evoluat până la producerea în fereastra grafică a imaginii redate acolo, se explică prin aceea că în sesiunea de lucru în care folosisem secvenţa de comenzi amintită fuseseră create şi variabilele "globale" x0, x1, etc. - încât smoothScatter() le-a putut folosi pe acestea (şi abia restul liniilor de program din weave() a creat şi folosit variabilele "locale" x0, x1, etc. definite în apelul funcţiei plotart()). Iar acum, putem să reproducem exact imaginea din [1] §4 numai dacă putem reconstitui variabilele "globale" folosite în sesiunea de lucru respectivă…

Corectura tipică pentru această situaţie (în R, ca şi în Python) constă în definirea parametrilor formali nu prin expresii care depind de alţi parametri formali (cum avem în weave(sct_x = y0 etc.)), ci prin valoarea NULL; apoi, în corpul funcţiei se verifică dacă valoarea parametrului a rămas NULL - caz în care se forţeză (prin eval()) evaluarea parametrului formal corespunzător; altfel, se foloseşte valoarea explicitată direct în apelul funcţiei pentru acel parametru:

weave <- function(x0, y0, x1, y1, 
                  col="black", grd=c(rgb(0, 0.5, 0.6, 0), rgb(0.2, 0.6, 0.4, 0)),
                  lwd=0.6, trim=FALSE, trim_at = 2, 
                  sct_x = NULL, sct_y = NULL,  # în loc de: sct_x = y0, sct_y = x1
                  ...
                 ) {
    opar <- par(mar=c(0, 0, 0, 0), bty="n", xaxt="n", yaxt="n")
    if(is.null(sct_x)) {  # Dacă în apel nu s-a furnizat valoarea parametrului,
        sct_x = eval(y0)  # se forţează evaluarea variabilei locale dorite (sct_x = y0)
    }
    if(is.null(sct_y)) {
        sct_y = eval(x1)
    }
    smoothScatter(sct_x, sct_y, col = NA, 
                  colramp = colorRampPalette(grd, alpha=TRUE), ...)
    # ... celelalte linii de program din [1] ...
}

Ilustrăm cele două cazuri (întâi fără a specifica valorile pentru "sct_x" şi sct_y şi apoi, cu setarea acestora în linia de apel):

Pentru prima imagine, s-au folosit valorile "implicite" sct_x = eval(y0) şi sct_y = eval(x1) (care în acest caz chiar nu sunt potrivite) - iar pentru a doua, s-au folosit valorile transmise prin linia de apel. Imaginile rezultate pe ecran prin aceste apeluri doar "seamănă", cu aceea redată în [1] §4 - nefiind în stare să reconstitui variabilele "globale" din sesiunea de lucru în care fusese obţinută aceasta; le-am reprodus aici cu scopul de a evidenţia încă un "defect" al construcţiilor din [1].

Poţi seta "prin program" limitele axelor şi mărimea zonelor care împrejmuiesc graficul de produs în fereastra grafică - dar nu însăşi dimensiunea fereastrei grafice asociate ecranului; probabil că este firesc să fie aşa, fiindcă pentru ecran dispunem de mouse (şi de exemplu în cazul primei imagini, putem "trage" de marginile ferestrei, pentru a "întregi" figura respectivă).

În schimb, pentru alte sisteme grafice decât ecranul ("png", de exemplu) avem posibilitatea de a seta convenabil dimensiunea ferestrei grafice asociate, iar în [1] chiar am făcut aceasta - doar că am setat dimensiuni fixe (şi am constatat deja că valorile fixate nu sunt totdeauna potrivite). Dar este uşor de corectat acest "defect" de dimensionare, cum se poate vedea ceva mai jos.

Să revenim la imaginea pe care ne propusesem la început să o imităm (din nota citată mai sus, "This is not a bird") - observând în sfârşit, un al treilea "defect" al funcţiilor noastre: posibilităţile de furnizare a vectorilor de coordonate necesari sunt prea limitate: ai de folosit neapărat "way = 4" - implicând numai funcţia weave() - pentru a introduce coordonatele prin expresii diferite de cele vizate direct în corpul funcţiei plotart(); iar weave() implică segments(), cerând astfel patru vectori de coordonate (pe când lines() ar fi necesitat numai doi vectori).

În nota citată sunt redate formulele coordonatele punctelor, în planul complex:

λ*A(t) + (1-λ)*B(t)
A(t) = 3*sin(t)^3 - 0.75*j*cos(4*t)
B(t) = 1.5*sin(t)^5 - 0.5*j*cos(3*t)
unde 0 ≤ t ≤ 2π, 0 ≤ λ ≤ 1, iar j este unitatea imaginară

Aspectul nou faţă de ceea ce am avut în vedere până acum, constă în implicarea produsului cartezian: trebuie să împerechem fiecare valoare λ cu fiecare valoare A(t), etc.; putem folosi pentru aceasta funcţia expand.grid().

Calculul coordonatelor este acum laborios, încât el trebuie organizat la nivel "global" (nu putem exprima coordonatele direct în linia de apel plotart(), cum am putut face până acum). Funcţia următoare primeşte cele două secvenţe de valori pentru 'λ' şi respectiv 't' şi returnează o listă conţinând vectorul absciselor şi vectorul ordonatelor (calculaţi după formulele de mai sus):

pair <- function(q, a) {
    p1 <- expand.grid(3*q, sin(a)^3)  # partea reală din λ*A(t)
    p2 <- expand.grid(1.5*(1-q), sin(a)^5)  # partea reală din (1 - λ)*B(t)
    x <- p1$Var1*p1$Var2 + p2$Var1*p2$Var2  # vectorul tuturor absciselor
    p1 <- expand.grid(-0.75*q, cos(4*a))  # partea imaginară din λ*A(t)
    p2 <- expand.grid(0.5*(q-1), cos(3*a))  # partea imaginară din (1 - λ)*B(t)
    y <- p1$Var1*p1$Var2 + p2$Var1*p2$Var2  # vectorul tuturor ordonatelor
    list(x, y)
}

Invocăm pair(), obţinând lista celor doi vectori (de abscise şi respectiv, ordonate); dimensionăm adecvat acestor valori, fereastra grafică (pentru "png") - multiplicând cu 80 distanţa maximă dintre abscise şi respectiv, ordonate (şi am ales 80 ştiind că de obicei, "png" vizează 72 "pixels per inch"); apoi apelăm funcţia noastră crochet():

lxy <- pair(seq(0, 1, 0.2), seq(0, 2*pi, pi/750))
xlim <- range(lxy[[1]])  # pentru dimensionarea adecvată a ferestrei grafice
ylim <- range(lxy[[2]])
png = png(filename="images/mostra10.png", bg="transparent", units="px",
          width = (xlim[2]-xlim[1])*80+16, height = (ylim[2]-ylim[1])*80+16)
crochet(lxy[[1]], lxy[[2]], cex=0.7, col1="gray18", col2="gray20", lwd=0.5)
dev.off()

Bineînţeles că, "îndesind" valorile parametrilor transmişi în pair() (şi eventual, mărind valoarea parametrului "lwd" din apelul crochet()) - putem "înnegri" complet figura, pentru a obţine o şi mai mare asemănare cu imaginea din nota citată mai sus (dar rezultatul redat mai sus ni se pare totuşi, o variantă interesantă a acesteia).

crochet() a plotat "puncte" de o anumită mărime şi formă (v. [1]); dacă am plota linii - folosind direct funcţia lines() - atunci nici n-ar fi nevoie de "îndesit" valorile parametrilor (şi am putea obţine o imagine cât de apropiată am vrea, faţă de imaginea citată):

lxy <- pair(c(0.1, 0.7), seq(0, 2*pi, pi/720))  # pentru λ avem acum doar două valori
xlim <- range(lxy[[1]])
ylim <- range(lxy[[2]])
png = png(filename="images/mostra13.png", bg="transparent", units="px",
          width = (xlim[2]-xlim[1])*80+16, height = (ylim[2]-ylim[1])*80+16)
opar <- par(mar = c(0,0,0,0))
plot.new()
plot.window(xlim = xlim, ylim = ylim, asp = 0.9)
lines(lxy[[1]], lxy[[2]], col="gray25")
par(opar)
dev.off()

Aceste ultime trei versiuni au rezultat prin secvenţa de comenzi redată deasupra, pentru diverse alegeri ale valorilor parametrilor lui pair(). Este clar acum că în "pachetul" iniţiat în [1] va trebui să adăugăm o funcţie care să modeleze această secvenţă (folosind numai lines()) şi poate, nişte funcţii "auxiliare" - precum pair(), dar probabil mai generală decât aceasta - prin care să avem posibilitatea de a combina valorile unor parametri.

vezi Cărţile mele (de programare)

docerpro | Prev | Next