#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: Heikki Välisuo
"""


Yliluonnollisen järven virtausten simulointi

Tämä ohjelma ei käy esimerkiksi tyylikkäästä ohjelmoinnista. Olen selvitellyt pahimpia sotkujani sitä mukaa, kun olen niihin kompastellut. Olen tehnyt sen verran siistiä koodia, että minun on itse tätä melko helppo muutella tarpeen mukaan, mutta tämän lukeminen lienee vaikeaa. Ehkä en vuoden kuluttua itsekään ymmärrä, mitä tässä tapahtuu. Yrittäessäni korjailla omia vanhoja ohjelmiani, olen huomannut, että ohjelmia on helpompi kirjoittaa kuin lukea.


Ohjelmoinnissa on tapana kapseloida yhteen kuuluvia tietoja ja toimintoja olioiksi, joiden sisäisestä elämästä muun ohjelman ei tarvitse tietää mitään. Tyyppiä 'maalari' oleva olio maalaa simuloinnin jokaisella aika-askelella pinnankorkeuksista, virtauksista ja väriaineiden valumisesta kuvan.

Olion voin ajatella maalarin työpajaksi, jolle tarvittaessa lähetetään tiedot järven tilasta ja pyydetään sitä maalaamaan niistä havainnolliset kuvat.

Piirtelyyn käytin matplotlib.pyplot-modulia. Ei ollut helppo oppia sitäkään käyttämään.


import sys
import datetime as tim
import random
import numpy as np
import math
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import dfdt_virtaukset as df
import params as par
import saaret as ly


class Cl_Maalari:

    # 'figsize=[12.8, 7.2], dpi=100' tuottaa 1280x720 pixelin kuvia,
    # joista saa helposti koottua vastaavan resoluution videon

    def __init__(self, paint):
        plt.ioff()
        self.fig, self.taulu = plt.subplots(
            1, 2, figsize=[12.8, 7.2],
            dpi=100, tight_layout=True)
        self.taulu[0].set_aspect('equal')
        self.taulu[1].set_aspect('equal')

        self.x = np.full(ly.DIM_j, 0.0)
        self.y = np.full(ly.DIM_i, 0.0)
        for j in range(0, ly.DIM_j):
            self.x[j] = par.ds*j
        for i in range(0, ly.DIM_i):
            self.y[i] = par.ds*i
        self.max_speed = 0.0
        self.min_paint = np.min(paint)
        self.max_paint = np.max(paint)
        self.kuvadir = sys.argv[1]
        print('kuvadir ', self.kuvadir)

    def sys_plot(self, rho, u, v, paint, n):
        min_rho, max_rho = np.min(rho), np.max(rho)

        # pinnankorkeus. Logaritminen asteikko tuo esiin sekä
        # ison konsentraation roiskeet, että diffuusion aiheuttaman
        # värien hitaan hiipumisen.
        self.taulu[0].pcolormesh(rho, cmap='RdYlGn_r',
                                 norm=colors.SymLogNorm(
                                     linthresh=0.01, base=10))

        # virtausnuolet, väri hetkellisen nopeuden mukaan,
        # paksuus veden virtauksen mukaan
        speed = np.sqrt(u**2 + v**2)
        self.max_speed = np.max(speed)
        if self.max_speed > 0.01:
            q = rho*speed  # nesteen virtaus
            q_max = np.max(q)
            sp_lw = 4.0*q/q_max + 0.1
            # clr = (131/255.0, 185/255.0, 0)  color=clr)

            self.taulu[0].streamplot(self.x, self.y, u, v,
                                     color=speed, cmap='coolwarm',
                                     linewidth=sp_lw)

        # väriaineiden konsentraatio
        self.taulu[1].pcolormesh(
            paint, cmap='RdYlGn',
            vmin=par.pnt_cut*self.min_paint,
            vmax=par.pnt_cut*self.max_paint)
        # norm=colors.SymLogNorm(linthresh=1.0, base=10))

        # Näin voi zoomata
        # taulu[1].pcolormesh(rho[80:105, 20:60], cmap='plasma_r')

        s1 = 'pinnankorkeus {:.4g} -- {:.4g}; '
        s2 = '  suurin nopeus: {:.4g};  aika: {:4.1f}'
        txt = self.fig.text(0.4, 0.98, (s1+s2).format(
            min_rho, max_rho, self.max_speed, n*par.dt,
            fontsize='x-large', backgroundcolor='white'))
        self.taulu[0].axis('off')
        self.taulu[1].axis('off')

        # Kuva päivitetään joka simulointiaskelella.
        # Ei onnistu ilman pikku pausea
        plt.pause(0.01)
        # Tallennetaan kuva videon tekemistä varten
        figno = '{:05d}'.format(n)
        plt.savefig(self.kuvadir + '/taulu_'+figno+'.png', format='png')
        # plt.show(block=False)

        # Tyhjennetään kuvien sisältö. Muuten uusi kuva tulee vanhan päälle
        self.taulu[0].cla()
        self.taulu[1].cla()
        txt.remove()



Allaolevaan funktiota ei tarvita tässä ohjelmassa, mutta laitoin sen muistutukseksi siitä, että kannattaa olla perillä siitä, miten funktioiden parametrit välittyvät pythonissa.

(Eli siis tein aluksi muutaman vaikeasti jäljitettävän virheen.) Läheskään kaikista webissä olevista selityksistä ei selviä olennainen, mutta allaolevaa funktiota kokeilemalla selviää koko totuus.


def test_swap():
    a = np.full((2, 2), 1.0)
    b = np.full((2, 2), 2.0)
    print(a, '\n', b, '\n\n')

    a, b = b, a
    print(a, '\n', b, '\n\n')

    a = b
    print(a, '\n', b, '\n\n')

    a[1, 1] = 100.0
    print(a, '\n', b, '\n\n')



Sovelsin liiankin paljon paljon yritys-erehdys -menetelmää. Onneksi montaa toimintoa sai testattua piirtämällä kuvan sen tuloksesta.

Alla apufunktiota piirtelyyn. Ohjelmoitu quick-and-dirty copy-paste -menetelmällä



def test_plt1(a, text):
    plt.matplotlib.use('qtagg')
    plt.ion()

    fg, kuva = plt.subplots(1, 1, tight_layout=True)
    plt.axis('on')
    major_ticks = np.arange(0, ly.DIM_i+1, 10)
    minor_ticks = np.arange(0, ly.DIM_i+1, 5)
    kuva.set_aspect('equal')
    kuva.grid(which='both')
    kuva.set_xticks(major_ticks)
    kuva.set_xticks(minor_ticks, minor=True)
    kuva.set_yticks(major_ticks)
    kuva.set_yticks(minor_ticks, minor=True)

    kuva.pcolormesh(a, cmap='inferno')  # , cmap='winter'
    fg.text(0.45, 0.97, text, fontsize='x-large', backgroundcolor='white')
    # kuva[0].pcolormesh(a[0:20, 20:60])
    # kuva[1].pcolormesh(b[30:90, 60:100])
    plt.savefig(text+'.png', format='png')
    # plt.show(block=True)


def test_plt(a, kartta, text):
    print(text, a.shape)
    print(text, kartta.shape)
    fg, kuva = plt.subplots(1, 1, tight_layout=True)
    plt.axis('on')

    major_ticks = np.arange(0, ly.DIM_i+1, 10)
    minor_ticks = np.arange(0, ly.DIM_i+1, 5)

    kuva.set_aspect('equal')
    kuva.grid(which='both')
    kuva.set_xticks(major_ticks)
    kuva.set_xticks(minor_ticks, minor=True)
    kuva.set_yticks(major_ticks)
    kuva.set_yticks(minor_ticks, minor=True)

    kuva.pcolormesh(a)  # , cmap='Spectral_r'
    kuva.pcolormesh(kartta, alpha=0.1)
    fg.text(0.45, 0.97, text, fontsize='x-large', backgroundcolor='white')
    # kuva[0].pcolormesh(a[0:20, 20:60])
    # kuva[1].pcolormesh(b[30:90, 60:100])
    plt.savefig(text+'.png', format='png')
    # plt.show(block=False)


def test_plt2(a, b, kartta, text):
    fg, kuva = plt.subplots(1, 1, tight_layout=True)
    plt.axis('on')

    major_ticks = np.arange(0, ly.DIM_i+1, 10)
    minor_ticks = np.arange(0, ly.DIM_i+1, 5)

    kuva.set_aspect('equal')
    kuva.grid(which='both')
    kuva.set_xticks(major_ticks)
    kuva.set_xticks(minor_ticks, minor=True)
    kuva.set_yticks(major_ticks)
    kuva.set_yticks(minor_ticks, minor=True)

    x = np.full(ly.DIM_j, 0.0)
    y = np.full(ly.DIM_i, 0.0)
    for j in range(0, ly.DIM_j):
        x[j] = par.ds*j
    for i in range(0, ly.DIM_i):
        y[i] = par.ds*i

    speed = np.sqrt(a**2 + b**2)
    max_speed = np.max(speed)
    kuva.streamplot(x, y, a, b, linewidth=4.0*speed/max_speed,
                    color="green")  # , cmap='Blues')

    kuva.pcolormesh(kartta, alpha=0.1)

    fg.text(0.45, 0.97, text, fontsize='x-large', backgroundcolor='white')
    plt.savefig(text+'.png', format='png')
    # plt.show(block=False)



Geometrinen pähkinä:

Koski on ympyrän, jonka keskipiste on (i0, j0) sektorista [kulma0, kulma1] siivu, jonka säde on välillä [r0, r1]. (Mikä mahtaa olla tuollaisen siivun virallinen matemaattinen nimi?)

Koski aiheuttaa ympyrän kehän tangentin suuntaisen voiman F. Koskien aiheuttamat voimat esitetään kahtena NxN taulukkona, joista toisessa x-suuntaiset, toisessa y-suuntaiset voimien komponentit. Tee funktio tätä varten

Lisäksi olisi kiva kallistaa koskea sisäkarteeseen päin, eli lisätä pieni voima, joka osoittaa kohti ympyrän keskipistettä.

(Differentiaaliyhtälöiden numeerisen ratkaisu onnistuu helpommin, kun kaikki on pehmeän pyöreää. Matemaattisesti ilmaisten, kiva, jos funktiot olisivat jatkuvasti derivoituvia. Siksi olisi kiva, jos koski olisi joka suuntaan 'pyöreä'. Jos kosken niskalta voima kasvaa lineaarisesti, koski kääntyy laskuun pyöreästi. ???

Alla on typerä brute-force -ratkaisu. Koska tämä funktio suoritetaan vain kerran jokaisella koskella, koodin tehottomuus ei haittaa, mutta nolottaa kyllä.


def koski(Fe_u, Fe_v, i0, j0, kulma0, kulma1, r0, r1, suunta, tilt, Fe):
    rsteps = 200
    ksteps = 200
    d_kulma = (kulma1 - kulma0)/ksteps
    dr = (r1-r0)/rsteps
    slope1 = 0.2*ksteps
    slope2 = 0.8*ksteps
    rslope1 = 0.3*rsteps
    rslope2 = 0.7*rsteps

    r = r0
    for mr in range(0, rsteps):
        if mr < rslope1:
            rscale = mr/rslope1
        elif mr > rslope2:
            rscale = (rsteps-mr)/(rsteps-rslope2)
        else:
            rscale = 1.0
        kulma = kulma0
        for k in range(0, ksteps):
            if k < slope1:
                scale = k/slope1
            elif k > slope2:
                scale = (ksteps-k)/(ksteps-slope2)
            else:
                scale = 1.0
            i = int(math.sin(kulma)*r)
            j = int(math.cos(kulma)*r)
            F = scale*rscale*Fe
            if suunta == 'pos':
                Fe_u[i0+i, j0+j], Fe_v[i0+i, j0+j] = \
                    rotate(F, kulma + np.pi/2.0 + tilt)
            else:
                Fe_u[i0+i, j0+j], Fe_v[i0+i, j0+j] = \
                    rotate(F, kulma - np.pi/2.0 - tilt)
            kulma += d_kulma
        r += dr




Käännetään voimavektori 'kosken' kaarteen suuntaiseksi. Kokeilin satunnaisuuden lisäämistä 'koskiin', mutta tuntui turhalta, joten laitoin kommentiksi


def rotate(R, alfa):
    Cu = 1.0  # random.uniform(0.95, 1.05)
    Cv = 1.0  # random.uniform(0.95, 1.05)
    return Cu*R*np.cos(alfa), Cv*R*np.sin(alfa)



Alla koodissa on 'r1 = 38.0' -tyyppisiä lausekkeita. Yritys-ja-erehdys vaiheessa voi tilapäisesti käyttää tuollaisia häkkeröintejä, mutta jos niitä jää koodiin, koodia on tosi työläs muunnella. Ja muunneltavuus on välttämätöntä.

Ohjelmassani järven koko on NxN ruutua. Mitä suurempi N, sitä tarkempi ratkaisu ja sitä jyrkempiä virtausten mutkia se kykenee ratkaisemaan. Mutta, mitä suurempi N, sitä kauemmin laskenta kestää.

Tätä kirjoittaessani ohjelmani tuottaa taustalla videota varten kuvia noin kuvan sekunnissa. Olisi kiva pienentää N:ää ja kokeilla riittääkö tarkkuus. Vaan mitä kirjoittaa lausekkeen 'r1 = 38.0' tilalle? Miten edes löytäisin kaikki korjaamista tarvitsevat lausekkeet?

'Quick-and-dirty' on dirty, mutta ei quick

Ennen kuin kehitän ohjelmaani pidemmälle, nykyiset tilapäiset häkkäykset pitäisi korjata. Tuota miettiessä projekti alkaa tuntua riittävän valmiilta jo nyt.



def kosket(**kwargs):

    Fe_u = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    Fe_v = np.full((ly.DIM_i, ly.DIM_j), 0.0)

    # ellipsit
    i_off = 10
    j_off = 10
    r0 = 30.0
    r1 = 45.0
    tilt = 0.2
    koski(Fe_u, Fe_v, ly.E_I - i_off, ly.E_J - j_off,
          3.0/4.0*math.pi, 7.0/4.0*math.pi,
          r0, r1, 'pos', tilt, par.Fe)
    koski(Fe_u, Fe_v, ly.E_I - i_off, ly.DIM_j - ly.E_J + j_off,
          -3.0/4.0*math.pi, 1.0/4.0*math.pi,
          r0, r1, 'pos', tilt, par.Fe)
    koski(Fe_u, Fe_v, ly.DIM_i - ly.E_I + i_off, ly.DIM_j - ly.E_J + j_off,
          -1.0/4.0*math.pi, 3.0/4.0*math.pi,
          r0, r1, 'pos', tilt, par.Fe)
    koski(Fe_u, Fe_v, ly.DIM_i - ly.E_I + i_off, ly.E_J - j_off,
          1.0/4.0*math.pi, 5.0/4.0*math.pi,
          r0, r1, 'pos', tilt, par.Fe)

    # keskiympyrä
    #
    # parametrillä 'vastapyorre' voi valita, 'laskeeko' keskellä
    # kiertävä koski myötä- vai vastapäivään
    r0 = 45.0
    r1 = 60.0
    if kwargs['vastapyorre']:
        Fey = -par.Fey
        tilt = -0.1
    else:
        Fey = par.Fey
        tilt = 0.1
    koski(Fe_u, Fe_v, ly.DI_2, ly.DJ_2,
          -1.0/8.0*math.pi, 1.0/8.0*math.pi,
          r0, r1, 'pos', tilt, Fey)
    koski(Fe_u, Fe_v, ly.DI_2, ly.DJ_2,
          7.0/8.0*math.pi, 9.0/8.0*math.pi,
          r0, r1, 'pos', tilt, Fey)
    koski(Fe_u, Fe_v, ly.DI_2, ly.DJ_2,
          3.0/8.0*math.pi, 5.0/8.0*math.pi,
          r0, r1, 'pos', tilt, Fey)
    koski(Fe_u, Fe_v, ly.DI_2, ly.DJ_2,
          11.0/8.0*math.pi, 13.0/8.0*math.pi,
          r0, r1, 'pos', tilt, Fey)
    return Fe_u, Fe_v


def test_kosket():
    a, b = kosket(vastapyorre=True)
    n_kartta = test_kartta()
    test_plt(a, n_kartta, 'kosket_u')
    test_plt(b, n_kartta, 'kosket_v')
    test_plt2(a, b, n_kartta, 'kosket')



Koskien voimakenttä
Koskien x-suuntaiset voimat
Koskien y-suuntaiset voimat

Koskien voimien ohjelmointia vastaava, mutta helpompi tehtävä: Pyöristä järven kulmat, merkitsemällä R-säteisen ympyrän ulkopuolelle jäävä alue maaksi (= 'saari').

Käydään läpi järven nurkkaan piirretyn R-sivuisen neliön sisään jäävät pisteet ja merkitään maaksi ne, joiden etäisyys järvelle jäävästä neliön sivusta on > R. (Kyllä, jokaisesta geometrisesta ongelmasta kannattaa piirtää kuva)

Olisi tyylikästä ohjelmoida myös 'koski'-funktio tällä periaatteella, mutta en jaksa korjailla.



def kulmatf(kartta, R):
    i0 = R
    j0 = R
    for i in range(0, i0+1):
        for j in range(0, j0+1):
            if (i0-i)**2 + (j0-j)**2 > R**2:
                kartta[i, j] = 'saari'
    i0 = R
    j0 = ly.DIM_j - R
    for i in range(0, i0+1):
        for j in range(j0-1, ly.DIM_j):
            if (i0-i)**2 + (j0-j)**2 > R**2:
                kartta[i, j] = 'saari'
    i0 = ly.DIM_i - R
    j0 = ly.DIM_j - R
    for i in range(i0-1, ly.DIM_i):
        for j in range(j0-1, ly.DIM_j):
            if (i0-i)**2 + (j0-j)**2 > R**2:
                kartta[i, j] = 'saari'
    i0 = ly.DIM_i - R
    j0 = R
    for i in range(i0-1, ly.DIM_i):
        for j in range(0, j0+1):
            if (i0-i)**2 + (j0-j)**2 > R**2:
                kartta[i, j] = 'saari'



Kartta on taulukko, joka kertoo, onko ruutu maata, vettä vai rantaa.

ly.saaret on työkaluni tuottama lista saarien määrittelyistä: nimi, keskipiste, muoto ja asento

ly.saarella on työkaluni tuottama funktio, joka kertoo, onko ruutu saarta kuvaavan ellipsin sisällä.




def init_kartta():
    kartta = np.full((ly.DIM_i, ly.DIM_j), 'jarvi', dtype='U5')
    for saari in ly.saaret:
        (name, (i0, j0), w, h, kulma) = saari
        # skannataan varmuuden vuoksi koko järvi, vaikka riittäisi
        # skannata suorakaide, jonka sisällä ellipsi on
        for i in range(1, ly.DIM_i-1):
            for j in range(1, ly.DIM_j-1):
                if ly.saarella(i, j, i0, j0, w, h, kulma) <= 0:
                    kartta[i, j] = 'saari'
    # leikataan 'piikit'
    # Pienen ympyrän kylkeen saattaa jäädä yksittäinen ruutu
    # markkeeraamaan pyöreyttä.
    # (Yo. kommentti on vain tulevaisuuden minulle tiedoksi :-)
    for i in range(1, ly.DIM_i-1):
        for j in range(1, ly.DIM_j-1):
            if kartta[i, j] == 'saari':
                if kartta[i-1, j] == 'jarvi' and kartta[i+1, j] == 'jarvi':
                    kartta[i, j] = 'jarvi'
                elif kartta[i, j-1] == 'jarvi' and kartta[i, j+1] == 'jarvi':
                    kartta[i, j] = 'jarvi'
    # järven rannat merkataan maaksi, jotta saadaan
    # reuna-ehdot toimimaan
    kartta[0:ly.DIM_i, 0] = 'saari'
    kartta[0:ly.DIM_i, 1] = 'saari'
    kartta[0:ly.DIM_i, ly.DIM_j-1] = 'saari'
    kartta[0:ly.DIM_i, ly.DIM_j-2] = 'saari'
    kartta[0, 0:ly.DIM_j] = 'saari'
    kartta[1, 0:ly.DIM_j] = 'saari'
    kartta[ly.DIM_i-1, 0:ly.DIM_j] = 'saari'
    kartta[ly.DIM_i-2, 0:ly.DIM_j] = 'saari'
    # Ei tehdä suorakulmaista järveä.
    # Pyöristetään nurkat.
    kulmatf(kartta, 45)
    # Merkitään karttaan rannat
    reunapisteet(kartta)
    return kartta

'''
    Koska järven rannoilla pitää laskennassa ottaa huomioon reunaehdot,
    merkitään rannat karttaan.
    Jos ruutu on saarella, mutta jokin sen viereisistä ruuduista järvellä,
    merkitään ruutu rannaksi.
'''

def reunapisteet(kartta):
    for i in range(1, ly.DIM_i-1):
        for j in range(1, ly.DIM_j-1):
            if kartta[i, j] == 'saari':
                rannallako(kartta, i, j)


def rannallako(kartta, i, j):
    if kartta[i, j+1] == 'jarvi':
        kartta[i, j] = 'ranta'
    elif kartta[i, j-1] == 'jarvi':
        kartta[i, j] = 'ranta'
    elif kartta[i-1, j] == 'jarvi':
        kartta[i, j] = 'ranta'
    elif kartta[i+1, j] == 'jarvi':
        kartta[i, j] = 'ranta'


Tätä ei ole helppo selittää.

Virtausten ja pinnankorkeuden muutokset riippuvat viereisten kuutioiden tilasta. Entä jos viereiden kuutio onkin maalla?

Hakukoneilemalla 'fluid mechanics boundary layer' löysin toinen toistaan matemaattisempia selityksiä, miten ottaa rannat huomioon. En jaksanut perehtyä, mutta pari huomiota jäi mieleeni.

Rannan tuntumaan voi ajatella vyöhykkeen, jossa vesi ei virtaa rannan suuntaisesti eikä järvelle tai sieltä rantaan päin. Oikeassa järvessä aallot loiskuvat rantaan, mutta keskimäärin vesi ei nouse eikä laske, eli ei ole nettovirtausta.

Sattui niin kivasti, että tämän ohjelmani aluksi nollaan taulukot, joissa ylläpidetään tietoa virtauksista, eikä taulukoita sen jälkeen päivitetä siltä osin kuin ruudut ovat maalla.

Paine — tässä tapauksessa pinnan korkeus — on kiperämpi juttu. Kirjaviisauksien mukaan paine-eron kapean rantavyöhykkeen yli pitäisi olla nolla. Loogista. Yhtälöt eivät erota maata vedestä, mutta ymmärtävät, että kun paine rajan kahden puolen on sama, ei siitä mennä yli. Eli siis paine nollaksi, eli pinnankorkeudet samoiksi. Samoiksi missä? Välille 'ranta' — 'saari'

Rantavyöhyke. Lila on vettä, oranssi saaren rantaa, jonne kopion pinnankorkeuden arvot.

Homma hoituu vaikkapa kopioimalla pinnankorkeuden arvot rantavyöhykkeeltä ruudun verran 'sisämaahan'. Paitsi kuvan ongelmaa: Samaan rutuun pitäisi saada kaksi eri pinnankorkeuden arvoa.

Minulla on ollut monta loistavaa ideaa, miten ratkaista yo. ongelma ja samalla sekunnilla, kun olen idean saanut, olen alkanut sitä myös ohjelmoida.

Mikään kuningasajatuksistani ei ole tuottanut kunnollista lopputulosta ja tuloksen nähtyäni minun oli helppo ymmärtää, miksei idea ollutkaan kovin hyvä. Hetken harkinta olisi pelastanut paljolta turhalta työltä.

Yksi ideoistani oli kirjoittaa alusta saakka erikseen yhtälöt rantavyöhykkeelle. Sekin tuotti 'seisovia aaltoja' laajalle alueelle. Ehkä rosoreunainen ranta tuottaa oikeasti tuollaisen ilmiön. Onneksi näin taidetta tehdessä tuo ei haittaa.

Kuvan aallot eivät johtune fysiikan laeista vaan siitä, etten viitsinyt miettiä loppuun saakka, millainen reuna-ehto rannoille pitäisi koodata.

Seuraavat kolme funktiota kopioivat kuvan lilalta vyöhykkeeltä pinnankorkeuden arvoja oranssille vyöhykkeelle.


def rantavedet(kartta):
    rannat = []
    for i in range(1, ly.DIM_i-1):
        for j in range(1, ly.DIM_j-1):
            if kartta[i, j] == 'ranta':
                viereiset = []
                # for (m, n) in [(i-1, j), (i+1, j), (i, j-1), (i, j+1)]:
                for m in range(i-1, i+2):
                    for n in range(j-1, j+2):
                        if kartta[m, n] == 'jarvi':
                            viereiset.append((m, n))
                if len(viereiset) > 0:
                    rannat.append(((i, j), viereiset))
    return rannat


def tasaa_paineita(rho, rannat):
    for (p0, viereiset) in rannat:
        rho[p0] = keskiarvo(p0, rho, viereiset)


def keskiarvo(p0, rho, viereiset):
    N = float(len(viereiset))
    if N > 0:
        x = 0
        for viereinen in viereiset:
            x += rho[viereinen]
        return x/N
    else:
        return rho[p0]


def test_kartta():
    tmp = np.full((ly.DIM_i, ly.DIM_j), -1.0)
    kartta = init_kartta()
    rannat = rantavedet(kartta)
    for i in range(0, ly.DIM_i):
        for j in range(0, ly.DIM_j):
            if kartta[i, j] == 'jarvi':
                tmp[i, j] = -2.0
            elif kartta[i, j] == 'ranta':
                tmp[i, j] = 1.0
            elif kartta[i, j] == 'saari':
                tmp[i, j] = 2.0
    for (po, viereiset) in rannat:
        for p1 in viereiset:
            tmp[p1] = -1.0
    test_plt1(tmp, 'rannat')
    return tmp


Luodaan taulukko väriaineille

Jos parametri maasto=True, maat ja vedet peitetään alussa satunnaisesti levitetyillä väriaineilla.

Toinen vaihtoehto on roiskia simuloinnin alussa maisemaan pieniä yksittäisiä väriläiskiä alempana olevalla funktiolla.


def init_paint(paint):
    # paint = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    # if kwargs['maasto']:
    tau = 0.97
    pnt0 = par.pnt_0
    paint[0, 0] = 0.0  # random.uniform(-pnt0, pnt0)
    for i in range(1, ly.DIM_i):
        paint[i, 0] = tau*paint[i-1, 0] + \
            (1.0-tau)*random.uniform(-pnt0, pnt0)
    for j in range(1, ly.DIM_j):
        paint[0, j] = tau*paint[0, j-1] + \
            (1.0-tau)*random.uniform(-pnt0, pnt0)
    for i in range(1, ly.DIM_i):
        for j in range(1, ly.DIM_j):
            x = tau*paint[i-1, j] + (1.0-tau)*random.uniform(-pnt0, pnt0)
            y = tau*paint[i, j-1] + (1.0-tau)*random.uniform(-pnt0, pnt0)
            # paint[i, j] = min(par.pnt_cut, max(-par.pnt_cut, (x+y)/2.0))
            paint[i, j] = (x+y)/2.0
    print('paint min max: ', np.min(paint), np.max(paint))
    return paint  # np.where(abs(paint) < 0.02*par.pnt_0, 0, paint)

# def init_paint(kartta, **kwargs):
#     paint = np.full((ly.DIM_i, ly.DIM_j), 0.0)
#     if kwargs['maasto']:
#         tau = 0.97
#         pnt0 = par.pnt_0
#         paint[0, 0] = 0.0  # random.uniform(-pnt0, pnt0)
#         for i in range(1, ly.DIM_i):
#             paint[i, 0] = tau*paint[i-1, 0] + \
#                 (1.0-tau)*random.uniform(-pnt0, pnt0)
#         for j in range(1, ly.DIM_j):
#             paint[0, j] = tau*paint[0, j-1] + \
#                 (1.0-tau)*random.uniform(-pnt0, pnt0)
#         for i in range(1, ly.DIM_i):
#             for j in range(1, ly.DIM_j):
#                 x = tau*paint[i-1, j] + (1.0-tau)*random.uniform(-pnt0, pnt0)
#                 y = tau*paint[i, j-1] + (1.0-tau)*random.uniform(-pnt0, pnt0)
#                 # paint[i, j] = min(par.pnt_cut, max(-par.pnt_cut, (x+y)/2.0))
#                 paint[i, j] = (x+y)/2.0
#     print('paint min max: ', np.min(paint), np.max(paint))
#     return paint  # np.where(abs(paint) < 0.02*par.pnt_0, 0, paint)


Roiskaistaan maisemaan muutama väritäplä. Väritäplän muotoilun pitäisi onnistua parilla sisäkkäisellä for-silmukalla, mutta sitä oli yllättävän vaikea suunnitella, joten hätäpäissäni turvauduin copy-pasteamiseen.

Tämän ohjelman alussa esitellyn maalari-olion toivomuksesta tehdään jokaista punaista roisketta vastaava sininen. Näin värien keskiarvo pysyy nollana, mikä helpottaa maalarin työtä.

Maalari-olio maalaa positiiviset arvot punaisen sävyillä ja negatiiviset sinisen sävyillä, joten ohjelmassa riittää tarkastella vain yhden väriaineen liikkeitä. Sekoittuessaan väriaineet neutraloivat toisensa, koska maalari maalaa nollaruudut valkoisella.


def roiskeita(paint):
    for k in range(0, 50):
        clr = random.uniform(0, par.pnt_0)

        ip = random.randrange(2, ly.DIM_i-3)
        jp = random.randrange(2, ly.DIM_j-3)
        paint[ip, jp] = clr
        paint[ip-1, jp] = 2.0/3.0*clr
        paint[ip+1, jp] = 2.0/3.0*clr
        paint[ip-2, jp] = 1.0/3.0*clr
        paint[ip+2, jp] = 1.0/3.0*clr
        paint[ip, jp-1] = 2.0/3.0*clr
        paint[ip, jp+1] = 2.0/3.0*clr
        paint[ip, jp-2] = 1.0/3.0*clr
        paint[ip, jp+2] = 1.0/3.0*clr
        paint[ip-1, jp-1] = 0.5*clr
        paint[ip-1, jp+1] = 0.5*clr
        paint[ip+1, jp-1] = 0.5*clr
        paint[ip+1, jp+1] = 0.5*clr

        clr = -clr
        ip = random.randrange(2, ly.DIM_i-3)
        jp = random.randrange(2, ly.DIM_j-3)
        paint[ip, jp] = clr
        paint[ip-1, jp] = 2.0/3.0*clr
        paint[ip+1, jp] = 2.0/3.0*clr
        paint[ip-2, jp] = 1.0/3.0*clr
        paint[ip+2, jp] = 1.0/3.0*clr
        paint[ip, jp-1] = 2.0/3.0*clr
        paint[ip, jp+1] = 2.0/3.0*clr
        paint[ip, jp-2] = 1.0/3.0*clr
        paint[ip, jp+2] = 1.0/3.0*clr
        paint[ip-1, jp-1] = 0.5*clr
        paint[ip-1, jp+1] = 0.5*clr
        paint[ip+1, jp-1] = 0.5*clr
        paint[ip+1, jp+1] = 0.5*clr
    return(paint)


# Ihan vaan muistutus siitä, mitä kaikkea numpy:llä voi tehdä
# kätevän tehokkaasti

# def nollaa_vari_huiput(a):
#     return np.where(abs(a) < 0.99*par.pnt_0, a, 0)

def test_plots():
    x = np.full(ly.DIM_j, 0.0)
    y = np.full(ly.DIM_i, 0.0)
    for j in range(0, ly.DIM_j):
        x[j] = par.ds*j
    for i in range(0, ly.DIM_i):
        y[i] = par.ds*i

    test_kartta()
    # test_reunat()
    test_kosket()
    # test_valuma(x, y)


##############################################################
# Pääohjelman alku
##############################################################
def main():
    print(tim.datetime.now())
    random.seed()
    # Turha jatkaa simulointia numpy-rutiinien ylivuodon jälkeen
    # Asetetaan ylivuoto virhetilanteeksi
    np.seterr(over='raise')
    plt.matplotlib.use('qtagg')
    plt.ioff()

    rho = np.full((ly.DIM_i, ly.DIM_j), par.rho_0)
    Dt0_rho = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    Dt1_rho = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    rho_nxt = np.full((ly.DIM_i, ly.DIM_j), par.rho_0)
    u = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    Dt0_u = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    Dt1_u = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    u_nxt = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    v = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    Dt0_v = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    Dt1_v = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    v_nxt = np.full((ly.DIM_i, ly.DIM_j), 0.0)

    paint = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    Dt0_paint = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    Dt1_paint = np.full((ly.DIM_i, ly.DIM_j), 0.0)
    paint_nxt = np.full((ly.DIM_i, ly.DIM_j), 0.0)

    kartta = init_kartta()
    rannat = rantavedet(kartta)
    if par.maasto:
        paint = init_paint(paint)
    else:
        paint = roiskeita(paint)

    Fe_u, Fe_v = kosket(vastapyorre=True)

    # Luodaan maalarin työpaja
    Maalari = Cl_Maalari(paint)
    # Tein toisenkin maalailemaan zoomauksia, mutta kaksi maalaria
    # vilkuttelivat kilvan tuotoksiaan niin ikävästi, että
    # jouduin kommentoimaan toisen sivuun.
    # zoom = Cl_Maalari()

    n = 0
    t = 0.0
    dt = par.dt
    overflow = False

    # +++++++++++++++ simulointi ++++++++++++++++++++++++++


Muuntaessani x- ja y- suunnan osittaisdifferentiaaliyhtälöitä differenssiyhtälöiksi, olisin voinut muuntaa myös aikaderivaatat differenssiyhtälöiksi ja sen jälkeen ratkaista nopeuksien, pinnankorkeuden ja väriaineiden konsentraatiot kussakin ruudussa hetkellä t + dt niiden arvojen hetkellä t perusteella. Menettelyä kutsutaan eulerin algoritmiksi.

Sitä aluksia kokeilinkin. Tiesin, että Eulerin algoritmi ei ole kovin tarkka, mutta ajattelin, ettei se haittaa näin taiteen teossa. Eri juttu, jos suunnittelisin lentokoneita. (Tosin Boeing 737 MAX onnettomuuksien syistä päätellen, eivät lentokoneiden suunnittelijatkaan ole turhan tarkkoja).

Pieni virhe ei olisi minua haitannut, mutta virhettä kertyi niin, että tietokoneestani 'loppuivat numerot' — Järven pintaan pullahti niin korkea kupru, ettei tietokoneestani löytynyt riittävän suurta numeroa siitä kertomaan. Tapahtui 'floating point overflow', kuten tietokonekielellä on tapana sanoa.

Onneksi wikipedia tiesi kertoa Heunin menetelmästä


    while t < par.tf and not overflow:
        # simuloinnin alkuaskelilla roiskitaan väritäpliä maisemaan
        if n < 100 and not par.maasto:
            paint = roiskeita(paint)
        # try — except rakentaan avulla pysäytän simuloinnin, jos
        # tulee 'floating point overflow'
        try:
            # Lasketaan kussakin ruudussa aikaderivaatat nopeudella
            # ja pinnankorkeudelle: Heun vaihe 1
            df.dfdt(kartta, rho, u, v, paint, Fe_u, Fe_v,
                    Dt0_rho, Dt0_u, Dt0_v, Dt0_paint)
            rho_nxt = rho + Dt0_rho*dt
            paint_nxt = paint + Dt0_paint*dt
            u_nxt = u + Dt0_u*dt
            v_nxt = v + Dt0_v*dt
            # Heunin menetelmän vaihe 2.
            df.dfdt(kartta, rho_nxt, u_nxt, v_nxt, paint_nxt, Fe_u, Fe_v,
                    Dt1_rho, Dt1_u, Dt1_v, Dt1_paint)
            rho_nxt = rho + (Dt0_rho + Dt1_rho)/2.0*dt
            paint_nxt = paint + (Dt0_paint + Dt1_paint)/2.0*dt
            u_nxt = u + (Dt0_u + Dt1_u)/2.0*dt
            v_nxt = v + (Dt0_v + Dt1_v)/2.0*dt
        except FloatingPointError as err:
            print('Overflow ', err)
            print('n, t ', n, t)
            overflow = True
        # Tämä näyttänee hämärältä?
        # Vaihdetaan 'vanhoihin' ja 'uusiin' taulukoihin osoittavat
        # pointterit.
        # Useimmat muuttujat tässä simuloinnissa ovat isoja numpy —
        # 'numeric python' — taulukoita, joita pitää käsitellä harkiten
        rho, rho_nxt = rho_nxt, rho
        paint, paint_nxt = paint_nxt, paint
        u, u_nxt = u_nxt, u
        v, v_nxt = v_nxt, v
        Maalari.sys_plot(rho, u, v, paint, n)
        tasaa_paineita(rho, rannat)
        # i0 = 145
        # i1 = 185
        # j0 = 90
        # j1 = 135
        # zoom.sys_plot(rho[i0:i1, j0:j1], u[i0:i1, j0:j1], v[i0:i1, j0:j1],
        #               paint[i0:i1, j0:j1], n)
        t += dt
        n += 1
        print(n, t)

    plt.close('all')
    print('Valmis! ', n, t, tim.datetime.now())


if __name__ == "__main__":
    main()