Het Marangoni-effect1 is het fenomeen waarbij er massaoverdracht plaatsvindt langs het grensvlak tussen twee vloeistoffen als gevolg van een verschil in oppervlaktespanning2. In 2017 bedacht Kohta Suzuno van de Meiji University in Japan een manier om met dit effect de kortste weg naar de uitgang van een doolhof te vinden: vul het doolhof met melk (water kan ook maar de witte kleur van melk geeft een beter contrast), plaats een zuur hydrogelblok bij de uitgang, kleur de melk aan de ingang met inkt en maak de gekleurde melk basisch door er met een penseel kortstondig vloeibare zeep in aan te brengen. Als gevolg van het pH-verschil verandert de oppervlaktespanning, waardoor de gekleurde melk naar het hydrogelblok gedreven wordt.
In 2018 toonden Fernando Temprano-Coleto en zijn collega's aan dat het zelfs helemaal niet nodig is om de uitgang te markeren met een zuur hydrogelblok, zolang het oppervlak aan de in- en uitgang van het doolhof maar een voldoende groot gebied beslaat. Dit wordt geïllustreerd in bovenstaande video.
We simuleren stroming doorheen een rechthoekig doolhof. Voor de eenvoud wordt de stroming niet veroorzaakt door een verschil in oppervlaktespanning, maar laten we een vloeistof door het doolhof stromen. Het doolhof heeft juist één ingang, waar we op hoogte 1.0 een kraan plaatsen waarlangs een vloeistof in het doolhof stroomt met debiet $$\delta$$. Het doolhof heeft ook juist één uitgang, waar we op hoogte 0.0 (wat correspondeert met de onderkant van het doolhof) een kraan plaatsen waarlangs alle vloeistof zonder beperking uit het doolhof kan stromen.
Hieronder tonen we aan de linkerkant hoe een doolhof in een tekstbestand voorgesteld wordt als een rechthoekig rooster. De muren van het doolhof worden aangeduid met hekjes (#) en de gangen worden aangeduid met spaties. Een doolhof is altijd langs de buitenste rand omgeven door muren. De ingang van het doolhof wordt voorgesteld door een plusteken (+) en de uitgang door een minteken (-). De ingang en de uitgang moeten niet noodzakelijk in de buitenste rand van het doolhof liggen. Aan de rechterkant zie je een grafische voorstelling van hetzelfde doolhof.
########### + # # ### ### # # # # # # ####### # # # # ### # ##### # # # # # ### # # # # # # - ###########
We simuleren het vloeistofniveau in de cellen van het doolhof in discrete tijdsstappen. Voor de kraan aan de ingang van het doolhof wordt opengedraaid (stap 0 van de simulatie), is het vloeistofniveau 1.0 aan de ingang en 0.0 in de gangen en aan de uitgang van het doolhof.
In elke tijdsstap wordt het vloeistofniveau in elke cel van het rooster herberekend op basis van het vloeistofniveau in de vorige tijdsstap. Daarbij blijft het vloeistofniveau aan de ingang en de uitgang constant op 1.0 en 0.0 staan. Uiteraard wordt er geen vloeistofniveau berekend in de cellen van het rooster die corresponderen met muren, en stellen we het vloeistofniveau daar per definitie in op -1.0.
Het nieuwe vloeistofniveau $$x_n$$ van een cel die correspondeert met een gang (niet de in- of uitgang) wordt berekend op basis van het vorige vloeistofniveau van die cel ($$x$$), het vorige vloeistofniveau van de naburige cellen boven ($$a$$), rechts ($$b$$), onder ($$c$$) en links ($$d$$) die corresponderen met gangen (inclusief in- en uitgang) en het debiet $$\delta$$ ($$0 \leq \delta \leq 1$$) waarmee de vloeistof aan de ingang in het doolhof stroomt. Deze berekening gebeurt aan de hand van de formule \[ x_n = x + \delta\left((a - x) + (b - x) + (c - x) + (d - x)\right) \] waarbij in de laatste som alle termen $$(m - x)$$ worden weggelaten waarvoor buurcel $$m$$ correspondeert met een muur van het doolhof. Op die manier behoudt het vloeistofniveau altijd een waarde uit het interval [0.0, 1.0]. Als je klikt op bovenstaande figuur met het vloeistofniveau bij tijdsstap 0, dan kan je doorklikken naar de simulaties van de 10 volgende tijdsstappen. Hieronder zie je de weergave van het vloeistofniveau na een simulatie over 1000 tijdsstappen.
Als we nu een drijvend voorwerp loslaten bij de ingang van het doolhof, dan zal het telkens naar de naburige cel stromen waarvan het vloeistofniveau het kleinste is van alle naburige cellen (boven, rechts, onder, links). Daar is de stroming tussen de twee naburige cellen immers het grootst. Dit proces stopt als er geen naburige cellen zijn waarvan het vloeistofniveau strikt kleiner is dan dat van de huidige cel. In bovenstaande afbeelding (vloeistofniveau na 1000 simulatiestappen) wordt de route van een drijvend voorwerp aangegeven door een gele lijn, en volgt die inderdaad het kortste pad van de ingang naar de uitgang van het doolhof.
Definieer een klasse Doolhof waarmee doolhoven kunnen voorgesteld worden die men kan gebruiken voor het simuleren in discrete tijdsstappen van het vloeistofniveau en het bepalen van de route waarlangs de stroming het sterkst is. Bij het aanmaken van een doolhof (Doolhof) moet de locatie (str) van een tekstbestand doorgegeven worden. Daarin wordt de configuratie van een rechthoekig doolhof voorgesteld in het formaat zoals hierboven beschreven. Er mag vanuit gegaan worden dat het tekstbestand een geldige voorstelling van een doolhof bevat, zonder dat dit expliciet moet gecontroleerd worden.
Op een doolhof $$d$$ (Doolhof) moet je minstens de volgende methoden kunnen aanroepen:
Een methode volgende_niveau waaraan twee argumenten moeten doorgegeven worden: i) de positie van een cel binnen het rooster van doolhof $$d$$ en ii) het debiet $$\delta \in \mathbb{R}$$ (float; $$0 \leq \delta \leq 1$$) waarmee de vloeistof aan de ingang in doolhof $$d$$ stroomt. De positie binnen het rooster wordt aangegeven door een tuple $$(r, k)$$, waarbij $$r$$ het rijnummer (int) en $$k$$ het kolomnummer (int) van de cel in het rooster aanduiden. Hierbij worden de rijen van boven naar onder en de kolommen van links naar rechts genummerd vanaf nul. Op basis van het huidige vloeistofniveau in de cellen van het rooster, moet de methode het vloeistofniveau (float) teruggeven die de gegeven cel zal hebben tijdens de volgende simulatiestap. Het vloeistofniveau bij een muur van het doolhof is per definitie gelijk aan -1.0. De methode mag het huidige vloeistofniveau van het rooster van doolhof $$d$$ niet aanpassen.
Een methode simuleer_niveau waaraan het debiet $$\delta \in \mathbb{R}$$ (float; $$0 \leq \delta \leq 1$$) moet doorgegeven worden waarmee de vloeistof aan de ingang in doolhof $$d$$ stroomt. De methode heeft nog een tweede optionele parameter stappen waaraan het aantal simulatiestappen $$n$$ kan doorgegeven worden (standaardwaarde: $$n = 1$$). De methode moet $$n$$ discrete tijdsstappen simuleren, waarbij telkens het vloeistofniveau van doolhof $$d$$ herberekend wordt op basis van het vloeistofniveau in de vorige tijdsstap. De methode moet een verwijzing naar doolhof $$d$$ teruggeven.
Een methode route waaraan geen argumenten moeten doorgegeven worden. De methode moet een lijst (list) van posities teruggeven, die de route aangeeft waarlangs een drijvend voorwerp beweegt dat wordt losgelaten bij de ingang van doolhof $$d$$. Hierbij worden posities van cellen in het rooster van doolhof $$d$$ op dezelfde manier voorgesteld als bij de methode volgende_niveau. De route moet berekend worden aan de hand van de procedure die hierboven wordt omschreven. Als er bij het bepalen van de naburige cel waarlangs de route zal verdergezet worden twee cellen zijn waarvan het vloeistofniveau het kleinste is, dan wordt de route prioritair verdergezet naar boven, naar rechts, naar onder en finaal pas naar links.
Als er een doolhof $$d$$ (Doolhof) wordt doorgegeven aan de ingebouwde functies repr en str, dan moeten die functies een stringvoorstelling (str) van doolhof $$d$$ teruggeven. Het formaat van beide voorstellingen kan afgeleid worden uit onderstaand voorbeeld. De functie str gebruikt voor de stringvoorstelling hetzelfde formaat als bij de voorstelling van het doolhof in het tekstbestand. Het enige verschil is dat de route waarlangs een drijvend voorwerp beweegt, wordt aangeduid met tildes (~) in plaats van spaties. De stringvoorstelling die wordt teruggegeven door de functie repr stelt een rooster voor, waarin alle kolommen bestaan uit vijf karakters en telkens van elkaar worden gescheiden door één enkele spatie. Een muur wordt voorgesteld door vijf hekjes (#####) en het vloeistofniveau in de andere cellen wordt voorgesteld als een reeël getal dat wordt afgerond tot op drie decimale cijfers en waarvan ook steeds drie decimale cijfers worden weergegeven.
In onderstaande voorbeeldsessie gaan we ervan uit dat de tekstbestanden doolhof_01.txt3, doolhof_02.txt4 en doolhof_03.txt5 zich in de huidige directory bevinden.
>>> doolhof = Doolhof('doolhof_01.txt6') # na 0 stappen
>>> doolhof
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
1.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 #####
##### 0.000 ##### ##### ##### 0.000 ##### ##### ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 ##### 0.000 #####
##### 0.000 ##### ##### ##### ##### ##### ##### ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 0.000 0.000 #####
##### ##### ##### 0.000 ##### 0.000 ##### ##### ##### ##### #####
##### 0.000 0.000 0.000 ##### 0.000 ##### 0.000 0.000 0.000 #####
##### 0.000 ##### ##### ##### 0.000 ##### 0.000 ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 ##### 0.000 0.000
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
>>> doolhof.volgende_niveau((0, 0), 0.2)
-1.0
>>> doolhof.volgende_niveau((1, 0), 0.2)
1.0
>>> doolhof.volgende_niveau((1, 1), 0.2)
0.2
>>> doolhof.volgende_niveau((1, 2), 0.2)
0.0
>>> doolhof.route()
[(1, 0), (1, 1)]
>>> print(doolhof)
###########
+~ #
# ### ### #
# # # #
# ####### #
# # #
### # #####
# # # #
# ### # # #
# # # -
###########
>>> doolhof.simuleer_niveau(0.2) # na 1 stap
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
1.000 0.200 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 #####
##### 0.000 ##### ##### ##### 0.000 ##### ##### ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 ##### 0.000 #####
##### 0.000 ##### ##### ##### ##### ##### ##### ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 0.000 0.000 #####
##### ##### ##### 0.000 ##### 0.000 ##### ##### ##### ##### #####
##### 0.000 0.000 0.000 ##### 0.000 ##### 0.000 0.000 0.000 #####
##### 0.000 ##### ##### ##### 0.000 ##### 0.000 ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 ##### 0.000 0.000
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
>>> doolhof.volgende_niveau((0, 0), 0.2)
-1.0
>>> doolhof.volgende_niveau((1, 0), 0.2)
1.0
>>> doolhof.volgende_niveau((1, 1), 0.2)
0.28
>>> doolhof.volgende_niveau((1, 2), 0.2)
0.04000000000000001
>>> doolhof.route()
[(1, 0), (1, 1), (1, 2)]
>>> print(doolhof)
###########
+~~ #
# ### ### #
# # # #
# ####### #
# # #
### # #####
# # # #
# ### # # #
# # # -
###########
>>> doolhof.simuleer_niveau(0.2) # na 2 stappen
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
1.000 0.280 0.040 0.000 0.000 0.000 0.000 0.000 0.000 0.000 #####
##### 0.040 ##### ##### ##### 0.000 ##### ##### ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 ##### 0.000 #####
##### 0.000 ##### ##### ##### ##### ##### ##### ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 0.000 0.000 #####
##### ##### ##### 0.000 ##### 0.000 ##### ##### ##### ##### #####
##### 0.000 0.000 0.000 ##### 0.000 ##### 0.000 0.000 0.000 #####
##### 0.000 ##### ##### ##### 0.000 ##### 0.000 ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 ##### 0.000 0.000
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
>>> doolhof.volgende_niveau((0, 0), 0.2)
-1.0
>>> doolhof.volgende_niveau((1, 0), 0.2)
1.0
>>> doolhof.volgende_niveau((1, 1), 0.2)
0.328
>>> doolhof.volgende_niveau((1, 2), 0.2)
0.08000000000000002
>>> doolhof.route()
[(1, 0), (1, 1), (1, 2), (1, 3)]
>>> print(doolhof)
###########
+~~~ #
# ### ### #
# # # #
# ####### #
# # #
### # #####
# # # #
# ### # # #
# # # -
###########
>>> doolhof.simuleer_niveau(0.2, 8) # na 10 stappen
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
1.000 0.481 0.256 0.113 0.040 0.010 0.002 0.000 0.000 0.000 #####
##### 0.249 ##### ##### ##### 0.002 ##### ##### ##### 0.000 #####
##### 0.088 0.033 0.012 ##### 0.000 0.000 0.000 ##### 0.000 #####
##### 0.033 ##### ##### ##### ##### ##### ##### ##### 0.000 #####
##### 0.010 0.002 0.000 ##### 0.000 0.000 0.000 0.000 0.000 #####
##### ##### ##### 0.000 ##### 0.000 ##### ##### ##### ##### #####
##### 0.000 0.000 0.000 ##### 0.000 ##### 0.000 0.000 0.000 #####
##### 0.000 ##### ##### ##### 0.000 ##### 0.000 ##### 0.000 #####
##### 0.000 0.000 0.000 ##### 0.000 0.000 0.000 ##### 0.000 0.000
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
>>> doolhof.route()
[(1, 0), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (5, 2), (5, 3), (6, 3), (7, 3), (7, 2), (7, 1)]
>>> print(doolhof)
###########
+~ #
#~### ### #
#~ # # #
#~####### #
#~~~# #
###~# #####
#~~~# # #
# ### # # #
# # # -
###########
>>> doolhof.simuleer_niveau(0.2, 990) # na 1000 stappen
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
1.000 0.933 0.887 0.841 0.795 0.750 0.709 0.669 0.630 0.592 #####
##### 0.913 ##### ##### ##### 0.747 ##### ##### ##### 0.555 #####
##### 0.893 0.892 0.891 ##### 0.745 0.743 0.742 ##### 0.519 #####
##### 0.875 ##### ##### ##### ##### ##### ##### ##### 0.483 #####
##### 0.858 0.842 0.828 ##### 0.323 0.353 0.384 0.416 0.449 #####
##### ##### ##### 0.814 ##### 0.294 ##### ##### ##### ##### #####
##### 0.783 0.792 0.802 ##### 0.266 ##### 0.114 0.090 0.067 #####
##### 0.775 ##### ##### ##### 0.239 ##### 0.137 ##### 0.045 #####
##### 0.770 0.766 0.764 ##### 0.212 0.187 0.162 ##### 0.022 0.000
##### ##### ##### ##### ##### ##### ##### ##### ##### ##### #####
>>> doolhof.route()
[(1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (2, 9), (3, 9), (4, 9), (5, 9), (5, 8), (5, 7), (5, 6), (5, 5), (6, 5), (7, 5), (8, 5), (9, 5), (9, 6), (9, 7), (8, 7), (7, 7), (7, 8), (7, 9), (8, 9), (9, 9), (9, 10)]
>>> print(doolhof)
###########
+~~~~~~~~~#
# ### ###~#
# # #~#
# #######~#
# #~~~~~#
### #~#####
# #~#~~~#
# ###~#~#~#
# #~~~#~-
###########
>>> print(Doolhof('doolhof_02.txt7').simuleer_niveau(0.2, stappen=1000))
###########
# # #
# ####### #
# #
#####+#####
# ~~~~~#
# #######~#
# #~~~~#
#### #~####
# #~~~~-
###########
>>> print(Doolhof('doolhof_03.txt8').simuleer_niveau(0.2, stappen=500))
#########################
# # # # # # #~-
# # # ### # ### # # # #~#
# # # #~~~ #~~~# #~#
# #######~#~#####~#~###~#
#~~~~~~~~~#~ #~#~~~~~#
#~### #####~#####~##### #
#~# #~~~~~#~~~~~ # #
#~#####~#####~### ### ###
#~ #~~~~~~~# # # # #
#~### # ### ### ##### # #
#~# # # # #
#~# ### # ##### ### # ###
+~# # # # # # #
#########################
Lagzi I, Soh S, Wesson PJ, Browne KP, Grzybowski BA (2010). Maze solving by chemotactic droplets. Journal of the American Chemical Society 132(4), 1198–1199. 9
Stricker L (2017). Numerical simulation of artificial microswimmers driven by Marangoni flow. Journal of Computational Physics 347, 467–389. 10
Suzuno K, Ueyama D, Branicki M, Tóth R, Braun A, Lagzi I (2017). Marangoni flow driven maze solving. Advances in Unconventional Computing, 237–243. 11
Temprano-Coleto F, Peaudecerf FJ, Landel JR, Gibou F, Luzzatto-Fegiz P (2018). Soap opera in the maze: geometry matters in Marangoni flows. Physical Review Fluids 3(10), 100507. 12
Mao ZS, Chen J (2004). Numerical simulation of the Marangoni effect on mass transfer to single slowly moving drops in the liquid–liquid system. Chemical Engineering Science 59(8-9), 1815-1828. 13