Source code for wolfgpu.cli

"""
Author: HECE - University of Liege, Stéphane Champailler, Pierre Archambeau
Date: 2024, 25

Copyright (c) 2024,25 University of Liege. All rights reserved.

This script and its content are protected by copyright law. Unauthorized
copying or distribution of this file, via any medium, is strictly prohibited.
"""

import sys

from wolfgpu.plots import plot_evolution_of_simulation

if not (
    (sys.version_info.major, sys.version_info.minor) >= (3, 11) and sys.version_info.micro >= 0
):
    print(
        f"""I run on python 3.11.x only. You have {sys.version}. Sorry :-(
Please note that Python 3.11 is in security maintenance. So the
latest installable version is 3.11.9. Later 3.11 releases require
you to build python from source yourself. Check:
https://www.python.org/downloads/release/python-3119/
"""
    )
    exit()

from enum import Enum
import logging
from pathlib import Path
from time import time, sleep
from datetime import datetime
from textwrap import wrap
import os
import re
import shutil
import argparse
import importlib.metadata

import tqdm
import numpy as np
import numpy.ma as ma
import wolfgpu.version
from wolfgpu.tile_packer import TilePackingMode

from wolfhece.mesh2d.wolf2dprev import prev_sim2D
from wolfhece.wolf_array import (
    WolfArray,
    WolfArrayMB,
    WolfArrayMNAP,
)

# FIXME Wolf trashes the locale, so I reset it to system wide locale.
import locale
locale.setlocale(locale.LC_ALL, '')

from wolfgpu.test_scenarios import (
    save_short_format,
    create_steady_flow_swimming_pool_bottom_to_top, scenario1, scenario4,
    scenario_cube_drop, scenario_drying, scenario_sine_ground_sine_water,
    scenario_small_movement, scenario_still_water, scenario_still_water_rocky_bed
)

from wolfgpu.SimulationRunner import SimulationRunner, MOSTLY_DRY_MESHES_THRESHOLD
from wolfgpu.loaders.wolf_loader import load_sim_to_gpu
from wolfgpu.loaders.simple_sim_loader import load_simple_sim_to_gpu
from wolfgpu.simple_simulation import InfiltrationInterpolation, ReportFrequencyType, SimulationDuration, SimulationDurationType, TimeStepStrategy, SimpleSimulation

from wolfgpu.glsimulation import (
    GLSimulation,
    TileOptimisation,
    TIME_SAMPLING_RESOLUTION,
    DEFAULT_TOTAL_NUMBER_OF_DRY_UP_OUTER_LOOPS,
    DEFAULT_DRY_UP_OUTER_LOOPS_POLICY,
    DryUpOuterLoopPolicy
)

from wolfgpu.wolf_utils import (
    write_simulation,
    set_report_frequency,
)

from wolfgpu.utils import (
    init_global_logging,
    delete_dir_recursion,
    nice_timestamp
)

from wolfgpu.results_store import PerformancePolicy


# from wolfgpu.wolf_utils import force_load # see force_load in 'prev_sim2D' class
# Done here to avoid the message from pygame
import pygame
from wolfgpu.gl_utils import init_pygame, init_glfw


# If version reported here is wrong, be aware of this: https://stackoverflow.com/questions/75007547/difference-between-version-pip-show-and-importlib-metadata-version
[docs] VERSION = importlib.metadata.version('wolfgpu')
from wolfgpu.version import __version__ # resolve(): Make the path absolute, resolving any symlinks.
[docs] DATA_DIR = Path(__file__).parent.resolve() / "data"
[docs] WOLF_CLI_EXE = DATA_DIR / "wolfcli.exe"
[docs] BENCHMARK_AGGREGATED_RESULT_FILE = Path(f"wolfgpu_benchmark_results_{__version__.replace('.','_')}_{datetime.strftime(datetime.now(),'%Y-%m-%d_%H_%M') }.csv")
[docs] DRY_UP_LOOPS_RE = re.compile("([0-9]+)(-)?")
# Test case: Crues/2021-07 Vesdre/CSC - Convention - ARNE/Data/Results/Verviers_repository/Simulations/scen_ref_corrected/Q25[50]_not_corctd # BIG_TEST=Path(__file__).parent / "data" / "Q50_not_corctd" # Crues\2021-07 Vesdre\CSC - Convention - ARNE\Data\Results\Q25\X1a - Hoegne - Tr 3 - Confluence Wayai - Forges Thiry
[docs] BIG_TEST = ( DATA_DIR / "X1a - Hoegne - Tr 3 - Confluence Wayai - Forges Thiry" )
[docs] POWER_MODE = os.name == "nt" and os.environ.get("USERNAME") == "StephaneC" and os.environ.get("COMPUTERNAME") == "FEDER1"
if POWER_MODE: import cProfile
[docs] LOGOS = [ r""" __ __ _ __ ___ ___ / / /\ \ \___ | |/ _| / _ \ / _ \/\ /\ \ \/ \/ / _ \| | |_ / /_\// /_)/ / \ \ \ /\ / (_) | | _/ /_\\/ ___/\ \_/ / \/ \/ \___/|_|_| \____/\/ \___/""", r""" ________ __ ___ _______ ______ _______ | | | |.-----.| |.' _| __| __ \ | | | | | || _ || || _| | | __/ | | |________||_____||__||__| |_______|___| |_______|""", r""" __ __ __ _____ __________________ ____ ___ / \ / \____ | |_/ ____\/ _____/\______ \ | \ \ \/\/ / _ \| |\ __\/ \ ___ | ___/ | / \ ( <_> ) |_| | \ \_\ \| | | | / \__/\ / \____/|____/__| \______ /|____| |______/ \/ \/ """, r""" __ __ _ __ ___ ___ _ _ \ \ / /__| |/ _|/ __| _ \ | | | \ \/\/ / _ \ | _| (_ | _/ |_| | \_/\_/\___/_|_| \___|_| \___/""" ]
[docs] def write_wolf_array(wa, generic_name: Path): # if's order is important because of inheritance. if isinstance(wa, WolfArrayMNAP): # suffix is not in the original filename, setting it ourselves. wa.filename = str(generic_name.with_suffix(".MNAP")) wa.write_all() elif isinstance(wa, WolfArrayMB): wa: WolfArrayMB wa.filename = str(generic_name.with_suffix(Path(wa.filename).suffix)) if len(wa.myblocks) >= 1: wa.write_all() elif isinstance(wa, WolfArray): wa.filename = str(generic_name.with_suffix(Path(wa.filename).suffix)) wa.write_all()
[docs] class Scenario(Enum):
[docs] TWO_ROOMS = 1
[docs] SWIMMING_POOL_WITH_HOLE = 2
[docs] TWO_SMALL_ROOMS = 3
[docs] LINEAR = 4
[docs] def bbox2(img): # From https://stackoverflow.com/questions/31400769/bounding-box-of-numpy-array rows = np.any(img, axis=1) cols = np.any(img, axis=0) rmin, rmax = np.where(rows)[0][[0, -1]] cmin, cmax = np.where(cols)[0][[0, -1]] return rmin, rmax, cmin, cmax
[docs] def bounding_polygon_xy(wa: WolfArray): # Bounding polygon is for *borders* which are # located on the left or bottom of each cell. rmin, rmax, cmin, cmax = bbox2(wa.array) # The coord conversion adds a 0.5 when covnerting. So # we adapt our coordinates so we have a bouding polygon # which is outside the data it surrounds. polygon = wa.get_xy_from_ij_array( np.array([(cmin - 1, rmin - 1), (cmin, rmax), (cmax, rmax), (cmax, rmin - 1)]) ) return polygon
[docs] def mask_border(wa: WolfArray): """ Set a new mask applying only to a one cell around the matrix. It is usefule because SUXY is not smart enough to handle borders which are on the very border of matrices.""" wa.mask_reset() m = np.zeros_like(wa.array, dtype=bool) m[0, :] = True m[:, 0] = True m[m.shape[0] - 1, :] = True m[:, m.shape[1] - 1] = True wa.array.mask = m wa.nbnotnull = wa.array.count()
[docs] def run_wolf_cli(destination_dir: Path): # destination_dir: wher WolfCLI is located # Return a handler on which one can .wait() import subprocess import shlex wcp = str(destination_dir / "wolfcli.exe") if os.path.exists(wcp): cmd = wcp + " run_wolf2d_prev genfile=simul" logging.info( f"Running wolf cli in {destination_dir}, as {' '.join(shlex.split(cmd, posix=False))}" ) return subprocess.Popen( shlex.split(cmd, posix=False), cwd=destination_dir, stdout=subprocess.DEVNULL, ) else: logging.error( f"Can't run wolfcli.exe because I can't find it at {destination_dir}" ) return None
[docs] def parse_dry_up_loops_parameter(param: str, glsim: GLSimulation): assert param is not None assert len(param.strip()) >= 1 m = DRY_UP_LOOPS_RE.match(param) if m and m.groups(): n = int(m.groups()[0]) if m.groups()[1] == "-": # at most N iteration (outer loops) assert n > 0, f"If you want no dry-up loops, then just specify `0`, not `{param}`" glsim.set_total_number_of_dry_up_outer_loops(n, DryUpOuterLoopPolicy.MAXIMUM) elif n == 0: # no iteration (outer loops) glsim.set_total_number_of_dry_up_outer_loops(n, DryUpOuterLoopPolicy.NO_DRY_UP) else: # exactly n iterations (outer loops) glsim.set_total_number_of_dry_up_outer_loops(n, DryUpOuterLoopPolicy.FIXED) else: raise Exception(f"The structure of the parameter -dry-up-loops is not correct. You gave '{param}'")
[docs] def force_cli_params_in_wolf_model(cli_args: argparse.Namespace, m: prev_sim2D): if cli_args.freq is not None: try: set_report_frequency(m, cli_args.freq) except ValueError as ex: logging.warning(f"I can't set the reporting frequency {cli_args.freq} into a Wolf model. This will be handled only on the GPU side.") if cli_args.npas is not None: m.parameters._nb_timesteps = int(cli_args.npas) if m.parameters._writing_mode == 1: logging.warning("You gave -npas but the work Wolf scenario has type frequency report = seconds. -npas will be interpreted as seconds by WolfCLI.") elif cli_args.sim_duration is not None: m.parameters._nb_timesteps = SimulationDuration.from_seconds(cli_args.sim_duration).duration if m.parameters._writing_mode == 0: logging.warning("You gave -sim-duration but the Wolf scenario has type frequency report = iterations. -sim-duration will interpreted as a number of iterations by WolfCLI.") if cli_args.optim_pas is not None: m.parameters._scheme_optimize_timestep = int(cli_args.optim_pas) if cli_args.dur is not None: m.parameters._timestep_duration = float(cli_args.dur) p = cli_args.ponderation if p is not None: if p == "euler": m.parameters._scheme_rk = 1. # logging.warning( # "I don't know how to set Euler-only on a Wolf simulation. But I do know on a GPU simulation, so I'll do that" # ) else: m.parameters._scheme_rk = p if cli_args.courant is not None: m.parameters._scheme_cfl = cli_args.courant if cli_args.manning is not None: m.frot.array[:, :] = cli_args.manning if cli_args.froude_max: assert cli_args.froude_max > 0.0, "Froude limit should be > 0" m.parameters.blocks[0]._froude_max = cli_args.froude_max
[docs] def force_params_in_gpu(cli_args: argparse.Namespace, gpu: GLSimulation): # Change parameters which can only be set in the GPU model (not in the Wolf model) # Well, we start with an exception: I set wolf parameters here. Why ? # Because those parameters are more flexible in the GPU than in Wolf. if cli_args.dur: gpu.set_fixed_time_step( cli_args.dur) if cli_args.froude_max: gpu.set_froude_limit(cli_args.froude_max) if cli_args.froude_bc_tolerance: gpu.set_froude_bc_tolerance(cli_args.froude_bc_tolerance) if cli_args.courant: gpu.set_courant( cli_args.courant) if cli_args.freq is not None: gpu.set_report_frequency( SimulationDuration.from_str(cli_args.freq)) if cli_args.npas is not None: gpu.set_simulation_duration( SimulationDuration.from_steps(cli_args.npas)) elif cli_args.sim_duration is not None: gpu.set_simulation_duration( SimulationDuration.from_seconds(cli_args.sim_duration)) if cli_args.optim_pas is not None: if int(cli_args.optim_pas) == 0: gpu.time_step_strategy = TimeStepStrategy.FIXED_TIME_STEP else: gpu.time_step_strategy = TimeStepStrategy.OPTIMIZED_TIME_STEP # if cli_args.freq is not None: # gpu.report_frequency = cli_args.freq p = cli_args.ponderation if p is not None: if p == "euler": gpu.set_euler_ponderation() else: gpu.set_rk_ponderation(p) if cli_args.infilerp is None: gpu.set_infiltration_interpolation(InfiltrationInterpolation.NONE) elif cli_args.infilerp == "linear": gpu.set_infiltration_interpolation(InfiltrationInterpolation.LINEAR) else: raise Exception(f"Unrecognized infiltration interpolation strategy: '{cli_args.infilerp}'") parse_dry_up_loops_parameter(cli_args.dry_up_loops, gpu) if cli_args.sim_duration: gpu.set_simulation_duration(SimulationDuration.from_seconds(cli_args.sim_duration))
[docs] def benchmark(destination_dir, cli_args): from wolfgpu.SimulationRunner import estimate_best_window_size if False and POWER_MODE: SIM_SIZES = [512,1024,2048] else: SIM_SIZES = [512,512,512,1024,1024,1024,2048,2048,2048,1024*4,1024*5,1024*6,1024*7,1024*8] if cli_args.ap is None: cli_args.ap = 0.8 # Make sure we measure performance of the simulator and not # of the reporting. cli_args.freq = "100000s" from OpenGL.GL import glGetIntegerv, GL_MAJOR_VERSION, GL_MINOR_VERSION, glGetString, GL_VERSION, GL_VENDOR, GL_SHADING_LANGUAGE_VERSION, GL_RENDERER with open(BENCHMARK_AGGREGATED_RESULT_FILE,"w") as res_out: pygame.init() pygame.display.set_mode((100, 100), pygame.OPENGL|pygame.DOUBLEBUF) glnfo = f"\"OpenGl version: {glGetIntegerv(GL_MAJOR_VERSION)}.{glGetIntegerv(GL_MINOR_VERSION)}; {glGetString(GL_VERSION).decode()}; {glGetString(GL_VENDOR).decode()} -- GL/SL:{glGetString(GL_SHADING_LANGUAGE_VERSION).decode() } -- {glGetString(GL_RENDERER).decode()}\"\n" pygame.quit() # Write some general info about the benchmark run. res_out.write(glnfo) from datetime import date today = date.today() res_out.write(f"\"{today.strftime('%B %d, %Y')}\"\n") for line in wolfgpu.version.multilines_version(): res_out.write(f"\"WolfGPU {line.strip()}\"\n") res_out.write(f'"Command: {" ".join(sys.argv[:])}"\n') import platform if platform.system() == "Windows": res_out.write(f"\"On {platform.system()}; computer: {os.getenv('COMPUTERNAME')}\"") else: res_out.write(f"\"On {platform.system()}\"") res_out.write("\n") res_out.write("\"Total Active Tiles: Total number of active tiles, cumulated over the whole simulation.\"\n") res_out.write("\"Est. T.A.T. : Estimated Total Active Tiles : an estimation of what should be the actual number of active tiles.\"\n") res_out.write("\"Real/Est. T.A.T. : Ratio of actual over estimated T.A.T.\"\n") res_out.write("\n") res_out.write("Nr,Start time,End time,Size,Nb iter,Iterations per second,Active Tiles,Total Time(s),Tiles/s,Total Active Tiles,Est. T.A.T.,Real/Est T.A.T.\n") res_out.flush() logging.info(f"Benchmark results will be stored in {BENCHMARK_AGGREGATED_RESULT_FILE.absolute()}") for sim_ndx, size in enumerate(SIM_SIZES): # FIXME For the moment, deleting GPU resources doesn't work # So I force a GPU flush by restarting pygame on each simulation. pygame.init() nfo = pygame.display.Info() window_width, window_height = estimate_best_window_size(nfo.current_w, nfo.current_h, min(SIM_SIZES), min(SIM_SIZES)) pygame.display.set_mode((window_width, window_height), pygame.OPENGL|pygame.DOUBLEBUF, vsync = 0) pygame.display.set_caption("WolfGPU - Benchmark") gameIcon = pygame.image.load( str(DATA_DIR / "wolf_logo.bmp") ) pygame.display.set_icon(gameIcon) # Why do we iconify ? Because we have noticed that the performance on # medium sized problems (1024,2048) are greatly improved when running # the benchmark on GPU1 over Remote Desktop Connection. pygame.display.iconify() print("-"*80) print(f"Benchmark {sim_ndx+1}/{len(SIM_SIZES)}") print("-"*80) cli_args.size = size # May seem a lot but necessary to have the water to cover the whole simulation surface # in bigger simulation cli_args.npas = max(2000,size*3) m = scenario4(destination_dir, N=int(cli_args.size), active_surface=float(cli_args.ap), optim_pas=True) glsim = load_sim_to_gpu(m, tile_size=cli_args.tile_size, interpret_freq_type_as_wolf=True, init_gl=False) force_params_in_gpu(cli_args, glsim) #glsim.set_total_number_of_dry_up_outer_loops(100, DryUpOuterLoopPolicy.MAXIMUM) simulation_runner = SimulationRunner( glsim, record_path=destination_dir / "gpu_results", early_out_delta_max=None, ) glsim._load_shaders() now = datetime.now() start_time = now.strftime("%H:%M:%S") simulation_runner.full_run(refresh_view=0.5) now = datetime.now() end_time = now.strftime("%H:%M:%S") gs = glsim.read_global_state() iter_from_time = simulation_runner.iter_per_second * simulation_runner.total_duration est_active_tiles = float((size**2) / (glsim.tile_size**2)) * iter_from_time tiles_per_sec = round(gs.total_active_tiles / simulation_runner.total_duration) res_out.write( f"{sim_ndx+1},{start_time},{end_time},{size},{cli_args.npas},{simulation_runner.iter_per_second:.2f},{gs.nb_active_tiles},{simulation_runner.total_duration:.1f},{tiles_per_sec},{gs.total_active_tiles},{est_active_tiles:.1f},{gs.total_active_tiles/est_active_tiles:.2f}\n" ) # Force write to disk. You need the two instructions (see python doc # for os.fsync()) res_out.flush() os.fsync(res_out.fileno()) glsim.drop_gl_resources() from wolfgpu.gl_utils import gl_clear_all_caches gl_clear_all_caches() pygame.quit() logging.info(f"Benchmark file is: {BENCHMARK_AGGREGATED_RESULT_FILE.absolute()}")
[docs] def main_func(): #from datetime import datetime global datetime #datetime.now() print(LOGOS[datetime.now().second % len(LOGOS)] + "\n") print(wolfgpu.version.long_version()) print("Copyright (c) 2023,2024 Hydraulics in Environmental and ") print(" Civil Engineering (HECE), University of Liège, Belgium") if datetime.today().month == 1 and datetime.today().day <= 14: print("Happy new year !") print("") BUILT_IN_SCENARIOS = [ "scenario_cube_drop", "scenario4", "small_movement", "scenario_sine_ground_sine_water", "scenario_still_water_rocky_bed", "still_water", "scenario_multiple_infiltration", ] # , "scenario1", "scenario_drying", "y_steady_influx"] max_term_width = min(80, shutil.get_terminal_size().columns) os.environ['COLUMNS'] = str(max_term_width) #shutil.get_terminal_size().columns) parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, epilog= "\n".join(wrap( "When running over a Wolf modelisation, the model is first " + "copied to a work directory. That's where you'll get your " + "results. We do that because we may run WolfCLI.exe and " + "thus override any previous results.", width=max_term_width)) + "\n\n" + "\n".join(wrap( "A bathymetry file is scanned regularly during the simulation. If you " + "update it, the simulation will integrate it on its next iteration. The " + "file must be a saved numpy array of np.float32, its name must be " + "'simul.top_update.npy' and located in the work directory (see info logs).", width=max_term_width))) parser.add_argument( "scenario", nargs="?", default=None, help=f"Path to Wolf modelisation directory (we expect 'simul' as generic file) or a built in scenario: {', '.join(BUILT_IN_SCENARIOS)}.", ) parser.add_argument("-o", "-output", default=None, type=str, help="Directory where to store results.", metavar="PATH", dest="output") #parser.add_argument("-help", action="store_true", help="Help") parser.add_argument( "-ap", default=None, type=float, help="Wet area proportion (0 to 1) of computation domain (used to parametrize some built in test scenarios).", ) parser.add_argument( "-size", default=None, type=int, help="Problem size (in meshes) (used to parametrize some built in test scenarios).", ) parser.add_argument( "-skip-write", action="store_true", default=False, help="Skip saving the scenario to disk (only for synthetic scenarios).", ) parser.add_argument( "-wolf", default=None, type=int, help="Set number of steps in WolfCLI and start WolfCLI.", ) parser.add_argument("-npas", default=None, type=int, help="Number of steps to do.") parser.add_argument("-sim-duration", default=None, type=str, help="Duration of the simulation (in simulation time). Mutually exclusive with -npas. " \ "The duration is given in seconds by default. It can also be given with a string " \ "such as 1h1m1s for 3661 seconds.") parser.add_argument( "-optim-pas", default=None, type=int, help="Force time step optimisation. 0 = no optimisation, 1 = optimisation.", ) parser.add_argument( "-dur", default=None, type=float, help="Time step duration (when not optimized).", ) parser.add_argument( "-ponderation", default=None, help="Force ponderation: 'euler' for single-euler (no ponderation), 0 < PONDERATION < 1 for RungeKutta 2nd order." " In 2nd order RK, the final equation is PONDERATION*estimator + (1-PONDERATION)*corrector.", ) parser.add_argument( "-infilerp", default=None, help="Type of infiltration interpolation. Can only be 'linear'. ", ) parser.add_argument( "-courant", default=None, type=float, help="Force Courant number." ) parser.add_argument( "-froude-max", default=None, type=float, help="Force Froude maximum." ) parser.add_argument( "-froude-bc-tolerance", default=None, type=float, help="Force Froude limited height boundary condition tolerance." ) parser.add_argument( "-manning", default=None, type=float, help="Force Manning coefficient." ) parser.add_argument( "-freq", type=str, default=None, help="If an integer number, one record every 'freq' iteration. If an integer number N suffixed by 's' (e.g. '10s'), one record every N seconds (measured in simulation time). " ) parser.add_argument( "-tile-size", default=16, type=int, help="Tile size, must be a power of 2" ) dry_up_loops_default = str(DEFAULT_TOTAL_NUMBER_OF_DRY_UP_OUTER_LOOPS) if DEFAULT_DRY_UP_OUTER_LOOPS_POLICY == DryUpOuterLoopPolicy.MAXIMUM and DEFAULT_TOTAL_NUMBER_OF_DRY_UP_OUTER_LOOPS >= 1: dry_up_loops_default += "-" parser.add_argument( "-dry-up-loops", type=str, default=dry_up_loops_default, metavar="N", help="Number of dry up outer loops. Outer loops are those used to solve dry ups across tiles." + \ " There are several possiblities for this value: 0 means no dry up corrections; N means an exact number of loops (for technical reason, these loops are also much faster); " + \ f"'N-' means up to N loops (will end earlier if all dry ups are solved). The default is '{dry_up_loops_default}'.", ) # parser.add_argument( # "-dry-up", # default="exact", # type=str, # help="Select the dry up management strategy", # ) parser.add_argument( "-no-tiles-packing", default=False, action="store_true", help="Disable tiles packing", ) parser.add_argument( "-early-out-threshold", default=None, type=float, help="If specified, will stop simulation if, for 3 records in a row, sum{all meshes m}(abs(m.h) + abs(m.qx) + abs(m.qy)) <= EARLY_OUT_THRESHOLD", ) parser.add_argument( "-start-from-last-record", default=False, action="store_true", help="Restart the sim from the last record", ) parser.add_argument( "-restart-from-record", type=int, help="Restart the sim from the n-th record (we insist: a record, not a step)", ) parser.add_argument( "-threshold-depth", nargs="?", const=1e-5, type=float, help="Clear all meshes where water depth is below the given level before starting simulation (default 1e-5m)", ) parser.add_argument( "-timeline", nargs=2, type=str, help="Plot the evolution of the simulation as reported in the results. First parameter is the path to the work directory (containing the Wolf model), second parameter is the path to the results (both as reported when starting the simulation).", ) parser.add_argument( "-benchmark", default=False, action="store_true", help=f"Run a full benchmark and report the results in a file called \"{BENCHMARK_AGGREGATED_RESULT_FILE}\".", ) parser.add_argument( "-debug", action="store_true", default=False, help="Activate debug logging" ) parser.add_argument( "-record-alphas", action="store_true", default=False, help="Enable the recording of the dry up management texture (a.k.a. alpha textures)", ) parser.add_argument( "-record-policy", default=None, type=PerformancePolicy, choices=list(PerformancePolicy), help=f"Results recording policy. '{PerformancePolicy.SPEED}' will record uncompressed arrays, allowing faster queries, '{PerformancePolicy.STORAGE}' (the default) is the opposite." ) parser.add_argument( "-version", action="store_true", default=False, help="Show information about this program", ) parser.add_argument( "-gpu-info", action="store_true", default=False, help="Show information about the GPU", ) parser.add_argument( "-quickrun", type=str, default="", metavar="PATH", help="Run a quick simulation from a SimpleSimulation object", ) parser.add_argument( "-no-validation", action="store_true", default=False, help="If set, failed checks made to ensure a simulation is valid will produce warnings instead of errors.", ) parser.add_argument( "-optimize-indirection", action="store_true", default=False, help="Activate tile indirection optimization for faster wet tiles detection (still being tested).", ) if POWER_MODE: parser.add_argument( "-shader-log", type=Path, default=None, metavar="PATH", help="(PowerMode) Enable logging of shader codegen to the given path.", ) parser.add_argument( "-profiler", default=False, action="store_true", help="Activate profiling", ) # parser.add_argument('-optim-tiles', choices=["CS","RS"], default=None, help="Enable or disable tile geometry shader GPU optimisation. CS=Compute shader, RS=Regular shader.") # Transform double hyphen in simple hyphen (allows one to use # Unix like arguments. for i in range(len(sys.argv)): if sys.argv[i] != "--help" and sys.argv[i].startswith("--"): sys.argv[i] = sys.argv[i][1:] cli_args = parser.parse_args() if cli_args.debug: init_global_logging(logging.DEBUG) else: init_global_logging(logging.INFO) assert not cli_args.wolf or not bool( cli_args.start_from_last_record ), "-npas-wolf incompatible with -start-from-last-record" assert not cli_args.wolf or not bool( cli_args.restart_from_record ), "-npas-wolf incompatible with -restart-from-record" assert not ( cli_args.start_from_last_record and cli_args.restart_from_record ), f"-start-from-last-record and -restart-from-record options are mutually exsclusive." assert (cli_args.sim_duration is None and cli_args.npas is None) or ((cli_args.sim_duration is not None) ^ (cli_args.npas is not None)), \ "-sim-duration and -npas parameters are mutually exclusive." assert not cli_args.optimize_indirection or (cli_args.optimize_indirection and not cli_args.no_tiles_packing), \ "For optimized indirection, you must have tile packing" if cli_args.scenario and cli_args.quickrun: logging.error( f"Using -quick-run and passing a scenario ({cli_args.scenario}) on the command line doesn't make sense. Choose one or the other." ) exit() if cli_args.scenario in BUILT_IN_SCENARIOS: if cli_args.sim_duration is not None and "s" in cli_args.sim_duration.lower(): logging.warning("The -sim-duration parameters set with time will not be written in the Wolf simulation data.") if cli_args.ponderation is not None: try: cli_args.ponderation = float(cli_args.ponderation) assert ( 0.0 < float(cli_args.ponderation) < 1.0 ), "Ponderation value is not correct. It should be between 0 and 1" except: # Not a float, so a string. cli_args.ponderation = cli_args.ponderation.lower() assert ( cli_args.ponderation == "euler" ), f"I don't recognize the ponderation : {cli_args.ponderation}" wolf_sim_path = cli_args.ponderation if cli_args.gpu_info: from wolfgpu.gl_utils import query_gl_caps, init_gl init_gl(10, 10) query_gl_caps() exit() if cli_args.version: print("\n".join(wolfgpu.version.multilines_version())) exit() if cli_args.benchmark: benchmark(Path.home() / "test_sim", cli_args) exit() if cli_args.timeline: plot_evolution_of_simulation( Path(cli_args.timeline[0]), Path(cli_args.timeline[1]) ) exit() if POWER_MODE and cli_args.scenario is None and not cli_args.quickrun: # FIXME Dirty shortcuts to develop faster (should put that in VSCode # .json config, but it's painful). cli_args.scenario = r"d:\StephaneC\Theux" if cli_args.scenario is None and not cli_args.quickrun: parser.print_help() exit() simulation_dir = None if cli_args.scenario in BUILT_IN_SCENARIOS: simulation_dir = (Path.home() / "test_sim" / cli_args.scenario).resolve() elif cli_args.scenario: simulation_dir = Path(cli_args.scenario).resolve() elif cli_args.quickrun: simulation_dir = Path(cli_args.quickrun).resolve() else: logging.error("Could not figure out simulation directory") exit() if cli_args.output: destination_dir = Path(cli_args.output).resolve() if destination_dir.exists(): assert not destination_dir.is_file(), f"The output directory ({destination_dir}) is a file ???" assert destination_dir.is_dir(), f"The output directory ({destination_dir}) is not a directory ?" assert simulation_dir.resolve() != destination_dir.resolve(), f"The result directory {simulation_dir} is the same as the model directory." else: destination_dir = simulation_dir / "simul_gpu_results" glsim = None if cli_args.scenario in BUILT_IN_SCENARIOS: logging.info("Building test scenario") if cli_args.ponderation is None: cli_args.ponderation = 0.3 if cli_args.courant is None: cli_args.courant = 0.25 if cli_args.manning is None: cli_args.manning = 0.04 if cli_args.scenario == "still_water": assert ( cli_args.ap is None ), "-ap is of no use for the cube drop scenario. Only -size is." if cli_args.size is None: cli_args.size = 128 # Perfectly still water m = scenario_still_water( simulation_dir, width=cli_args.size, height=cli_args.size, rockyness=0, water_height=np.float32(5.0), ) elif cli_args.scenario == "scenario_cube_drop": assert ( cli_args.ap is None ), "-ap is of no use for the cube drop scenario. Only -size is." if cli_args.size is None: cli_args.size = 128 m = scenario_cube_drop( simulation_dir, width=cli_args.size, height=cli_args.size ) elif cli_args.scenario == "scenario_sine_ground_sine_water": assert ( cli_args.ap is None ), "-ap parameter is not used for scenario_sine_ground_sine_water" if cli_args.size is not None: assert cli_args.size >= 100, "-size parameter must be >= 100" else: cli_args.size = 100 m = scenario_sine_ground_sine_water(simulation_dir, N=cli_args.size) elif cli_args.scenario == "scenario_still_water_rocky_bed": assert ( cli_args.ap is None ), "-ap parameter is not used for scenario_sine_ground_sine_water" if cli_args.size is not None: assert cli_args.size >= 10, "-size parameter must be >= 10" else: cli_args.size = 10 m = scenario_still_water_rocky_bed( simulation_dir, width=cli_args.size, height=cli_args.size ) elif cli_args.scenario == "scenario4": if cli_args.ap is None: cli_args.ap = 0.8 if cli_args.size is None: cli_args.size = 128 assert ( 0.5 <= cli_args.ap < 1.0 ), "Proportion of occupied meshes (-ap x) must me between >= 0.5 and < 1.0" assert ( cli_args.size >= 16 ), f"Size of problem (param. -size) must be >= 16, it is {cli_args.size}" m = scenario4( simulation_dir, N=int(cli_args.size), active_surface=float(cli_args.ap), optim_pas=True, ) elif cli_args.scenario == "small_movement": assert cli_args.ap is None, "-ap parameter is useless in this scenario" assert cli_args.size is None, "-size parameter is useless in this scenario" m = scenario_small_movement(simulation_dir) m.parameters._nb_timesteps = 300_000 # OK for rust, not on my PC m.parameters._scheme_optimize_timestep = 1 m.parameters._scheme_rk = 0.3 # Force some parameter cli_args.ponderation = m.parameters._scheme_rk cli_args.courant = m.parameters._scheme_cfl cli_args.manning = 0.04 elif cli_args.scenario == "scenario1": raise Exception( "This scenario uses boundary conditions which are still disabled" ) assert cli_args.size >= 32, "Size of problem (-size) must be >= 32" m = scenario1( simulation_dir, size=int(cli_args.size), bc_qx_factor=1.0, bc_hmod_factor=1.0, WATER_HEIGHT=10.0, IC_SPEED=1.0, friction=0.0, ) force_cli_params_in_wolf_model(cli_args, m) write_simulation(m, WOLF_CLI_EXE) elif cli_args.scenario == "scenario_drying": m = scenario_drying( simulation_dir, size=int(cli_args.size), WATER_HEIGHT=0.2, friction=0.0, top_topo=1.0, ) # import matplotlib.pyplot as plt # plt.imshow(m.top.array[2:-2,2:-2]) # plt.show() m.parameters._nb_timesteps = int(cli_args.npas) m.parameters._scheme_optimize_timestep = int(cli_args.optim_pas) m.parameters._timestep_duration = float(cli_args.dur) m.parameters._writing_frequency = cli_args.freq m.parameters._scheme_k_venkatakrishnan = 0.0 # was 1.0 m.parameters._scheme_centered_slope = 1 # was 2 write_simulation(m, WOLF_CLI_EXE) elif cli_args.scenario == "y_steady_influx": m = create_steady_flow_swimming_pool_bottom_to_top( simulation_dir, width=cli_args.size // 2, height=cli_args.size ) elif cli_args.scenario in BUILT_IN_SCENARIOS: m = locals()[cli_args.scenario](simulation_dir) elif not cli_args.quickrun: # User gives us a path to a regular, existing Wolf simulation wolf_sim_path = Path(cli_args.scenario) if not wolf_sim_path.exists(): logging.error(f"I can't find the scenario '{cli_args.scenario}'. I have looked in: {wolf_sim_path.absolute()}") exit() wolf_sim_path = wolf_sim_path.resolve() # Loading a regular Wolf modelisation assert ( cli_args.ap is None ), f"Setting -ap doesn't make sense when loading a Wolf scenario" assert ( cli_args.size is None ), f"Setting -size doesn't make sense when loading a Wolf scenario" if wolf_sim_path.is_file(): generic_file = wolf_sim_path.name wolf_sim_path = wolf_sim_path.parent else: generic_file = "simul" assert ( wolf_sim_path / generic_file ).exists(), f"I can't find {wolf_sim_path / generic_file}" logging.warning( "Assuming 'simul' is the name of the generic file in the Wolf directory" ) # When we use a built in scenario we write it as a WolfModel first # to be able to use the wolf model loader afterewards (to test it) if cli_args.scenario in BUILT_IN_SCENARIOS: if not cli_args.skip_write: write_simulation(m, WOLF_CLI_EXE) generic_file = "simul" # Load the wold modelisation in the GPU logging.info( f"Loading synthetic model at {str(simulation_dir / generic_file)}" ) m = prev_sim2D( str(simulation_dir / generic_file)) elif cli_args.scenario: logging.info(f"Loading Wolf model at {str(Path(cli_args.scenario) / generic_file)}") m = prev_sim2D( str(Path(cli_args.scenario) / generic_file) ) if cli_args.scenario: force_cli_params_in_wolf_model(cli_args, m) m.force_load() if cli_args.threshold_depth: candidates = m.hbin.array[:, :] > 0 msk = (m.hbin.array[:, :] < cli_args.threshold_depth) & candidates # from matplotlib import pyplot as plt # m.hbin.array[msk] = 1000 # plt.imshow(m.hbin.array) # plt.show() m.hbin.array[msk] = 0 logging.info( f"Thresholding water depth < {cli_args.threshold_depth} m. {np.sum(msk)} ({np.sum(msk)/np.sum(candidates):.3f}%) meshes were zeroed." ) if POWER_MODE and (cli_args.shader_log is not None and not cli_args.shader_log.exists()): cli_args.shader_log.mkdir(parents=True) shader_log_path = cli_args.shader_log else: shader_log_path = None if cli_args.no_tiles_packing: tiles_packing_mode = TilePackingMode.TRANSPARENT else: tiles_packing_mode = TilePackingMode.REGULAR if cli_args.scenario in BUILT_IN_SCENARIOS: if cli_args.scenario == "scenario4": report_label = f"scenario4_portion_{int(float(cli_args.ap)*10)}" else: report_label = re.sub( "_+", "_", Path(cli_args.scenario).name.replace(" ", "_").replace("-", "_"), ) else: report_label = "report_sim" if cli_args.quickrun: if not Path(cli_args.quickrun).exists(): logging.error("You must give an existing SimpleSimulation directory to -quickrun") exit() sim_path = Path(cli_args.quickrun).resolve() logging.info(f"Loading simple simulation {sim_path}") simple_simulation = SimpleSimulation.load(sim_path) if cli_args.manning is not None: simple_simulation.manning[:, :] = cli_args.manning page_flipper = init_pygame(simple_simulation.param_nx, simple_simulation.param_ny, iconify=True) glsim = load_simple_sim_to_gpu( simple_simulation, tile_size=cli_args.tile_size, shader_log_path=shader_log_path, tiles_packing_mode=tiles_packing_mode, optimize_indirection=cli_args.optimize_indirection, fail_if_invalid_sim=not cli_args.no_validation ) else: page_flipper = init_pygame(m.parameters.nbx, m.parameters.nby, iconify=True) glsim = load_sim_to_gpu( m, tile_size=cli_args.tile_size, shader_log_path=shader_log_path, interpret_freq_type_as_wolf=True, tiles_packing_mode=tiles_packing_mode, optimize_indirection=cli_args.optimize_indirection, init_gl=False, fail_if_invalid_sim=not cli_args.no_validation ) glsim.set_optim_geom_shaders(TileOptimisation.COMPUTE_SHADER) force_params_in_gpu(cli_args, glsim) pygame.display.set_caption("WolfGPU") gameIcon = pygame.image.load( str(DATA_DIR / "wolf_logo.bmp") ) pygame.display.set_icon(gameIcon) # if not cli_args.skip_write: # if cli_args.scenario in BUILT_IN_SCENARIOS: # short_normat_name = cli_args.scenario # elif cli_args.scenario: # short_normat_name = wolf_sim_path.name # save_short_format(m, glsim, simulation_dir / short_normat_name) # FIXME Rename to GPUSimulationRunner simulation_runner = SimulationRunner( glsim, record_path=destination_dir, early_out_delta_max=cli_args.early_out_threshold, page_flip_func=page_flipper, enable_alpha_recording=cli_args.record_alphas, record_policy=cli_args.record_policy ) if not cli_args.start_from_last_record and not cli_args.restart_from_record: res_dir = Path(simulation_runner.record_file()) logging.info(f"Clearing the results directory {res_dir}") if os.path.exists(res_dir): delete_dir_recursion(res_dir) # shutil.rmtree(destination_dir) if not os.path.exists(res_dir): os.makedirs(res_dir) # from tests.arrayview import array_view # array_view(m.napbin.array.data) if cli_args.quickrun: print_sim_params(glsim, simulation_runner, simple_simulation.manning, simple_simulation.nap, simple_simulation.h) else: print_sim_params(glsim, simulation_runner, m.frot.array.data, m.napbin.array.data, m.hbin.array.data) logging.info(f"GPU Results directory : {simulation_runner.record_file()}") logging.info(f"Bathymetry update file is scanned at {simulation_runner.scanned_file()}") logging.info( "Simulating... Hit 'q' on the simulation view to abort (ctrl-c is no good)" ) # cProfile.run('simulation_runner.full_run(refresh_view=0.5)', 'stats') if cli_args.record_policy is not None and (cli_args.start_from_last_record or cli_args.restart_from_record): raise Exception("When restarting a simulation, it is useless to set the -record-policy because it must be the same as the one used in the restarted simulation results store") if cli_args.start_from_last_record: simulation_runner.restart_from_record() elif cli_args.restart_from_record: simulation_runner.restart_from_record(cli_args.restart_from_record) if ( cli_args.start_from_last_record or cli_args.restart_from_record ) and cli_args.npas: assert cli_args.npas > simulation_runner.step_num, ( "The -npas must be the *total* number of steps in the " "simulation (not a number of steps you want to add on top of what's " "already done). The simulation you pointed at currently has " f"{simulation_runner.step_num} iterations." ) if POWER_MODE and cli_args.profiler: logging.info("Profiler is on") with cProfile.Profile() as pr: simulation_runner.full_run(refresh_view=0.1) pr.dump_stats("stats") else: logging.info("Profiler is off") simulation_runner.full_run(refresh_view=0.1) # from gl_utils import read_texture, GL_COLOR_ATTACHMENT0, GL_R32I # print(read_texture(simulation_runner._glsim._infiltration_fb, GL_COLOR_ATTACHMENT0, simulation_runner._glsim.width, simulation_runner._glsim.height, GL_R32I)) pygame.display.quit() if cli_args.wolf is not None: # Clean tqdm's output # Maybe useless: https://github.com/tqdm/tqdm/issues/737 # sys.stdout.flush() # print("") # Prepare the wolf modelisation to be run by WolfCLI generic_path = Path(m.mydir) / Path(m.filenamegen) m.parameters._nb_timesteps = cli_args.wolf m.parameters.write_file(fn=str(generic_path)) # # FIXME VERY BIG HACK See # # https://gitlab.uliege.be/HECE/HECEPython/-/issues/38 # with open(str(generic_path) + ".par", "a") as f: # for b in range(1, m.myparam.nblocks + 1): # f.write(f"{b}\n") # for b in range(m.myparam.nblocks): # f.write(f'""\n') wolf_cli_process = run_wolf_cli(Path(m.mydir)) REG = re.compile(r"\*s\*3 ITERATION : +([0-9]+)") REG_CPU = re.compile(r"\*s\*1 TEMPS TOTAL CPU : +([0-9.]+(E-[0-9]+)?)") last_wolfcli_iter = None logging.info(f"Watching {Path(destination_dir)/'simul.NFO'}") # model: prev_sim2D = prev_sim2D(dir=str(destination_dir), splash=False, in_gui=False) # sys.stdout trick, see : https://stackoverflow.com/questions/45862715/how-to-flush-tqdm-progress-bar-explicitly progress_bar = tqdm.tqdm(range(cli_args.wolf), leave=False, file=sys.stdout) nb_errors = 0 wolf_nfo = Path(destination_dir) / "simul.NFO" while wolf_cli_process.poll() is None: try: if wolf_nfo.exists() and wolf_nfo.stat().st_size > 1024: with open(Path(destination_dir) / "simul.NFO", "rb") as file: file.seek(-1024, os.SEEK_END) s = file.read().decode() match = REG.findall(s) if match: i = match[-1] progress_bar.update(int(i) - progress_bar.n) except Exception as ex: logging.error(ex) nb_errors += 1 if nb_errors >= 5: logging.error( "Too many errors while watching WolfCLI.exe running. Did wolfcli.exe accept the modelisation ? Aborting." ) wolf_cli_process.kill() exit() sleep(1) # Clean tqdm's output sys.stdout.flush() print("") with open(Path(destination_dir) / "simul.NFO", "rb") as file: nfo = file.read().decode(encoding="ISO-8859-15") match = REG_CPU.findall(nfo) assert match, "I'm expecting something in wolfcli.exe's .NFO file" # Remove the setup time of WolfCLI cpu_time = float(match[-1][0]) - float(match[0][0]) with open("report.txt", "a") as fout: gpu_iter_per_sec = max(1, simulation_runner.iter_per_second) wolfcli_iter_per_sec = max(1, float(cli_args.wolf) / cpu_time) accel = gpu_iter_per_sec / wolfcli_iter_per_sec wolf_sim_path = f"Total time CPU WolfCLI {cpu_time:.3f}s for {cli_args.wolf} iterations. => {wolfcli_iter_per_sec:g} it/s. GPU = {gpu_iter_per_sec:.2f} it/s. Accel={accel:.1f}\n" fout.write(wolf_sim_path) logging.info(wolf_sim_path) """ FIXME Now the question is : how do I run that in a way that I don't block WolfGUI ? - threads: the GIL will block anything CPU bound and I wonder if OpenGL counts as IO bound... - multiprocess: fine but howdir do I communictate progress information ? can I access the OpenGL context ? """
if __name__ == "__main__": main_func()