import time
import logging
from pathlib import Path
from typing import Union
from math import log10, ceil
import numpy as np
import pygame
# from wolfhece.PyGui import Wolf2DModel # deprecated in favor of prev_sim2D
from wolfhece.mesh2d.wolf2dprev import prev_sim2D
from importlib.metadata import version
[docs]
wolf_hece_version = tuple(map(int, version("wolfhece").split('.')))
if wolf_hece_version <= (1,8,11):
from wolfhece.mesh2d.wolf2dprev import prev_boundary_condition
elif wolf_hece_version >= (2,0,5):
from wolfhece.mesh2d.wolf2dprev import boundary_condition_2D as prev_boundary_condition
else:
# 1.8.13, 2.0.1 are broken for us :-(
raise Exception(f"I don't recognize this version of wolfhece : {wolf_hece_version}")
from ..glsimulation import GLSimulation, get_report_frequency
from ..tile_packer import TilePackingMode
from ..loaders.utils import convert_bc_type, make_impervious_bathymetry
from ..simple_simulation import InfiltrationInterpolation, SimulationDuration, SimulationDurationType, TimeStepStrategy, BoundaryConditionsTypes
from ..simple_simulation import NB_BC_TYPES, BoundaryConditionIndices as bci, BC_BLANK_VALUE
[docs]
def load_sim_to_gpu(
model: Union[Path, prev_sim2D],
init_gl=True,
glsim: Union[None, GLSimulation] = None,
tile_size=16,
interp_infiltration: InfiltrationInterpolation = InfiltrationInterpolation.NONE,
shader_log_path=None,
interpret_freq_type_as_wolf: bool = False,
tiles_packing_mode: TilePackingMode = TilePackingMode.REGULAR,
optimize_indirection: bool = False,
fail_if_invalid_sim: bool = True
) -> GLSimulation:
"""
`model`: You can pass either a directory to a model (a `Path`) or
a `Wolf2DModel`.
`glsim`: If provided, the model will be loaded into the given
simulation. If not provided, a brand new simulation will be
created.
`interpret_freq_type_as_wolf`: If true and unit of report frequency is
seconds (instead of steps), then we interpret the duration of the simulation
as a number of seconds (instead of a number of steps). This param was introduced
because in our test/debug, "npas" is always expected to be a number of steps
(and not seconds).
"""
assert isinstance(model, Path) or isinstance(model, prev_sim2D)
assert interp_infiltration in (InfiltrationInterpolation.NONE, InfiltrationInterpolation.LINEAR)
errors = []
def check_error(cond, msg):
if not cond:
errors.append(msg)
if init_gl:
pygame.init()
# Loads a Wolf simulation into the simulator
start_time = time.time()
if isinstance(model, Path):
logging.info(f"Loading model data from Wolf model {model}")
# Will figure out itself the name of the files inside the directory
wolf2d=prev_sim2D(fname=str(model))
#print(wolf2d.parameters.to_yaml())
# This is hackish but allows to load
# the data of the simultaion (seems
# like things are lazy loaded, or something like
# that)
else:
wolf2d = model
wolf2d.force_load()
logging.info(f"Uploading to GPU")
# TODO At some point, I'll have to merge all
# of that back into PA's models, this is s huge duplication
# of code (it was done to better understand what I take out
# of PA's model; now that I understand them better, I could
# directly take data out of them)
def copy_and_extend(a):
plus_row = np.vstack( [ a, np.zeros( (1,a.shape[1]) ) ])
plus_col = np.hstack( [ plus_row, np.zeros( (plus_row.shape[0],1) ) ])
return plus_col
if wolf2d.nb_blocks > 1:
logging.warning("The modelisation appears to be multi-blocks. I will *ONLY* handle the *first* one !")
# FIXME Depending on the source, you may or may not
# have a valid maks here.
nap = wolf2d.napbin.array.data.copy()
# FIXME Depending on the source, an active (= non masked) value is denoted
# by +1 or -1; why are there two possibilities ? Inactive
# seem to be always zero though.
# So we fix that: 0 means inactive; 1 means active.
nap[nap != 0] = 1
check_error(not np.all(nap == 0), "NAP matrix is full of zeros, meaning every cells are inactive!")
# FIXME Why copy ?
bathymetry = wolf2d.top.array.filled( np.max(wolf2d.top.array) )
check_error(np.sum(bathymetry < 0) == 0, "I have found negative levels of bathymetry. I can't handle that.")
water_height = wolf2d.hbin.array.filled(0)
# print(water_height)
# print(wolf2d.hbin.array)
check_error( np.sum(water_height < 0) == 0, "I have found negative levels of water. I can't handle that.")
qxbin = wolf2d.qxbin.array.filled(0)
qybin = wolf2d.qybin.array.filled(0)
manning = wolf2d.frot.array.filled( np.max(wolf2d.frot.array) )
check_error( np.sum(manning < 0) == 0, "I have found negative Manning coefficients. I can't handle that.")
# wolf2d.infiltration.plot_plt()
infiltration = wolf2d.infiltration
noptpas=wolf2d.parameters._scheme_optimize_timestep
nxfin=wolf2d.nbx
nyfin=wolf2d.nby
dxfin=float(wolf2d.dx)
dyfin=float(wolf2d.dy)
check_error( dxfin == dyfin, "Only squared meshes supported; 'dxfin' must equal 'dyfin'")
ponderation = wolf2d.parameters._scheme_rk
froude_max = wolf2d[1]._froude_max
if ponderation > 1.0 or ponderation <= 0.0:
logging.warning(f"Unsupported ponderation value {ponderation}. I expect 'ponderation' between zero and one. I'll replace by R-K with 0.3/0.7.")
ponderation = 0.3
vncsouhaite=wolf2d.parameters._scheme_cfl
npas= int(wolf2d.parameters._nb_timesteps)
tstep = wolf2d.parameters._timestep_duration or None
# This mumbo jumbo to make sure wxWidget leaves us in peace to define our
# own GL context It doesn't work if we can't close all frames which happens
# when your mouse is on an OpenGL frame and the text lens gets activated. So
# keep your mouse in the corner of the screen (or find a way to close all
# frames)
# wolf2d.allviews.Close()
# wolf2d.allviews.mytooltip.Close()
# wolf2d.Close()
# wolf2d.mylogs.GetFrame().Close()
# ex = None
# wolf2d = None
# logging.info("Sleeping")
# # sleep(5)
# logging.info("Awaking")
if init_gl:
from math import sqrt
display_nfo = pygame.display.Info()
screen_width = int(display_nfo.current_w*0.9)
screen_height = int(display_nfo.current_h*0.9)
sim_area = nxfin*nyfin
screen_area = (screen_width * screen_height)/5
area_ratio = screen_area / sim_area
aspect_ratio = nxfin / nyfin
window_height = sqrt(area_ratio)*nyfin
window_width = sqrt(area_ratio)*aspect_ratio*nyfin
if window_height > screen_height:
window_height = screen_height
window_width = aspect_ratio * window_height
elif window_width > screen_width:
window_width = screen_width
window_height= window_width / aspect_ratio
window_height = int(round(window_height))
window_width = int(round(window_width))
# w,h=nxfin,nyfin
# |pygame.DOUBLEBUF
# Note: vsync=0: not sure this works. But setting no vblank on the
# nvidia control panel does work.
#window_width, window_height = nxfin, nyfin
pygame.display.set_mode((window_width, window_height), pygame.OPENGL|pygame.DOUBLEBUF, vsync = 0)
#pygame.display.set_mode((w, h), pygame.OPENGL)
# # The glDisables will work only after pygame.display.set_mode has been
# # called.
# glDisable(GL_BLEND)
# glDisable(GL_DEPTH_TEST)
# # We absolutely don't want anti alisaing (for exampel
# # when computeing the max step size)
# glDisable(GL_POLYGON_SMOOTH)
# logging.info("OpenGL initialized")
# numpy is row-major (https://ncar-hackathons.github.io/scientific-computing/numpy/02_memory_layout.html)
# consequently, addressing a numpy array is done as a[y,x] (y first).
# In a WolfArray, the x and y are swapped when compared
# to the width and height of a numpy array.
# I have two solutions:
# - I keep the (x,y) shape but then I need to transpose all WolfArray matrices
# before sending the to GPU wolf and transpose them again when writing
# results.
# - I swap x,y to (y,x) but then I am at the mercy of any non-symetric
# process in the simulation.
check_error( (nxfin, nyfin) == bathymetry.shape, f"Shapes differ ? Params say: {wolf2d.nby}x{wolf2d.nbx}, .top file says {bathymetry.shape[0]}x{bathymetry.shape[1]}")
# FIXME contiguous array ? Not sure it is of anyhelp since
# pyopengl inputs np.array.
nap = np.ascontiguousarray(nap.transpose())
logging.info(f"NAP as numpy array is {nap.shape[0]} rows x {nap.shape[1]} columns")
if False:
# debug code
import matplotlib.pyplot as plt
print(f"{np.min(nap.data)} - {np.max(nap.data)}")
plt.title("Showing NAP values to figure which is which")
plt.imshow(nap, origin="lower")
plt.colorbar()
plt.show()
bathymetry = np.ascontiguousarray(bathymetry.transpose())
if False:
numpy_crd = (4093-1,631-1)
b = bathymetry[numpy_crd]
from matplotlib import pyplot as plt
plt.imshow(np.flipud(bathymetry))
plt.show()
water_height = np.ascontiguousarray(water_height.transpose())
qxbin = np.ascontiguousarray(qxbin.transpose())
qybin = np.ascontiguousarray(qybin.transpose())
manning = np.ascontiguousarray(manning.transpose())
# Replace "null" value (-99999) byt something
# meaningful so that when we query cells outside
# the "general contour", we find somethign appropriate.
# FIXME This should be done in the shaders so that computations
# are adapted to missing data (instead of providing "default"
# data). Note we have discussed that with PA in dec/2023 and
# we decided it was OK for the moment.
# Create a wall that is ten times has tall as the max bathy.
# Additional sorcery to make that height something clearly
# artificial, ie 9999.
#unreachable_height = (10 ** (ceil(log10(np.max( bathymetry))) + 1)) - 1
#unreachable_height = round(np.max( bathymetry) + 2*np.max(water_height))
# bathymetry[bathymetry < 0] = unreachable_height
# water_height[water_height < 0] = 0
# qxbin[qxbin == -99_999.0] = 0
# qybin[qybin == -99_999.0] = 0
# When handling boundary condition, we may look in cells
# outside the computation domain (inactive, masked cells).
# For these cells, we expect
# "neutral" data (zeros) that won't affect computations.
msk = nap == 0 # => True = inactive cell
bathymetry[msk] = 99999.
water_height[msk] = 0
qxbin[msk] = 0 # BUG ???
qybin[msk] = 0
# We enclose the simulation domain in a border
# Make sure to leave the B.C. as they are.
# FIXME I don't think it is necessary...
nap_draft = np.copy(nap)
for bc in wolf2d.parameters.clfbx.mybc:
cell_numpy_crd = (bc.j-1, bc.i-1)
nap_draft[cell_numpy_crd] = 1
for bc in wolf2d.parameters.clfby.mybc:
cell_numpy_crd = (bc.j-1, bc.i-1)
nap_draft[cell_numpy_crd] = 1
bathymetry = make_impervious_bathymetry(nap_draft, bathymetry)
# min_pos = (628-1, 1887-1)
# sl_row2 = slice(min_pos[0]-4, min_pos[0]+5)
# sl_col2 = slice(min_pos[1]-4, min_pos[1]+5)
# dbg_bath = bathymetry[sl_row2, sl_col2]
# dbg_qxbin = qxbin[sl_row2, sl_col2]
# dbg_qybin = qybin[sl_row2, sl_col2]
# dbg_h = water_height[sl_row2, sl_col2]
# These two are important because Qx is taken into account
# when doing splitting. So if we need to split between
# a cell that is inside the domain and one that is outside,
# having zeros in the (outside domain) Qx,Qy makes computation
# easier
if not np.all( np.logical_not( (qxbin != 0) & (water_height == 0) )):
logging.warning("There are cells where Qx is non-zero but where's no water ! Here are some coordinates:")
logging.warning(np.fliplr(np.argwhere((qxbin != 0) & (water_height == 0))))
qxbin[(qxbin != 0) & (water_height == 0)] = 0
if not np.all( np.logical_not( (qybin != 0) & (water_height == 0) )):
logging.warning("There are cells where Qy is non-zero but where's no water ! Here are some coordinates:")
logging.warning(np.fliplr(np.argwhere((qybin != 0) & (water_height == 0))))
qybin[(qybin != 0) & (water_height == 0)] = 0
logging.info(f"{100*(np.sum(nap > 0)/(nxfin*nyfin)):.1f}% active meshes; {100*(np.sum(water_height !=0)/(nxfin*nyfin)):.1f}% meshes with water")
# assert np.alltrue( np.logical_not( (qxbin != 0) & (water_height == 0) )), \
# "There are cells where Qx is non-zero but where's no water !"
# assert np.alltrue( np.logical_not( (qybin != 0) & (water_height == 0) )), \
# "There are cells where Qy is non-zero but where's no water !"
# Handle weak boundary conditions, horizontal direction
# 1=active cell, shall be computed; 0=inactive cell, will not be computed.
# Pay attention to the fact that if an active cell touches an inactive
# one, then, there must be boundary conditions.
# Bit 1: cell is active(==1) or not (==0)
# Bit 2: weak boundary condition QX (left border) (==1) or not (==0)
weak_boundary_cond = np.zeros((nyfin, nxfin), dtype=np.uint32)
weak_boundary_cond[(water_height > 0) | (nap == 1)] = 1 # FIXME Not correct, use the NAP mask !
# Mapping cell numpy coordinates (x,y) onto an array
# of values. Each value represents a boundary condition.
# The index of the value represents the type of the condition.
# If the value is BC_BLANK_VALUE it means the b.c. is not
# used on the cell.
# e.g. bc_descriptions[ (i,j) ] = [BC's H on left border's value, HMOD on left border's value, ... ]
bc_descriptions = dict()
# ------------------------------------------------------------------------
# Boundary Conditions on left borders
# ------------------------------------------------------------------------
bc: prev_boundary_condition
for bc in wolf2d.parameters.clfbx.mybc:
bc_type = convert_bc_type(bc.ntype)
logging.debug(f"B.C. left border at {bc.i},{bc.j}, type: {bc_type}")
# Attention! x,y are swapped !!!
cell_numpy_crd = (bc.j-1, bc.i-1)
if cell_numpy_crd not in bc_descriptions:
bc_descriptions[cell_numpy_crd] = [BC_BLANK_VALUE] * NB_BC_TYPES
x, y = bc.i-1, bc.j-1 # i,j are Fortran coords => translate to numpy
# - Either the BC is on the left border of a cell which
# is on the interior side of the border of the domain.
# - Either the BC is on the left border of a cell which
# is on the exterior side of the border of the domain.
left_cell_crd = (y, x - 1)
if not( (x == 0 and nap[cell_numpy_crd] == 1) \
or (nap[cell_numpy_crd] == 1 and nap[left_cell_crd] == 0) \
or (nap[cell_numpy_crd] == 0 and nap[left_cell_crd] == 1)):
err = f"Weak boundary condition at i={bc.i},j={bc.j}: this B.C. is not on the border of the computation domain"
logging.error(err)
raise Exception(err)
if bc_type == BoundaryConditionsTypes.QX:
logging.debug(f"Weak boundary condition over QX at i={bc.i},j={bc.j}, value={bc.val}")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_QX_ON_LEFT.value] = bc.val
elif bc_type == BoundaryConditionsTypes.QY:
logging.debug(f"Weak boundary condition over QY at i={bc.i},j={bc.j}, value={bc.val}")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_QY_ON_LEFT.value] = bc.val
elif bc_type == BoundaryConditionsTypes.H:
loc_msg = f"Weak boundary condition over H at i={bc.i},j={bc.j}"
logging.debug(f"{loc_msg}, value={bc.val}")
if bc.val < 0:
# negative values are a hack to express impervious boundaries
logging.error(f"{loc_msg}, h={bc.val} is negative !!!")
if qxbin[cell_numpy_crd] > 0:
logging.warning(f"{loc_msg} doesn't provide enough information to the cell (see cell flow direction)")
if nap[cell_numpy_crd] == 0 \
and bathymetry[left_cell_crd] > bc.val:
logging.error(f"{loc_msg}: seems located rightside of the " \
"domain but the bathymetry in the neighbour " \
f"cell is above the surface level of the b.c. {bathymetry[left_cell_crd]} > {bc.val}, I expect it to be below.")
if nap[cell_numpy_crd] == 1 \
and bathymetry[cell_numpy_crd] > bc.val:
logging.error(f"{loc_msg}: seems located leftside of the " \
"domain but the bathymetry in the cell is " \
f"above the surface level of the b.c. {bathymetry[left_cell_crd]} > {bc.val}, I expect it to be below.")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_H_ON_LEFT.value] = bc.val
elif bc_type == BoundaryConditionsTypes.HMOD:
logging.debug(f"Weak boundary condition over HMOD at i={bc.i},j={bc.j}, value={bc.val}")
if bc.val < 0:
# negative values are a hack to express impervious boundaries
logging.warning(f"Weak boundary condition over HMOD at i={bc.i},j={bc.j}, value={bc.val} is negative !!!")
if x < qxbin.shape[1] and y < qxbin.shape[0] and qxbin[y,x] > 0:
logging.warning(f"Weak boundary condition over HMOD at i={bc.i},j={bc.j} doesn't provide enough information to the cell (see cell flow direction)")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_HMOD_ON_LEFT.value] = bc.val
elif bc_type == BoundaryConditionsTypes.FROUDE_NORMAL:
logging.debug(f"Weak boundary condition Froude normal direction at i={bc.i},j={bc.j}, value={bc.val}")
check_error( bc.val > 0, f"The Froude number must be > 0, you gave {bc.val}")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_FROUDE_NORMAL_ON_LEFT.value] = bc.val
else:
logging.error(f"Unsupported weak condition type {bc_type}")
# Attention: if we have a BC on the left border of cell (i,j)
# we record it. But we also mark the cell (i-1,j) with a
# no-values boundary condition. This special "no-values b.c." cell
# will be interpreted by the GPU engine as a cell with a b.c.
# on its *right* border. This is a bit hacky but makes sure
# that the values of a boundary condition are not duplicated
# across cells (which always carries a risk of mis-duplication bug)
# Of course if one wants to change the BC on the (i-1,j) cell
# he can as it will not disable the detection on a right border BC.
# FIXME This is dangerous as usually a BC is defined on a border
# but is meant to produce effect only on one side of the border.
cell_numpy_crd_left = (cell_numpy_crd[0], cell_numpy_crd[1]-1)
if cell_numpy_crd_left not in bc_descriptions:
bc_descriptions[cell_numpy_crd_left] = [BC_BLANK_VALUE] * NB_BC_TYPES
# ------------------------------------------------------------------------
# Boundary Conditions on bottom borders
# ------------------------------------------------------------------------
bc: prev_boundary_condition
for bc in wolf2d.parameters.clfby.mybc:
bc_type = convert_bc_type(bc.ntype)
logging.debug(f"B.C. bottom border at {bc.i},{bc.j}, type: {bc_type}")
# Attention! x,y are swapped !!!
cell_numpy_crd = (bc.j-1, bc.i-1)
if cell_numpy_crd not in bc_descriptions:
bc_descriptions[cell_numpy_crd] = [BC_BLANK_VALUE] * NB_BC_TYPES
x, y = bc.i-1, bc.j-1 # i,j are Fortran coords => translate to numpy
bottom_cell_crd = (y - 1, x)
# - Either the BC is on the bottom border of a cell which
# is on the interior side of the border of the domain.
# - Either the BC is on the bottom border of a cell which
# is on the exterior side of the border of the domain.
if not( (x == 0 and nap[cell_numpy_crd] == 1) \
or (nap[cell_numpy_crd] == 1 and nap[bottom_cell_crd] == 0) \
or (nap[cell_numpy_crd] == 0 and nap[bottom_cell_crd] == 1)):
err = f"Weak boundary condition at i={bc.i},j={bc.j}: this B.C. is not on the border of the computation domain"
logging.error(err)
raise Exception(err)
if bc_type == BoundaryConditionsTypes.QX:
logging.debug(f"Weak boundary condition over QX (bottom border) at i,j:{(bc.i, bc.j)}, value={bc.val}")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_QX_ON_BOTTOM.value] = bc.val
elif bc_type == BoundaryConditionsTypes.QY:
logging.debug(f"Weak boundary condition over QY (bottom border) at i,j:{(bc.i, bc.j)}, value={bc.val}")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_QY_ON_BOTTOM.value] = bc.val
# elif bc_type == BoundaryConditionsTypes.H.value:
# logging.info(f"Weak boundary condition over H (bottom border) at i,j:{(bc.i, bc.j)}, value={bc.val}")
# if bc.val < 0:
# # negative values are a hack to express impervious boundaries
# logging.warning(f"Weak boundary condition over H (horiz border) at i={bc.i},j={bc.j}, value={bc.val} is negative !!!")
# if qxbin[y,x] > 0:
# logging.warning(f"Weak boundary condition over H (horiz border) at i={bc.i},j={bc.j} doesn't provide enough information to the cell (see cell flow direction)")
# if bathymetry[cell_numpy_crd] > bc.val:
# logging.error(f"Weak boundary condition over H at i={bc.i},j={bc.j}: the bathymetry is above the surface level, I expect it to be below.")
# bc_descriptions[cell_numpy_crd][BC_TABLE_INDEX_FOR_H_ON_BOTTOM] = bc.val
elif bc_type == BoundaryConditionsTypes.H:
# ........................................................................ NEW
loc_msg = f"Weak horizontal boundary condition over H (surface altitude) at i={bc.i},j={bc.j}"
#logging.info(f"{loc_msg}, value={bc.val}")
if bc.val < 0:
# negative values are a hack to express impervious boundaries
logging.error(f"{loc_msg}, h={bc.val} is negative !!!")
if qybin[cell_numpy_crd] > 0:
logging.warning(f"{loc_msg} doesn't provide enough information to the cell (see cell flow direction)")
if nap[cell_numpy_crd] == 0 \
and bathymetry[bottom_cell_crd] > bc.val:
logging.error(f"{loc_msg}: seems located top side of a mesh in the " \
"domain but the bathymetry in the neighbour " \
f"cell is above the surface level of the b.c. {bathymetry[bottom_cell_crd]} > {bc.val}, I expect it to be below.")
if nap[cell_numpy_crd] == 1 \
and bathymetry[cell_numpy_crd] > bc.val:
logging.error(f"{loc_msg}: seems located bottom side of a mesh in the " \
"domain but the bathymetry in the cell is " \
f"above the surface level of the b.c. {bathymetry[bottom_cell_crd]} > {bc.val}, I expect it to be below.")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_H_ON_BOTTOM.value] = bc.val
# ........................................................................ NEW
elif bc_type == BoundaryConditionsTypes.HMOD:
logging.debug(f"Weak boundary condition over HMOD (water height, horiz border) at i={bc.i},j={bc.j}, value={bc.val}")
if bc.val < 0:
# negative values are a hack to express impervious boundaries
logging.warning(f"But at i={bc.i},j={bc.j}, B.C.'s value={bc.val} is negative !!!")
if x < qybin.shape[1] and y < qybin.shape[0] and qybin[y,x] > 0:
logging.warning(f"But at i={bc.i},j={bc.j} B.C. doesn't provide enough information to the cell (see cell flow direction)")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_HMOD_ON_BOTTOM.value] = bc.val
elif bc_type == BoundaryConditionsTypes.FROUDE_NORMAL:
logging.debug(f"Weak boundary condition Froude normal direction at i={bc.i},j={bc.j}, value={bc.val}")
check_error( bc.val > 0, f"The Froude number must be > 0, you gave {bc.val}")
bc_descriptions[cell_numpy_crd][bci.BC_TABLE_INDEX_FOR_FROUDE_NORMAL_ON_BOTTOM.value] = bc.val
else:
raise Exception(f"Unsupported weak condition type {bc_type} at i={bc.i},j={bc.j}")
# Mirror this BC on the top border of the bottom side cell.
cell_numpy_crd_bottom = (cell_numpy_crd[0]-1, cell_numpy_crd[1])
if cell_numpy_crd_bottom not in bc_descriptions:
bc_descriptions[cell_numpy_crd_bottom] = [BC_BLANK_VALUE] * NB_BC_TYPES
# ........................................................................
# Handle strong boundary conditions
# ........................................................................
strong_boundary_cond = np.ones((nyfin, nxfin), dtype=np.float32)
strong_boundary_cond[:,:] = -99999.0
strong_boundary_cond_values = np.zeros((nyfin, nxfin, 3), dtype=np.float32)
bc: prev_boundary_condition
for bc in wolf2d.parameters.clf.mybc:
bc_type = convert_bc_type(bc.ntype)
if bc_type == BoundaryConditionsTypes.QX:
x, y = bc.i-1, bc.j-1
logging.error(f"IGNORED Strong boundary condition over QX at x={x},y={y}={bc.val}")
strong_boundary_cond[x, y] = 0.0
strong_boundary_cond_values[y, x] = np.array([0.0,bc.val,0.0])
elif bc_type == BoundaryConditionsTypes.H:
# FIXME BoundaryConditionsTypes.H.value is shared with *weak*
# boundary conditions. So is that number OK to be shared ?
x, y = bc.i-1, bc.j-1
logging.error(f"IGNORED Strong boundary condition over H at x={x},y={y}={bc.val}")
strong_boundary_cond[x, y] = 1.0
strong_boundary_cond_values[y, x] = np.array([bc.val,0.0,0.0])
else:
raise Exception(f"Unsupported strong condition type {bc_type}")
# ........................................................................
# Checking boundary conditions are coherent
# ........................................................................
for crd, bc_values in bc_descriptions.items():
if np.all( np.array((bc_values) == BC_BLANK_VALUE)):
# We don't have any b.c. here.
# Two possible reasons:
# - Wolf has more types of BC than what we support here). So none
# is marked
# - this `bc_values` represents a beacon for a top or right b.c.
continue
ij_crd = "i:{},j:{}".format(crd[1]+1,crd[0]+1)
# Either there's no Qx and no Qy, either there is a Qx AND a Qy
check_error( (bc_values[bci.BC_TABLE_INDEX_FOR_QX_ON_LEFT.value] != BC_BLANK_VALUE) == (bc_values[bci.BC_TABLE_INDEX_FOR_QY_ON_LEFT.value] != BC_BLANK_VALUE), \
f"At {ij_crd}, If you set Qx on the left border you must set Qy too, and vice versa.")
check_error( (bc_values[bci.BC_TABLE_INDEX_FOR_QX_ON_BOTTOM.value] != BC_BLANK_VALUE) == (bc_values[bci.BC_TABLE_INDEX_FOR_QY_ON_BOTTOM.value] != BC_BLANK_VALUE), \
f"At {ij_crd}, If you set Qx (or Qy) on the bottom border you must set Qy (or Qx) too.")
h_or_hmod = bc_values[bci.BC_TABLE_INDEX_FOR_H_ON_LEFT.value] != BC_BLANK_VALUE or bc_values[bci.BC_TABLE_INDEX_FOR_HMOD_ON_LEFT.value] != BC_BLANK_VALUE
qx_or_qy = bc_values[bci.BC_TABLE_INDEX_FOR_QX_ON_LEFT.value] != BC_BLANK_VALUE or bc_values[bci.BC_TABLE_INDEX_FOR_QY_ON_LEFT.value] != BC_BLANK_VALUE
# Read : (none of them) OR (any of them but not the two to of them)
check_error( (not h_or_hmod and not qx_or_qy) or (h_or_hmod ^ qx_or_qy), \
f"At {ij_crd} {bc_values}, If you set Qx,Qy on left border then you can't set h/hmod.")
if bc_values[bci.BC_TABLE_INDEX_FOR_QX_ON_LEFT.value] != BC_BLANK_VALUE:
if nap[crd] != 0:
check_error( nap[crd[0],crd[1]-1] == 0, \
f"Setting a left border b.c. at {ij_crd} makes sense if leftside cell is out of computation domain")
else:
check_error( nap[crd[0],crd[1]-1] != 0, \
f"Setting a right border b.c. at {ij_crd} makes sense if leftside cell is inside of computation domain")
# ........................................................................
# Converting boundary conditions into GPU data
# ........................................................................
# Convert the noted cells' boundary conditions into a
# sequential array and pointing towards it.
if bc_descriptions:
bc_cells_array = []
for i,(cell_numpy_crd,bc_values) in enumerate(bc_descriptions.items()):
bc_cells_array.append(bc_values)
# Here is a tricky thing:
# If there are some b.c. on a cell, then we give it an index >= 1.
# If there are no b.c. we give the index 0.
# This way we can know if there are b.c. or not without
# looking at b.c. themselves. It's an optimisation.
# Moreover, don't forget that a right-border B.C. will
# be located outside of the active cells !
weak_boundary_cond[cell_numpy_crd] |= (i+1) << 1 # bit 1 is NAP.
logging.debug(f"weak b.c.: #{weak_boundary_cond[cell_numpy_crd]}: {cell_numpy_crd} -> {bc_values}")
bc_cells_array = np.array(bc_cells_array,dtype=np.float32)
#print(bc_cells_array)
#bc_cells_array = bc_cells_array.transpose()
logging.info(f"There are {len(bc_descriptions)} boundary conditions.")
else:
bc_cells_array = None
logging.debug(f"{100*(np.sum(weak_boundary_cond == 1)/(nxfin*nyfin)):.1f}% active meshes without BC in weak boundary conditions array")
if errors:
for error in errors:
logging.error(error)
if errors and fail_if_invalid_sim:
raise Exception("Errors detected, check log")
# ........................................................................
# Infiltration
# ........................................................................
if hasattr(infiltration, "my_Q") and infiltration.my_Q.size > 0:
infiltration_timings = infiltration.my_Q
nb_infiltration_lines = infiltration_timings.shape[0]
logging.debug("Infiltration timings array")
logging.debug(infiltration_timings)
assert np.all(np.diff(infiltration_timings[:,0]) > 0), f"The infiltration timings are not strictly increasing: {infiltration_timings[:,0]}"
if interp_infiltration == InfiltrationInterpolation.LINEAR:
assert infiltration_timings[0,0] == 0, f"The first infiltration time must be zero if you use interpolation"
assert nb_infiltration_lines >= 2, f"You need at least two data points in infiltrations if you use interpolation"
zones_numbers = np.unique(wolf2d.inf.array[wolf2d.inf.array > 0]).tolist()
logging.info(f"Infitration zones numbers are : {zones_numbers}")
used_zones = set()
for row in infiltration_timings:
for zone_ndx in range(1, row.shape[0]):
if row[zone_ndx] > 0:
used_zones.add(zone_ndx)
if used_zones != set(zones_numbers):
logging.warning(f"Mismatch between the zones in the infiltration table ({','.join(map(str,used_zones))}) and the zones in the infiltration map {','.join(set(map(str, zones_numbers))) or '[]'}.")
for zone_ndx in range(1,infiltration_timings.shape[1]):
a = wolf2d.inf.array == zone_ndx
b = np.sum(a)
if b == 0:
logging.warning(f"The zone {zone_ndx} doesn't appear in the infiltration map.")
# Compute zones sizes (once so we optimize a bit)
zones_sizes = {}
for zone_ndx in range(1, row.shape[0]):
a = wolf2d.inf.array == zone_ndx
b = np.sum(a)
zones_sizes[zone_ndx] = b
# Spread each infiltration's the water over its zone's meshes.
for row in infiltration_timings:
for zone_ndx in range(1, row.shape[0]):
b = zones_sizes[zone_ndx]
original_inf = row[zone_ndx]
if b > 0:
# Beware it's a surface !
row[zone_ndx] /= b * dxfin * dyfin
#logging.info(f"Infiltration t_start={row[0]:.1f} s, zone {zone_ndx} has {b} cells with {original_inf} m²/s => infiltration per cell = {row[zone_ndx]:.4g} m²/s")
logging.debug("Infiltration timings array after water dispatch")
logging.debug(infiltration_timings)
# print(wolf2d.inf.array)
# print(wolf2d.inf.array.dtype)
# -1 because we must be 0 based (and -xxxx means no zone)
# but wolf is 1 based
# FIXME Align the two
infiltration_zones_array=wolf2d.inf.array.transpose() - 1
# import matplotlib.pyplot as plt
# for row in infiltration_zones_array:
# print(row.tolist())
# plt.imshow(infiltration_zones_array)
# plt.show()
else:
infiltration_timings = None
infiltration_zones_array = None
# ........................................................................
# Admin
# ........................................................................
report_freq = get_report_frequency(wolf2d)
if interpret_freq_type_as_wolf and report_freq.type == SimulationDurationType.SECONDS:
simulation_duration=SimulationDuration.from_seconds(npas)
else:
simulation_duration=SimulationDuration.from_steps(npas)
logging.debug(f"Simulation will run for {npas} steps, records every {int(wolf2d.parameters._writing_frequency)} steps")
if noptpas == 1:
logging.debug("Optimized timestep")
tss = TimeStepStrategy.OPTIMIZED_TIME_STEP
if tstep:
logging.warning("An optimized timestep strategy doesn't need a timestep")
tstep = None
else:
logging.debug("Fixed timestep")
tss = TimeStepStrategy.FIXED_TIME_STEP
# water_height[:,:] = 0
# bathymetry[bathymetry < 0.1] = np.max(bathymetry)
# strong_boundary_cond[:,:] = -99999.0
# strong_boundary_cond[900,:] = 5.0
# strong_boundary_cond[0:200,:] = 0.1
# strong_boundary_cond[1535:1735,:] = 0.1
# strong_boundary_cond[nap == 0] = -99999.0
if glsim is not None:
reload_shaders = froude_max != glsim.froude_max
glsim.set_params(
width=nxfin,
height=nyfin,
dx=dxfin,
dy=dyfin,
ponderation_runge_kutta=ponderation,
courant=vncsouhaite,
froude_max=froude_max,
time_step_strategy=tss,
time_step=tstep,
simulation_duration=simulation_duration,
report_frequency=report_freq,
basefilename=Path(wolf2d.filenamegen).name,
tile_packing_mode=tiles_packing_mode,
optimized_indirection=optimize_indirection,
)
if reload_shaders:
# Have to relaod the shaders if some of their *const*
# have changed... (constants are part of the source code,
# they're not uniform parameters).
logging.debug("Reloading shaders")
glsim._shader_log_path = shader_log_path
else:
glsim = GLSimulation(width=nxfin,
height=nyfin,
dx=dxfin,
dy=dyfin,
ponderation_runge_kutta=ponderation,
courant=vncsouhaite,
froude_max=froude_max,
time_step_strategy=tss,
time_step = tstep,
max_steps=simulation_duration,
report_frequency=report_freq,
basefilename=Path(wolf2d.filenamegen).name,
tile_size=tile_size,
shader_log_path=shader_log_path,
tile_packing_mode=tiles_packing_mode,
optimized_indirection=optimize_indirection)
logging.info("Filling the GPU")
glsim.set_tile_size(tile_size)
glsim.set_buffers(
# Three times the same array of shape (h,w,3)
quantity_arrays=[np.dstack([water_height, qxbin, qybin]) for i in range(3)],
bathymetry_array=bathymetry,
# strong_bc_array=strong_boundary_cond, strong_bc_values_array=strong_boundary_cond_values,
manning_array=manning,
active_meshes_array=nap != 0, # FIXME not necessary, remove.
weak_bc_array=weak_boundary_cond,
bc_cells_array=bc_cells_array,
infiltration_zones_array=infiltration_zones_array,
infiltration_timings=infiltration_timings
)
glsim.complete_textures()
# FIXME These info should not be here. Maybe return values ?
if init_gl:
glsim._window_width, glsim._window_height = window_width, window_height
logging.info(f"Model load and GPU data upload took {time.time() - start_time:.1f}s")
return glsim