From a7552ac3f84514342585004977e6c4c00bae8dcb Mon Sep 17 00:00:00 2001 From: Steven van der Linden Date: Sat, 10 May 2025 20:48:08 +0200 Subject: [PATCH] documentation for IBM --- book/_toc.yml | 6 +- book/developers/usermanual.md | 2 +- book/intro.md | 2 +- book/running/compilation.ipynb | 12 +- book/running/ibm.ipynb | 299 ++++++++++++++++++++++++++++++++- book/running/papers.ipynb | 6 + book/running/settingup.ipynb | 2 +- book/running/tracers.001.nc | Bin 7152 -> 7152 bytes 8 files changed, 315 insertions(+), 14 deletions(-) diff --git a/book/_toc.yml b/book/_toc.yml index de517f2..04bb4b5 100644 --- a/book/_toc.yml +++ b/book/_toc.yml @@ -17,16 +17,14 @@ parts: - caption: Advanced DALES options chapters: - file: running/tracers.ipynb + - file: running/ibm.ipynb # - file: running/chemistry.ipynb # - file: running/ibm.ipynb # - file: running/openbcs.ipynb - file: running/gpu.md - - caption: Options outside main branch (for now) - chapters: - - file: running/ibm.ipynb - caption: Recent publications using DALES chapters: - file: running/papers.ipynb - - caption: Contributing + - caption: Contributing to the documentation chapters: - file: developers/usermanual.md \ No newline at end of file diff --git a/book/developers/usermanual.md b/book/developers/usermanual.md index e94e8cc..01a0444 100644 --- a/book/developers/usermanual.md +++ b/book/developers/usermanual.md @@ -1,6 +1,6 @@ # User Manual -The DALES user manual is built using [Jupyter Book](https://jupyterbook.org/en/stable/intro.html). In short, Jupyter Book allows you to easily build a website from Markdown files and/or Jupyter Notebooks. This page will explain the very basics of adding new content to the user manual. +The DALES documentation is built using [Jupyter Book](https://jupyterbook.org/en/stable/intro.html). In short, Jupyter Book allows you to easily build a website from Markdown files and/or Jupyter Notebooks. This page will explain the very basics of adding new content to the documentation. ## Prerequisites diff --git a/book/intro.md b/book/intro.md index 1394aab..e292409 100644 --- a/book/intro.md +++ b/book/intro.md @@ -9,7 +9,7 @@ This documentation is a work-in-progress and for a future release of DALES. The aim of this manual is to provide an up-to-date starting point for new users, explaining how to compile, setup and run the Dutch Atmospheric Large-Eddy Simulation. The manual will cover, among other things, general settings, and options (for example, statistics output), and instructions how to make more advanced cases (for example, simulations with tracers). The principles of the DALES model are described in [Heus et al., 2010](https://doi.org/10.5194/gmd-3-415-2010). ```{note} -This manual is written for the main-branch (v. X.Y) in mind. Other branches may contain new features that are not necessarily described in this manual. +This manual is written for the main-branch (v5.0) in mind. Other branches may contain new features that are not necessarily described in this manual. ``` At a later stage, this manual may be extended with in-depth information on code structure, variable naming schemes, and implementation details, better enabling people to become _new contributors_ to DALES. diff --git a/book/running/compilation.ipynb b/book/running/compilation.ipynb index 918f5b7..056d951 100644 --- a/book/running/compilation.ipynb +++ b/book/running/compilation.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "(sec:compilation)=", + "(sec:compilation)=\n", "# Compilation of DALES\n", "\n", "## Generic compilation\n", @@ -34,11 +34,11 @@ "\n", "| Option | Description | Allowed values | Default |\n", "| ------ | ----------- | -------------- | ------- |\n", - "| `-DENABLE_FFTW` | Build with FFTW | On/Off | On |\n", - "| `-DENABLE_HYPRE` | Build with HYPRE | On/Off | Off |\n", - "| `-DENABLE_FP32_FIELDS` | Use single precision floating-point numbers for prognostic fields (momentum, temperature, et cetera) | On/Off | Off |\n", - "| `-DENABLE_FP32_POIS` | Use single precision floating-point numbers for the Poisson solver | On/Off | Off |\n", - "| `-DENABLE_ACC` | Build with GPU support through OpenACC | On/Off | Off |\n", + "| `-DENABLE_FFTW` | Build with FFTW | True/False | True |\n", + "| `-DENABLE_HYPRE` | Build with HYPRE | True/False | False |\n", + "| `-DENABLE_FP32_FIELDS` | Use single precision floating-point numbers for prognostic fields (momentum, temperature, etc.) | True/False | False |\n", + "| `-DENABLE_FP32_POIS` | Use single precision floating-point numbers for the Poisson solver | True/False | False |\n", + "| `-DENABLE_ACC` | Build with GPU support through OpenACC | True/False | False |\n", "\n", "```{caution}\n", "To use HYPRE or FFTW, the library needs to both be enabled at compilation and selected at runtime by setting the &SOLVER section of the namoptions input file. `FIX LINK: See Alternative Poisson solvers`.\n", diff --git a/book/running/ibm.ipynb b/book/running/ibm.ipynb index abee15a..203943e 100644 --- a/book/running/ibm.ipynb +++ b/book/running/ibm.ipynb @@ -6,7 +6,304 @@ "source": [ "# Immersed Boundary Method (IBM)\n", "\n", - "TO WRITE" + "Starting from version 5.0, DALES has a grid-conforming Immersed Boundary Method (IBM) implemented. This implementation is a modified version of an older implementation (see [Tomas, 2016](https://resolver.tudelft.nl/uuid:5d93a697-be49-4f63-b871-b763bc327139)). This section explains the IBM implementation, how to prepare a run with immersed boundaries, and how to start it. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## General description of the IBM\n", + "The Immersed Boundary Method implemented in DALES is _grid conforming_, meaning that a computational grid cell is either completely set as _free fluid_ or _solid object_. This effectively changes complex shapes (e.g., inclined roofs) to a cubical representation, which may be innaccurate when the resolution is much coarser than the size of such shapes. The benefit is however its ease of implementation, incl. control over the fluxes and velocity values. Boundaries between fluid and solid always coincide with a cubic face at which the corresponding velocity component is set to zero, thereby ensuring no advection over such boundaries occurs. Momentum and scalar values adjacent to solid cells are corrected by locally adapting the tendencies in two steps:\n", + "1. regular contributions to the diffusive flux over faces (as if no solid were present) are substracted;\n", + "2. new contributions as a result of wall drag or wall flux are calculated and added.\n", + "\n", + "No correction for subgrid TKE is applied as diffusive flux contribution over walls is assumed to be negligble (which may require further testing). Tendencies for values inside solids are every substep set to correct for any drift, for example, for temperature ```thlp(i,j,k) = (thlibm - thl0(i,j,k))*rk3coefi```.\n", + "\n", + "

<< still add sketch >>

\n", + "\n", + "Currently, boundary conditions for solid walls are still limited. They are listed below:\n", + "* **temperature:** Dirichlet condition (fixed temperature) for or zero heat flux across vertical walls and roofs. These values are set to separate constant values for walls ```thlwall``` and roofs ```thlroof```, allowing some thermal response between fluid and solid.\n", + "* **moisture:** No flux across walls and roofs.\n", + "* **scalars:** No flux across walls and roofs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preparing the IBM file\n", + "Due to the grid conformity of the IBM, only obstacle heights are required to use the IBM. That implies overhanging structures are not allowed, meaning no fluid cell can be positioned vertically below a solid cell. Obstacle heights should be supplied as a list of values in a file named ```ibm.inp.```, preceded by seven header lines (see example below). These values are ordered in a row-major ordering, with the first value corresponding to the top-left corner of the domain ```(xmin,ymax)```, the second value being one position to the right ```(xmin+1*dx,ymax)```, and the last value corresponding to the bottom-right corner ```(xmax,ymin)```. \n", + "```{code} python\n", + "# EXPERIMENT: ibm example\n", + "# Horizontal grid points Nx = 256, Ny = 256\n", + "# Horizontal grid size Delta x = 5.00, Delta y = 5.00\n", + "#\n", + "#\n", + "#\n", + "#\n", + " 0.0\n", + " 0.0\n", + " 5.7\n", + " 5.7\n", + " 6.8\n", + " 5.0\n", + " 0.0\n", + " .\n", + " .\n", + "```\n", + "Although the structure of this input file is straightforward, creating it requires some steps. Data of the desired obstacles first need to be acquired and then processed to get the correct input file. \n", + "\n", + "#### Using Wavefront OBJ format\n", + "The following example assumes that your buildings are represented in the Wavefront OBJ format. Building representation in OBJ format can be obtained directly via https://3dbag.nl for the Netherlands **or** be created via the open-source software [City4CFD](https://github.com/tudelft3d/City4CFD). Note that the second option is preferred when there is also topography involved and a high degree of accuracy is required. Further, the example assumes that the coordinates are specified in the Dutch RD coordinate system (_Rijksdriehoeksmeting_). Extension to different coordinates is trivial as long as an orthogonal coordinate system is used. \n", + "\n", + "```{code} python\n", + "##### IMPORT PACKAGES #####\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import math \n", + "import trimesh # <-- this package is required to handle OBJ files!\n", + "\n", + "##### SPECIFY GENERAL SETTINGS #####\n", + "\n", + "iexpnr = '001' \n", + "building_file = 'Buildings.obj'\n", + "terrain_file = 'Terrain.obj'\n", + "\n", + "dx = 5. # horizontal grid spacing in meters\n", + "Nx = 256 # number of grid cells in x-direction\n", + "Ny = 256 # number of grid cells in y-direction\n", + "bz = 8 # number of blend-out cells around edge, where no buildings will be placed\n", + "\n", + "origin_obj = [139751, 455487] # RD-COORDINATES of the center of domain (as in OBJ file)\n", + " # this example is situated on the Utrecht University campus\n", + " # alternatively, if these are lower-left of the OBJ file, \n", + " # origin_shift below should be set to zero\n", + "float_type = np.float64\n", + "\n", + "##### PROCESSING DATA #####\n", + "\n", + "origin_shift = [-dx*Nx/2, -dx*Ny/2] # shift from origin_obj to lower left corner of domain\n", + "origin_les = np.asarray(origin_obj) + np.asarray(origin_shift)\n", + "\n", + "# Import OBJ files containing meshes and shift them\n", + "mesh1 = trimesh.load(building_file)\n", + "\n", + "mesh1.vertices[:,0] = mesh1.vertices[:,0] - origin_shift[0]\n", + "mesh1.vertices[:,1] = mesh1.vertices[:,1] - origin_shift[1]\n", + "\n", + "mesh2 = trimesh.load(terrain_file)\n", + "\n", + "mesh2.vertices[:,0] = mesh2.vertices[:,0] - origin_shift[0]\n", + "mesh2.vertices[:,1] = mesh2.vertices[:,1] - origin_shift[1]\n", + "\n", + "# Combine building and terrain meshes in one mesh\n", + "mesh = trimesh.boolean.union([mesh1, mesh2], engine='blender', check_volume=False)\n", + "\n", + "# Voxelize the OBJ-mesh to computational grid spacing dx\n", + "voxgrid = mesh.voxelized(dx).hollow()\n", + "\n", + "# Create region of interest for LES\n", + "end = [Nx*dx, Ny*dy]\n", + "xh_les = np.linspace(dx/2, end[0]-dx/2, Nx) \n", + "yh_les = np.linspace(dy/2, end[1]-dy/2, Ny) \n", + "\n", + "# Prepare arrays for obstacle heights. Mind the orientation:\n", + "# Voxgrid structure orders data as increasing x, then y (xy-ordering); start bottom-left\n", + "# IBM input file should however start at top-left (largest y, smallest x; row-major) \n", + "# Column indices 'j' correspond to the x-dimension and row indices 'i' to y-dimension \n", + "# DALES itself corrects for this again by reading the file upside-down\n", + "# There is potential to correct for this code-wise (which has not been done yet)\n", + "\n", + "data_les = np.zeros((Ny,Nx), dtype=float)\n", + "mask_2d = np.zeros((Ny,Nx), dtype=bool)\n", + "\n", + "for bb in range(voxgrid.points.shape[0]):\n", + " xpos = voxgrid.points[bb,0]\n", + " ypos = voxgrid.points[bb,1]\n", + " zhei = voxgrid.points[bb,2]\n", + "\n", + " ii_pos = Ny - int(np.floor(ypos/dy))\n", + " jj_pos = int(np.floor(xpos/dx))\n", + "\n", + " if(bz<=jj_pos= dx)\n", + "\n", + "print('Maximum height in area: {:>3.1f}'.format(np.max(data_les)))\n", + "\n", + "##### WRITE IBM INPUT FILE #####\n", + "do = data_les.ravel() # first meshgridded and then converted to list in right direction\n", + "\n", + "f = open('ibm.inp.'+str(iexpnr),'w')\n", + "\n", + "# Ensure 7 lines of text heade\n", + "f.write('# EXPERIMENT: ibm example)\n", + "f.write('# Horizontal grid points Nx = {:>4d}, Ny = {:>4d}\\n'.format(Nx,Ny))\n", + "f.write('# Horizontal grid size Delta x = {:>6.2f}, Delta y = {:>6.2f}\\n'.format(dx,dx))\n", + "f.write('#\\n')\n", + "f.write('#\\n')\n", + "f.write('#\\n')\n", + "f.write('#\\n')\n", + "\n", + "for nn in range(do.shape[0]):\n", + " line = '{:>6.1f}\\n'.format(do[nn])\n", + " f.write(line)\n", + "\n", + "f.close() \n", + "\n", + "##### OPTIONAL: PLOT VOXELIZED BUILDINGS #####\n", + "x_mesh, y_mesh = np.meshgrid(xh_les,yh_les,indexing='xy') \n", + "y_mesh = np.flipud(y_mesh)\n", + "\n", + "xp = x_mesh[mask_2d].ravel() # like flatten, make continuous vector of xdata\n", + "yp = y_mesh[mask_2d].ravel() \n", + "zp = data_les[mask_2d].ravel() \n", + "b0 = np.zeros(zp.shape[0])\n", + "\n", + "fig = plt.figure()\n", + "ax = fig.add_subplot(projection='3d')\n", + "ax.bar3d(xp, yp, b0, dx, dy, zp, shade=\"True\")\n", + "ax.set_xlim3d(origin_les[0],origin_les[0]+dx*Nx)\n", + "ax.set_ylim3d(origin_les[1],origin_les[1]+dx*Ny)\n", + "ax.set_aspect('equal')\n", + "plt.axis('off')\n", + "ax.view_init(elev=30, azim=-0)\n", + "\n", + "plt.show()\n", + "\n", + "```\n", + "\n", + "#### Alternative: directly from LiDAR\n", + "Sometimes OBJ files are not readily available and creating them yourself with City4CFD is not feasible. Luckily, many authorities nowadays have made accurate LiDAR scans (point clouds) publicly available. For example, tiled LiDAR scans for the Netherlands (AHN) can accessed via [GeoTiles](https://geotiles.citg.tudelft.nl/) and tiled scans for France can be accessed via [IGN France](https://geoservices.ign.fr/lidarhd). Such LiDAR scans can be quickly voxelized for use with our IBM. Further, most LiDAR return points have a classification, which makes it possible to separate buildings from ground points. The table below shows the typical classification used by the Dutch AHN scans (which may vary among different institutes). \n", + "\n", + "| Classification value | Meaning |\n", + "| ------------ | ------- |\n", + "| `1` | unclassified (mostly vegetation) \n", + "| `2` | ground points | \n", + "| `6` | building points | \n", + "| `9` | water points | \n", + "| `26` | reserved for ASPRS definition| \n", + "\n", + "The example below shows the basics of processing such LiDAR scans. Note that the size of the domain is inferred from the dimensions of the LiDAR tile (assumed to be in meters). If you want only part of the domain, you will have to subsample ```data_les``` yourself. Likewise, if you want to simulate a larger region, you have to merge LiDAR tiles yourself. \n", + "\n", + "_Thanks to Mahmoud Ahmed (TUDelft) for sharing his Python script._ \n", + "```{code} python\n", + "##### IMPORT PACKAGES #####\n", + "\n", + "import math\n", + "import matplotlib.pyplot as plt\n", + "import open3d as o3d\n", + "import numpy as np\n", + "import laspy as lp # <-- this package should be installed with laz backend\n", + " # pip3 install \"laspy[lazrs,laszip]\"\n", + "\n", + "##### SPECIFY INPUT FILE AND VOXEL SIZE (=GRID SIZE) #####\n", + "\n", + "file = '37EN2_11.LAZ' # AHN3 Wippolder area in Delft, downloaded from GeoTiles \n", + "dx = 10.\n", + "\n", + "##### PROCESS LIDAR POINT CLOUDS #####\n", + "\n", + "point_cloud = lp.read(file)\n", + "\n", + "# Optional: only extract one or two classes. In Dutch AHN, buildings correspond to class 6\n", + "point_cloud = point_cloud.points[point_cloud.classification == 6]\n", + "\n", + "pcd = o3d.geometry.PointCloud()\n", + "\n", + "pcd.points = o3d.utility.Vector3dVector(\n", + " np.vstack((point_cloud.x, point_cloud.y, point_cloud.z)).transpose())\n", + "\n", + "pcd.colors = o3d.utility.Vector3dVector(\n", + " np.vstack((point_cloud.red, point_cloud.green, point_cloud.blue))\n", + " .transpose()/255)\n", + "\n", + "# Create voxels from point cloud\n", + "voxel_grid = o3d.geometry.VoxelGrid.create_from_point_cloud(pcd, voxel_size=dx)\n", + "voxels = voxel_grid.get_voxels()\n", + "\n", + "# Determine number of voxels in x- and y-directions\n", + "Lx = voxel_grid.get_oriented_bounding_box().extent[0]\n", + "Ly = voxel_grid.get_oriented_bounding_box().extent[1]\n", + "\n", + "Nx = math.floor(Lx/dx) if ( math.floor(Lx/dx) % 2 == 0 ) else math.floor(Lx/dx)-1\n", + "Ny = math.floor(Ly/dx) if ( math.floor(Ly/dx) % 2 == 0 ) else math.floor(Ly/dx)-1\n", + "\n", + "# Prepare arrays for obstacle heights and coordinate meshes\n", + "data_les = np.zeros((Ny,Nx), dtype=float)\n", + "mask_2d = np.zeros((Ny,Nx), dtype=bool)\n", + "\n", + "xh_les = np.linspace(dx/2, Nx*dx-dx/2, Nx) \n", + "yh_les = np.linspace(dx/2, Ny*dx-dx/2, Ny) \n", + "\n", + "for v in voxels:\n", + " if (v.grid_index[0] >= Nx or v.grid_index[1] >= Ny): continue\n", + " ii_pos = (Ny - 1) - v.grid_index[1]\n", + " jj_pos = v.grid_index[0] \n", + " zhei = (v.grid_index[2] + 1) * dx # dx is also voxel size\n", + "\n", + " data_les[ii_pos,jj_pos] = max(data_les[ii_pos,jj_pos], zhei)\n", + " mask_2d [ii_pos,jj_pos] = True\n", + "\n", + "##### OPTIONAL: PLOT VOXELIZED POINT CLOUD #####\n", + "\n", + "# Visualize voxels\n", + "o3d.visualization.draw_geometries([voxel_grid])\n", + "\n", + "##### OPTIONAL: EXPORT THE MESH AS .PLY #####\n", + "# vox_mesh = o3d.geometry.TriangleMesh()\n", + "\n", + "# for v in voxels:\n", + "# cube = o3d.geometry.TriangleMesh.create_box(width=1, height=1, depth=1)\n", + "# cube.paint_uniform_color(v.color)\n", + "# cube.translate(v.grid_index, relative=False)\n", + "# vox_mesh += cube\n", + "\n", + "# # Export the mesh as an .obj or .ply file\n", + "# vox_mesh.translate([0.5, 0.5, 0.5], relative=True)\n", + "# vox_mesh.scale(dx, [0, 0, 0])\n", + "# vox_mesh.translate(voxel_grid.origin, relative=True)\n", + "\n", + "# o3d.io.write_triangle_mesh(file+\"voxel_mesh_h.ply\", vox_mesh)\n", + "```\n", + "\n", + "From this point, writing of the IBM input file is the same as in the earlier example." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting &NAMIBM options\n", + "Now the input file containing the obstacle heights has been prepared, you are almost ready to use the IBM. The final step is to switch on the IBM and supply general settings in the ```&NAMIBM``` section of the ```namoptions.``` file. The available settings are show in table below.\n", + "\n", + "| Options | Default | Allowed Values | Description / Remark\n", + "| ------------ | ------- | ------- |------- |\n", + "| ```lapply_ibm``` | ```.false.```|```.true.``` or ```.false.``` | switch to enable immersed boundary method |\n", + "| ```lwallheat``` | ```.false.```|```.true.``` or ```.false.``` | switch to apply lateral heat flux from buildings |\n", + "| ```lpoislast``` | ```.true.```|```.true.``` or ```.false.``` | switch to apply Poisson solver **after** the IBM |\n", + "| ```thlwall``` | $293\\,\\mathrm{K}$ | $>0$ | potential temperature of vertical walls
(only if `lwallheat=.true.`) |\n", + "| ```thlroof``` | $293\\,\\mathrm{K}$ | $>0$ | potential temperature of roof faces|\n", + "| ```thlibm``` | $293\\,\\mathrm{K}$ | $>0$ | potential temperature inside of obstacles |\n", + "| ```qtibm``` | $0\\,\\mathrm{kg/kg}$ | $>0$ | moisture inside of obstacles |\n", + "| ```z0m_wall``` | $0.03\\,\\mathrm{m}$ | $>0$ and $<$ `zf(1)` | roughness length for momentum at walls/roofs |\n", + "| ```z0h_wall``` | $0.03\\,\\mathrm{m}$ | $>0$ and $<$ `zf(1)` | roughness length for heat at walls/roofs |\n", + "\n", + "At runtime, DALES will check for potentially conflicting options from different modules and throw ERROR messages when required." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## TODO: How to visualize IBM runs\n", + "..." ] } ], diff --git a/book/running/papers.ipynb b/book/running/papers.ipynb index ef0f3f0..843cd6a 100644 --- a/book/running/papers.ipynb +++ b/book/running/papers.ipynb @@ -7,10 +7,16 @@ "## List of publications using DALES\n", "The following contains a _non-exhaustive_ list of publications in which DALES was used.\n", "\n", + "### 2025\n", + "- Janssens, M., Jansson, F., Alinaghi, P., Glassmeier, F., and Siebesma, A. P. (2025). _Symmetry in mesoscale circulations explains weak impact of trade cumulus self‐organization on the radiation budget in large‐eddy simulations._ Geophysical Research Letters, **52**, e2024GL112288. https://doi.org/10.1029/2024GL112288\n", + "- Alinaghi, P., Siebesma, A. P., Jansson, F., Janssens, M., and Glassmeier, F. (2025). _External drivers and mesoscale self‐organization of shallow cold pools in the trade‐wind regime._ Journal of Advances in Modeling Earth Systems, **17**, e2024MS004540. https://doi.org/10.1029/2024MS004540\n", + "\n", "### 2024\n", "- Liqui Lung, F., Jakob, C., Siebesma, A. P. and Jansson, F. (2024). _Open boundary conditions for atmospheric large-eddy simulations and their implementation in DALES4.4_. Geoscientific Model Development, **17(9)**, 4053--4076, https://doi.org/10.5194/gmd-17-4053-2024\n", + "\n", "### 2023\n", "- Jansson, F., Janssens, M., Grönqvist, J. H., Siebesma, A. P., Glassmeier, F., Attema, J., et al. (2023). _Cloud Botany: Shallow cumulus clouds in an ensemble of idealized large-domain large-eddy simulations of the trades_. Journal of Advances in Modeling Earth Systems, **15**, e2023MS003796. https://doi.org/10.1029/2023MS003796 \n", + "\n", "### 2022\n", "\n", "### 2021\n", diff --git a/book/running/settingup.ipynb b/book/running/settingup.ipynb index f811774..c65e505 100644 --- a/book/running/settingup.ipynb +++ b/book/running/settingup.ipynb @@ -85,7 +85,7 @@ "...\n", "```\n", "\n", - "Checking out a specific branch (e.g., the `ruisdael` bracnh) is done via\n", + "Checking out a specific branch (e.g., the `ruisdael` branch) is done via\n", "```{code} shell\n", "git checkout -b remotes/origin/ruisdael\n", "```" diff --git a/book/running/tracers.001.nc b/book/running/tracers.001.nc index e9351869ee753aa6d0f827e8c637fc2e808e493a..a40c98977f3813ec11c895af550492ac2691de94 100644 GIT binary patch delta 18 acmexh{=s~M6$`7$?$nm2o9$UPiUR;nhzGp@ delta 18 Zcmexh{=s~M6$`7ebJ<