#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Mon Dec 13 17:46:38 2021

@author: puudeli
"""

# import random
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.widgets import Slider, Button
import itertools as itr

from Tilasto import tilasto
import paramsPandemia as pars
import Fantasmi


Piirretään fantasmit 'kartalle', piirretään väestön tilaa ajan funktiona ja rakennetaan 'ohjauskeskus' toimenpiteiden ohjaamista varten.


markers = {'A': 'o', 'B': 'v', 'C': 'D', 'D': 's'}
colors = {'altis': 'green', 'oireeton': 'salmon', 'sairas': 'red',
          'karanteenissa': 'firebrick',
          'immuuni': 'deepskyblue', 'rokotettu': 'lime', 'kuollut': 'silver'}


Luokkaan Kartta kuuluva objekti piirtää näytölle fantasiamaailman olioiden liikkeet ja kunkin olion sairauden tilan. Lisäksi se piirtää käyrinä tilastotietoa fantasiamaailman väestön tautitilanteesta.

Kuva kunkin päivän tilanteesta talletetaan tiedostoon. Simuloinnin jälkeen ne kootaan videoksi seuraavalla komentotiedostolla Pääohjelma


class Kartta_c:

Taiteilijakielellä voisi sanoa, että alla viritetään kangas kuvan maalaamista varten.

Lisäksi viritetään maalaukseen viereen kojetaulu simuloinnin ohjaamiseen

Piirtelykomennot ovat mystisiä. Minulta kului toinenkin päivä googlaillessa, miten kuvia piirrellään, vaikka suurinpiirtein tiesin, mitä hakea.

Kun kyllästyin käsin ohjaamisen, jätin jäljelle vain osan toiminnoista. Sangen sotkuista nyt. Sorry siitä.


    def __init__(self):
        # Maailman koko näytöllä pikseleinä
        x_max, y_max = pars.dim
        self.loppu = False
        # ohjaustapahtumat: testaus, liikkumisen jarruttaminen, rokotukset
        self.tapahtumat = [[pars.x_max, 'alku', 0]]

        plt.ioff()
        plt.rcParams.update({'font.size': 14})
        # Tällä syntyy näytölle 'kangas'/ikkuna
        # (x_max, y_max) pikseliä, jolle piirrellä
        # dpi = dots per inch
        fig_siz = [int(x_max/100.0), int(y_max/100.0)]
        self.fig, self.ax1 = plt.subplots(1, figsize=fig_siz,
                                          dpi=100, tight_layout=False)
        self.ax2 = self.ax1.twinx()

        # Naksautetaan ikkuna fullscreen moodiin
        manager = plt.get_current_fig_manager()
        manager.full_screen_toggle()

        # Tehdään tilaa liukusäätimille ja ohjausnappuloille
        #  näytön oikeaan laitaan.
        sli_x0 = 0.85
        sli_x1 = 0.9
        sli_y0 = 0.4
        sli_w = 0.015
        plt.subplots_adjust(left=0.02, right=sli_x0, bottom=0.02, top=0.95)


Seuraava pätkä on sotkuista. Vaihdoin käsiohjauksesta automaattiohjaukseen. Olisi pitänyt koodata lisää, että sekä käsi- että automaattiohjaus olisivat mahdollisia, mutta en jaksanut vaan kommentoin ylimääräisen pois odottamaan mahdollista inspiraatiota.

Ohjausnappuloita ja liukusäätimiä piirtämään ja hoitamaan luodaan ensin 'artistit' plt.axes()-komennoilla. (Dokumentaatiossa kirjoittavat artisteista ja axes kommennoista samassa lauseessa. En ymmärrä sen enempää.)

Button ja slider -komennoilla piirretään ohjaimet.

on_clicked ja on_changed -komennoilla kerrotaan, mikä funktio suoritetaan, kun ohjaimiin kosketaan.


        # Liukusäädin fantasmien nopeuden säätämiseen
        self.v_ax = plt.axes([sli_x1, sli_y0,
                              sli_w, 1.0-sli_y0-4.0*sli_w])

        # Liukusäädin testausten määrään ohjailemiseen
        self.testi_ax = plt.axes([sli_x1+3.0*sli_w, sli_y0, sli_w,
                                  1.0-sli_y0-4.0*sli_w])

        # Nappula, jota painamalla kaupungille tupsahtaa
        # itämisvaiheessa oleva taudinkantaja
        # infect_ax = plt.axes([sli_x1, 0.04 + 3*sli_w, 3.5*sli_w, 2.0*sli_w])
        # self.infect = Button(
        #     infect_ax, '*', hovercolor='magenta')

        # Nappula simuloinnin hallittuun pysäyttämiseen
        stop_ax = plt.axes([sli_x1, 0.04, 3.5*sli_w, 2.0*sli_w])
        self.stop = Button(stop_ax, 'stop', color='red')

        self.rokota_ax = plt.axes(
            [sli_x1, 0.04 + 6.0*sli_w, 3.5*sli_w, 2.0*sli_w])

        # Tällaisetkin vielä tarvitaan, että ohjelma reagoi, kun säätimiin
        # ja nappuloihin kosketaan
        # self.v_set.on_changed(self.upd_v)
        # self.testi_set.on_changed(self.upd_testi)
        # self.infect.on_clicked(self.infectf)
        self.stop.on_clicked(self.stop_simu)
        # self.rokota_b.on_clicked(self.rokotaf)

        self.ohjaimet()

    def ohjaimet(self):
        """
        Tällä automaattiohjaus päivittää liukusäädinten ja
        ohjausnappuloiden tilan.

        Päivitys hoituu automaattisesti, jos ohjaimiin kosketaan
        käsiohjauksella.
        """
        self.v_set = Slider(
            self.v_ax, label='liike', valinit=Fantasmi.ohjaus.jarru,
            valmin=0.0, valmax=1.0, orientation='vertical')
        self.testi_set = Slider(
            self.testi_ax, label='testaus', valinit=Fantasmi.ohjaus.testaus,
            valmin=0.0, valmax=1.0, orientation='vertical')
        if Fantasmi.ohjaus.rokota:
            vari = 'green'
        else:
            vari = 'gray'
        self.rokota_b = Button(self.rokota_ax, 'rokotus', color=vari)


Piirretään näytölle käppyröitä kertomaan fantasiamaailman väestön terveydentilasta.


    def historiat(self):
        self.ax2.plot(tilasto.paivat, tilasto.karanteenissa, zorder=1.0,
                      color='black', linestyle='dotted', linewidth=2,
                      label='karanteenissa')

        ab = [a+b for (a, b) in zip(tilasto.karanteenissa, tilasto.sairas)]
        self.ax2.plot(tilasto.paivat, ab, zorder=1.0,
                      color='darkviolet', linestyle='dotted', linewidth=2,
                      label='+ sairas vapaana')

        abc = [a+c for (a, c) in zip(ab, tilasto.alussa)]
        self.ax2.plot(tilasto.paivat, abc, zorder=1.0,
                      color='goldenrod', linestyle='dotted', linewidth=2,
                      label='+ itämisvaihe')
        self.ax2.legend(loc='lower left')
        self.ax2.set_ylim(ymin=0.0)

    def rajat(self):
        """
        Piirretään fantasmien kotinurkkausten rajat
        """
        self.ax1.add_patch(Rectangle(
            (0, 0), (0.5-pars.rajavyohyke)*pars.x_max,
            (0.5-pars.rajavyohyke)*pars.y_max,
            edgecolor='lightgray', linestyle='dashed', facecolor='none'))

        self.ax1.add_patch(Rectangle(
            (0, (0.5+pars.rajavyohyke)*pars.y_max),
            (0.5-pars.rajavyohyke)*pars.x_max,
            (0.5-pars.rajavyohyke)*pars.y_max,
            edgecolor='lightgray', linestyle='dashed', facecolor='none'))

        self.ax1.add_patch(Rectangle(
            ((0.5+pars.rajavyohyke)*pars.x_max, 0),
            (0.5-pars.rajavyohyke)*pars.x_max,
            (0.5-pars.rajavyohyke)*pars.y_max,
            edgecolor='lightgray', linestyle='dashed', facecolor='none'))

        self.ax1.add_patch(Rectangle(
            ((0.5+pars.rajavyohyke)*pars.x_max,
             (0.5+pars.rajavyohyke)*pars.y_max),
            (0.5-pars.rajavyohyke)*pars.x_max,
            (0.5-pars.rajavyohyke)*pars.y_max,
            edgecolor='lightgray', linestyle='dashed', facecolor='none'))


Piirretään fantasmit kartalle: sijainti, 'kansallisuus' ja taudin vaihe

Fantasmit voisi piirtää yksi kerrallaan näytölle, mutta on nopeampi piirtää kaikki samanlaiset yhdellä piirtokomennolla. Tätä varten joudun keräämään väestön ryhmiksi. Näyttänee hämärältä, mutta oli tosi mielenkiintoista löytää ratkaisu ryhmittelyyn.


    def kartalle(self, vaesto):
        legends_a = []
        legends_b = []
        vaesto.sort(key=kansa_tila)
        for kotipaikka, kansa in itr.groupby(vaesto, kansa_f):
            l_kansa = list(kansa)
            legends_b = []
            for vaihe, ryhma in itr.groupby(l_kansa, tila_f):
                lr = list(ryhma)
                axn = self.plottaa(kotipaikka, vaihe, lr)
                legends_b.append(axn)
            legends_a.append(legends_b)
        legends_a.sort(key=len, reverse=True)
        self.ax1.legend(handles=legends_a[0], loc='upper left')

    def plottaa(self, kotipaikka, vaihe, ryhma):
        """
        Läiskäistään kaikki yhdenlaiset fantasmit kartalla
        """
        xy = [nn.paikka for nn in ryhma]
        lx = [x for (x, y) in xy]
        ly = [y for (x, y) in xy]
        axx = self.ax1.scatter(
            lx, ly, c=colors[vaihe], marker=markers[kotipaikka], s=40,
            zorder=1.0, label=vaihe)
        if vaihe == 'oireeton':
            self.tartunnat_f(ryhma)
        return(axx)


Jotta pandemian etenemistä olisi helpompi seurata, väläytetään tartunnan sattuessa näytöllä oranssinpunaista tähteä.

Kun fantasmi N saa tartunnan, sijoitetaan N.uusiTartunta = 3

Alla piirretään tähti kartalle

Lopputulos: Tähti välähtää videolla kolmen kuvan ajan, ensin pienenä, sitten isompana ja lopuksi pienenä Jos videon nopeudeksi laitetaan 10 kuvaa sekunnissa, tähti välähtää 3/10 sekunnin ajan.

(Tässä kartta-modulissa piirretään yhtä päivää vastaavaa pandemian tilannetta. Lopuksi kuva talletaan tiedostoon ja simuloinnin päätyttyä kuvat laitetaan peräkkäin videoksi.)


    def tartunnat_f(self, ryhma):
        s_koko = (0, pars.suoja_etaisyys,
                  2.0*pars.suoja_etaisyys,
                  pars.suoja_etaisyys)
        sairastuneet = [sairastunut for sairastunut in ryhma
                        if sairastunut.uusiTartunta > 0]
        if len(sairastuneet) > 0:
            sairastuneet.sort(key=tartunta_f)
            for indx, tautiset in itr.groupby(sairastuneet, tartunta_f):
                tautiset = list(tautiset)
                lx = [sairastunut.paikka[0] for sairastunut in tautiset]
                ly = [sairastunut.paikka[1] for sairastunut in tautiset]
                self.ax1.scatter(lx, ly, c='orangered', marker='*',
                                 s=s_koko[indx])

            for sairastunut in sairastuneet:
                sairastunut.uusiTartunta -= 1

    def hautausmaa(self, vaesto):
        """
        Simuloinnin päätteeksi näytetään, minne pandemian uhrit tuupertuivat.
        """
        kuolleet = [fantasmi for fantasmi in vaesto
                    if fantasmi.tila == 'kuollut']
        kuolleet.sort(key=kansa_f)
        kansat = []
        for kotipaikka, kansa in itr.groupby(kuolleet, kansa_f):
            kansat.append((kotipaikka, list(kansa)))
        for i in range(100):
            for kotipaikka, kansa in kansat:
                self.plottaa(kotipaikka, 'kuollut', kansa)
            self.tallenna()
            tilasto.paiva += 1

    def infot(self):
        """
        tekstimuotoista tietoa näytölle
        """
        rokot_osuus = tilasto.N_rokotetut/tilasto.N_vaesto*100.0
        # ohjaus = f'ohjaus : {Fantasmi.ohjaus.vaihe} | '
        tartunnat = f' tartunnat:  {tilasto.N_tartunnat} | '
        rokotetut = f' rokotetut: {rokot_osuus:.1f}% | '
        kuolleet = f' kuolleet: {tilasto.N_kuolleet}'
        txt1 = tartunnat + rokotetut + kuolleet
        self.ax1.text(0.4*pars.x_max, 1.01*pars.y_max, txt1)
        txt2 = 'päivä: ' + str(tilasto.paiva)
        self.ax1.text(1.0*pars.x_max, 1.01*pars.y_max, txt2, ha='right')

        for [paikka, vaihe, paiva] in self.tapahtumat:
            txt = f'päivä: {paiva} \n {Fantasmi.selitykset[vaihe]}'
            self.ax1.text(paikka+5.0, 0.9*pars.y_max, txt, alpha=0.5)

    def tapahtumat_f(self):
        """
        Piirretään näytölle pystyviiva, kun ohjausautomaatin tila vaihtuu
        """
        for i in range(len(self.tapahtumat)):
            self.tapahtumat[i][0] -= pars.points_day

        for i in range(len(self.tapahtumat)):
            if self.tapahtumat[i][0] <= 0:
                self.tapahtumat.pop(i)
                break
        xx = [tapahtuma[0] for tapahtuma in self.tapahtumat]
        self.ax1.vlines(xx, 0, pars.y_max)

    def sijainnit(self, vaesto):
        self.kartalle(vaesto)
        self.tapahtumat_f()
        self.historiat()
        self.rajat()
        self.infot()
        if Fantasmi.ohjaus.muuttunut:
            Fantasmi.ohjaus.muuttunut = False
            self.ohjaimet()
            self.tapahtumat.append(
                [pars.x_max, Fantasmi.ohjaus.vaihe, tilasto.paiva])
        self.tallenna()

    def tallenna(self):
        self.ax1.set_xlim(xmin=0.0, xmax=pars.x_max)
        self.ax1.set_ylim(ymin=0.0, ymax=pars.y_max)
        self.ax1.axis('off')
        plt.pause(0.01)

        # Talletetaan kunkin päivän tilanne tiedostoon kuvaksi, joista voi
        # koota videon
        figno = '{:05d}'.format(tilasto.paiva)
        plt.savefig('/home/heikki/maalaus/fantasy21_' +
                    figno + '.png', format='png', optimize=True)
        # pyyhitään taulu puhtaaksi seuraavan päivän kuvaa varten
        self.ax1.cla()
        self.ax2.cla()

    def rokotaf(self, _):
        if self.rokota:
            self.rokota = False
            self.rokota_b.label = 'rokota'
            self.rokota_b.color = 'gray'
        else:
            self.rokota = True
            self.rokota_b.label = 'lopeta'
            self.rokota_b.color = 'green'

    '''
    Kommennot, jotka suoritetaan, kun liukusäätimiin tai nappuloihin kosketaan
    '''

    # def upd_v(self, val):
    #     Fantasmi.ohjaus.jarru = val

    # def upd_testi(self, val):
    #     Fantasmi.ohjaus.testaus = val

    def stop_simu(self, _):
        print('stopping ')
        self.loppu = True

    # def infectf(self, _):
    #     uusitapaus()

    def lopeta(self):
        plt.close('all')


# Tätä tarvitaan sortatessa vaestoa järjestykseen
# kotipaikan ja sairauden vaiheen mukaan
#         vaesto.sort(key=kansa_tila)
def kansa_tila(e):
    return (e.koti, e.tila)

# Järjestetään kotipaikan mukaan
# for kotipaikka, kansa in itr.groupby(vaesto, kansa_f):
def kansa_f(e):
    return e.koti

# Arvaa itse
def tila_f(e):
    return e.tila

def tartunta_f(e):
    return e.uusiTartunta

kartta = Kartta_c()