- Questo progetto in determinate parti risulta sessista. Ciò è dovuto alle consegne e non al mio pensiero, scusate.
- La seguente relazione è stata trascritta dall'originale (scritta in LaTeX). Se notate errori per favore fatemelo presente.
Il lavoro si pone l’obiettivo di descrivere il simulatore della battaglia dei sessi che è stato commissionato per il corso di Metodologie di programmazione tenuto dal professore Pietro Cenciarelli. L’applicazione è scritta in Java e tenta di replicare la battaglia dei sessi descritta da Richard Dawkins ne “Il gene egoista”.
Nel saggio del 1976 di Richard Dawkins viene teorizzata la battaglia dei sessi nella quale si scontrano quattro tipologie di individui, due maschili e due femminili. Le tipologie maschili prendono il nome di Morigerati e Avventurieri che d’ora in poi chiameremo rispettivamente M e A. Le tipologie femminili invece sono Prudenti e Spregiudicate che d’ora in poi chiameremo P e S. Le popolazioni composte da tali tipologie si riproducono secondo regole evolutive derivate dalla tabella MAPS che rappresenta il premio evolutivo che un individuo guadagna dopo l’interazione con un altro individuo di sesso opposto.
La tabella MAPS dipende da tre parametri a, b e c che rappresentano rispettivamente il premio per aver diffuso il proprio parco genetico, il costo necessario a crescere un figlio e il costo del corteggiamento preliminare alla procreazione. La tabella MAPS sarà dunque questa:
M | A | |
---|---|---|
P | ||
S |
I parametri scelti da Dawkins sono e . Otteniamo quindi la tabella:
M | A | |
---|---|---|
P | ||
S |
Il nostro primo obiettivo sarà quello di raggiungere la stabilità con questi valori. Per avere una previsione di quale sarà tale stabilità andremo a risolvere due semplici equazioni nelle quali il termine rappresenta il numero di individui di tipologia e indica il premio evolutivo, estratto dalla tabella MAPS, di I dopo aver interagito con J:
Dalle equazioni appena riportate si può affermare che la popolazione è stabile quando ogni tipologia ha lo stesso guadagno medio della propria “rivale” dello stesso sesso. Otteniamo così una formula per sapere quale sarà il rapporto tra le tipologie rivali:
I rapporti di stabilità per i valori imposti da Dawkins sono quindi e che espressi in altro modo sono e .
Il simulatore visualizzerà tali rapporti sotto forma di numeri decimali e quindi assomiglieranno più a questi:
N.B. È importante rispettare i seguenti vincoli cambiando i valori di :
Il modello adottato per implementare il simulatore prevede che ogni individuo possa agire in completa libertà incontrando altri individui di sesso opposto e generando figli con metà del proprio parco genetico e metà del partner solo se entrambi gli individui sono “felici”. Esiste inoltre la possibilità che un individuo muoia di morte naturale prima di aver portato a termine la missione di far prosperare i propri geni.
Per capire quando un individuo è felice dobbiamo prima definire cosa è un rivale.
Si dice rivale di un individuo che appartiene alla tipologia qualsiasi individuo dello stesso sesso di ma di tipologia diversa da .
La nozione di rivale ci serve per definire il concetto di felicità di un individuo:
Un individuo si dice felice quando il suo guadagno medio è maggiore del proprio rivale.
Per fare un esempio:
Se in un determinato momento la tipologia P costituisce il 45% di tutte le donne, allora un individuo di tipo M avrà come guadagno medio:
Mentre un individuo di tipo A ha unguadagno medio:
In questo caso l’individuo di tipo A è felice perché e un incontro con una donna (anch’essa felice) gli frutterà un figlio.
Secondo la teoria di Dawkins gli uomini, producendo gameti più piccoli, hanno un investimento iniziale minore rispetto alla donna che si vede costretta a mettere a disposizione gameti molto più grandi e in numero minore. Questo porta l’uomo a cercare di produrre più gameti possibili e spargere il seme, mentre porta la donna a cercare un compagno affidabile che le assicuri la buona riuscita nella crescita di un figlio. Proviamo a trascrivere questa differenza per rappresentare l’“infelicità” di un individuo considerando il sesso dello stesso.
La probabilità che un individuo muoia è data dalla formula:
dove sono parametri dipendenti dal sesso di . Infatti nel caso sia uomo allora . Se invece fosse donna avremmo . In tal modo aver avuto più incontri pesa di più sulla salute di un uomo di quanto peserebbe su quella di una donna e l’aver fatto più figli pesa di più sulla salute di una donna di quanto non lo farebbe su quella di un uomo. Se questo passaggio suona strano e poco rigoroso è perché è nato da osservazioni empiriche e non da un ragionamento completamente estraneo all’implementazione finale del simulatore.
Come abbiamo già detto, la nascita di un nuovo individuo è dettata dall’incontro di due individui felici che lasceranno al nuovo nato metà dei propri geni ciascuno. Ma cosa è un gene? Abbiamo scelto di rappresentare questo meccanismo con il modello più semplice di ereditarietà genetica, ossia quello sviluppato da Gregor Mendel. Nel modello di Mendel un fenotipo (che nel nostro caso equivale a quello che noi chiamiamo tipologia o tipo di individuo) è dato dai caratteri genetici all’interno del gene di un individuo.
Un gene è una coppia non ordinata di caratteri genetici:
La sola definizione di gene non basta a rappresentare tutte e quattro le tipologie di individui, soprattutto perché è importante distinguere i fenotipi in base al sesso. Abbiamo scelto allora di racchiudere i geni all’interno di un cromosoma:
Un cromosoma è una coppia ordinata di geni. Il primo rappresenta il gene maschile, il secondo quello femminile.
Il carattere genetico può essere recessivo o dominante e l’estrazione del fenotipo seguirà le leggi di Mendel che per brevità non verranno riscritte in questo documento. Per convenzione i caratteri dominanti saranno espressi con un simbolo maiuscolo mentre quelli recessivi con uno minoscolo. La scelta di quali geni sono recessivi e quali dominanti è irrilevante dal punto di vista statistico ma la nostra scelta è caduta sul seguente schema:
Morigerati -> M
Avventurieri -> a
Prudenti -> p
Spregiudicate -> S
Prendiamo in esempio un individuo di tipo S. Il suo cromosoma potrebbe essere . Il genere dell’individuo (femminile) attiva il secondo gene che corrisponde al fenotipo di S.
Un individuo ha un cromosoma , se fosse maschio il suo tipo sarebbe perché il primo carattere domina sul secondo, se fosse femmina il suo tipo sarebbe invece .
Ora che il modello è stato sufficientemente illustrato possiamo dedicarci all’implementazione dello stesso in un’applicazione Java. Il simulatore è stato concepito per essere utilizzato con un’interfaccia a riga di comando.
La classe principale è Simulator nella quale avvengono le impostazioni del caso specifico della simulazione de “la battaglia dei sessi” e del processo delle opzioni e degli argomenti passati come input dall’utente. Il progredire della simulazione è controllato dalla classe Population dalla quale è possibile anche ottenere in qualsiasi momento uno stato rappresentato dalla classe SimulationState. Ogni individuo è un’istanza della classe Human ed ha come campo un’istanza della classe Chromosome.
Secondo il nostro modello ogni individuo è libero di agire in maniera del tutto autonoma, cosa che si sposa bene con un approccio concorrente all’implementazione del simulatore. La classe Human infatti implementa l’interfaccia Runnable che permette al codice di ogni individuo di essere eseguito da un thread creato ad hoc. Questa implementazione ha però mostrato presto i suoi limiti in quanto nei primi test si andava velocemente ad esaurire la memoria della macchina impedendo di creare nuovi thread. La soluzione è stata trovata nella libreria standard di Java che offre la classe ThreadPoolExecutor. Tale classe permette di porre un limite superiore ai thread in esecuzione estraendoli da una coda. La possibilità di impostare dinamicamente il limite superiore di thread della classe ha permesso di eseguire gli individui dello stesso tipo da una sola istanza di ThreadPoolExecutor con tale limite impostato secondo le proporzioni delle popolazioni corrispondenti. Tale accorgimento risolve due problemi riscontrati durante lo sviluppo:
-
Se il numero massimo di thread è superato e una tipologia è nettamente in vantaggio sulle altre può capitare che tutti i thread disponibili siano occupati da individui della stessa tipologia bloccando la simulazione.
-
Se la popolazione totale supera il numero massimo di thread potrebbero essere sfalsate le percentuali delle tipologie rispetto agli individui “attivi” nonostante la percentuale rispetto alla popolazione totale sia coerente con la simulazione.
L’ipotetica cittadina dove avviene la nostra “battaglia dei sessi” ha come rirtovo sociale un Hotel con all’interno un Bar dove le donne sono solite aspettare un drink offerto e gli uomini offrire da bere. L’incontro avviene in modo del tutto casuale in quanto la donna si siede al bancone (in coda) e l’uomo può offrire da bere esclusivamente alla prima donna in coda. Una volta che due individui si incontrano hanno un appuntamento. Se entrambi gli individui durante un appuntamento sono felici allora la donna ottiene un riferimento al cromosoma del partner con il quale genererà un figlio.
Implementare il modello genetico descritto precedentemente si è rivelato abbastanza semplice poiché non ha richiesto particolari accorgimenti. La classe Chromosome ha due campi di tipo Gene, che è una sottoclasse di Chromosome, e un campo di tipo Gender che indica il sesso dell’individuo e quindi il gene da “attivare” per estrarre il tipo. La classe Gene invece ha due campi di tipo primitivo char che rappresentano, appunto, i caratteri genetici che possono essere maiuscoli o minuscoli in base alla dominanza del gene. Inoltre la classe Chromosome conserva i riferimenti ai cromosomi dei genitori per poter estrarre un albero genealogico al termine della simulazione. Sia la classe Chromosome sia la classe Gene hanno un costruttore che prende in input due istanze della stessa classe e serve a semplificare il meccanismo di creazione di nuovi cromosomi con i geni ereditati dai genitori. Per semplicità un individuo creato (non generato durante una simulazione) avrà un cromosoma di default con solo un riferimento ad un gene (quello attivo).
Il codice Human h = new Human("M");
crea una nuova istanza di
Human con cromosoma ([MM][null])
mentre
Human h = new Human("S");
ritorna un individuo con cromosoma
([null][ss])
.
Il costruttore di Chromosome deve quindi prendere in considerazione anche l’eventualità che uno dei genitori sia stato generato. In quel caso l’intero gene attivo viene passato al figlio da entrambi i genitori:
public Chromosome(Chromosome father, Chromosome mother) {
this.father = father;
this.mother = mother;
if(father.femaleGene == null || mother.maleGene == null) {
this.maleGene = father.maleGene;
this.femaleGene = mother.femaleGene;
} else {
this.maleGene = new Gene(father.maleGene,mother.maleGene);
this.femaleGene = new Gene(father.femaleGene,mother.femaleGene);
}
if(Math.random()>.5) {
this.gender = Gender.MALE;
} else {
this.gender = Gender.FEMALE;
}
}
Mentre in tutti gli altri casi i geni sono creati da questo costruttore che fonde in maniera casuale i geni dei genitori:
public Gene(Gene g1, Gene g2) {
if(Math.random()>.5) {
this.x1 = g1.x1;
} else {
this.x1 = g1.x2;
}
if(Math.random()>.5) {
this.x2 = g2.x1;
} else {
this.x2 = g2.x2;
}
}
L’esecuzione del simulatore senza l’utilizzo di opzioni farà partire la simulazione con i valori di Dawkins e con una popolazione iniziale composta in egual misura da Morigerati, Avventurieri, Prudenti e Spregiudicate. L’output a video sarà prodotto solo alla fine della simulazione mostrando un grafico e varie informazioni utili ai test
|
| |
| |
| |
| |
| |
| |
| | |
| | |
| | |
| | |
| | |
| | | |
| | | |
| | | |
| | | |
A: 19,32% 102 M: 31,63% 167 P: 40,72% 215 S: 8,33% 44
Threads A: 0 Threads M: 0 Threads P: 0 Threads S: 0
population: 528
Threads: 0
Active Humans: 445
M/A+M: 0,62 P/S+P: 0,83 Avg gain: A: 2,55 M: 2,51 P: 1,24 S: 1,21
Male: 2,52 Female: 1,24
Exiting...
Simulation time: 00:14
La simulazione è durata 14 secondi ed ha raggiunto la stabilità sperata:
Il numero di thread attivi è zero perché la simulazione è terminata. Da notare il guadagno medio che è molto simile tra gli uomini e tra le donne, indice di stabilità dello stato raggiunto.
È possibile impostare il simulatore da riga di comando usando le varie
opzioni (per ottenere un elenco completo lanciare il programma con
l’opzione --help
).
L’opzione --verbose
se presente tra gli argomenti passati alla classe
principale permette all’utente di seguire in tempo reale la simulazione
tramite output a schermo.
|
| |
| |
| |
| |
| |
| |
| |
| |
| | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
A: 35,09% 100 M: 16,84% 48 P: 33,33% 95 S: 14,74% 42
Threads A: 88 Threads M: 48 Threads P: 60 Threads S: 31
population: 285
Threads: 229
Active Humans: 223
M/A+M: 0,32 P/S+P: 0,69 Avg gain: A: 4,6 M: 2,92 P: 0,65 S: -1,76
Male: 4,05 Female: -0,09
0% 100%
************************************************************
La barra del caricamento in basso mostra i passaggi evolutivi ancora necessari prima di mostrare un nuovo stato.
Grazie all’opzione -p
è possibile impostare una popolazione iniziale
diversa da quella di default. È necessario inserire quattro numeri
interi dopo -p
per inizializzare una popolazione con le percentuali
passate.
Lanciare il programma con il comando java Simulator
è equivalente a
lanciarlo con java Simulator -p 25 25 25 25
in quanto le proporzioni
di default sono M=25%,A=25%,P=25% e S=25%.
Il comando java Simulator -p 30 20 45 5
inizializzerà una popolazione
iniziale composta così: M=30%,A=20%,P=45% e S=5%.
Con questa opzione è possibile settare i parametri a, b e c.
Insieme all’opzione -m
è possibile visualizzare i valore della tabella
MAPS e la stabilità attesa senza far partire la simulazione.
Il comando java Simulator -abc 12 15 2 -m
produce come output:
P gains 0 with A
P gains 3 with M
A gains 0 with P
A gains 12 with S
S gains -3 with A
S gains 5 with M
M gains 3 with P
M gains 5 with S
M/M+A: 0.6
P/P+S: 0.7
La meccanica di nascita di nuovi individui seguendo le regole della
genetica ha prodotto la necessità di estrapolare i dati per osservare
l’evolversi dei singoli geni attraverso le generazioni. L’opzione -g
rende queste informazioni osservabili stampando un albero genealogico di
generazioni in base all’intero passato dopo l’opzione. È possibile
anche passare dopo il numero di generazioni un booleano per attivare il
pretty print della classe TreePrinter gentilmente offerta
dall’utente MightyPork (Ondřej Hruška) sul sito
stackoverflow.com1
Il comando java Simulator -g 3
produce un output simile al seguente:
| | |---P([Ma][pp])
| |---P([aM][pp])
| | |---A([aa][pp])
|---P([aa][pp])
| | |---P([aa][pp])
| |---A([aa][pp])
| | |---A([aa][pS])
P([aa][pp])
| | |---P([Ma][pp])
| |---P([MM][pp])
| | |---M([Ma][pp])
|---M([aM][pp])
| | |---P([aM][pp])
| |---M([aM][pp])
| | |---A([aa][pp])
Il comando java Simulator -g 3 true
produce un output simile al
seguente:
A([aa][pp])
┌─────────────────────────────┴─────────────────────────┐
M([Ma][pp]) P([aa][pp])
┌─────────────┴─────────────┐ ┌─────────────┴─────────────┐
M([aM][pp]) S([aa][Sp]) A([aa][pp]) P([Ma][pp])
┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐
A([aa][pp]) P([Ma][pp]) A([aa][Sp]) P([aa][pp]) A([aa][pp]) P([aa][pp]) M([Ma][pp]) P([aa][pp])
Il simulatore è stato testato con numerosi dati di input e si è rilevato abbastanza affidabile nel complesso. Tuttavia alcuni dati di input non portano a risultati corretti.
Impostiamo i valori a=30, b=50 e c=4 che ci danno la seguente tabella MAPS:
M | A | |
---|---|---|
P | ||
S |
I valori che le formule (1) e (2) ci forniscono sono e .
Due valori così alti però innescano un meccanismo non voluto nel quale Prudenti e Avventurieri continuano ad essere felici e generare figli finché che è di fatto la condizione di felicità dei Morigerati. A questo punto il grandissimo numero di Avventurieri presente nella popolazione porta l’estinguersi della già esigua popolazione di Spregiudicate prima che i Morigerati possano raggiungere la stabilità.
Il problema più grave del modello adottato è sicuramente il calcolo della felicità di un individuo che è legato allo stato della popolazione e non alle esperienze dei singoli individui. In una precedente formulazione del modello infatti la felicità di un individuo era ricavata da incontri passati. Tale approccio, però, si è rivelato troppo poco accurato in quanto nel momento in cui il singolo individuo completava la raccolta dei dati relativi ai suoi incontri, tali dati risultavano già “obsoleti” rispetto all’evoluzione repentina del totale della popolazione. Non escludiamo che si possa migliorare tale dinamica implementando un sistema che possa attenuare questa differenza rendendo il simulatore maggiormante corretto da un punto di vista genetico e biologico.