diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccddf66fef..2817349718 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -332,6 +332,29 @@ jobs: with: name: Mypy report path: mypy-report + spell-check-tutorial-prose: + name: "Documentation: pixi run docs-spelling" + runs-on: ubuntu-latest + needs: [cache-pixi-lock] + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Restore cached pixi lockfile + uses: Parcels-code/pixi-lock/restore@38495788b79a5ff26009aecc15daa9a8310b8832 # v0.1.0 + with: + cache-key: ${{ needs.cache-pixi-lock.outputs.cache-key }} + - uses: prefix-dev/setup-pixi@5185adfbffb4bd703da3010310260805d89ebb11 # v0.9.6 + with: + pixi-version: ${{ needs.cache-pixi-lock.outputs.pixi-version }} + locked: false # TODO: Remove once v7 of the lock file is removed, or once we stop having external source dependencies https://github.com/Parcels-code/parcels/pull/2550#issuecomment-4088660238 + cache: true + cache-write: ${{ github.event_name == 'push' && github.ref_name == 'main' }} + - name: Check tutorial prose + run: pixi run docs-spelling build-and-upload-nightly-parcels: # for alpha testing needs: [cache-pixi-lock] permissions: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d180c2a8cd..831606f69f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,3 +59,9 @@ repos: "--option", "align_comments=false", ] + - repo: https://github.com/vale-cli/vale + rev: v3.17.1 + hooks: + - id: vale + name: Vale British-English tutorial prose + files: ^docs/user_guide/(examples|getting_started)/tutorial.*\.md$ diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000000..3b0671fe18 --- /dev/null +++ b/.vale.ini @@ -0,0 +1,5 @@ +StylesPath = .vale/styles +MinAlertLevel = error + +[*] +BasedOnStyles = Parcels diff --git a/.vale/styles/Parcels/BritishEnglish.yml b/.vale/styles/Parcels/BritishEnglish.yml new file mode 100644 index 0000000000..257c3a21fb --- /dev/null +++ b/.vale/styles/Parcels/BritishEnglish.yml @@ -0,0 +1,39 @@ +extends: substitution +message: "Use British English: '%s' -> '%s'." +level: error +scope: text +ignorecase: true +swap: + analyze: analyse + analyzed: analysed + analyzing: analysing + behavior: behaviour + behaviors: behaviours + behavioral: behavioural + center: centre + centers: centres + centered: centred + centering: centring + color: colour + colors: colours + colored: coloured + coloring: colouring + customize: customise + customized: customised + customizing: customising + customization: customisation + labeled: labelled + labeling: labelling + modeler: modeller + modelers: modellers + modeling: modelling + normalize: normalise + normalized: normalised + normalizing: normalising + normalization: normalisation + optimize: optimise + optimized: optimised + optimizing: optimising + optimization: optimisation + organization: organisation + organizations: organisations diff --git a/docs/development/docsguide.md b/docs/development/docsguide.md index 1be5f5aaf2..8a2fb8d21d 100644 --- a/docs/development/docsguide.md +++ b/docs/development/docsguide.md @@ -41,4 +41,6 @@ a table showing the latest notebook execution: by any developer or user, so the documentation teaches all of us how to do something with Parcels. Sometimes it can be more natural to take on the tone of a teacher, writing to a student/learner, in which case it is okay to use "you". Please refrain from using impersonal subjects such as "the user". +- **Use British English in tutorial prose.** The Vale check covers Markdown files and Markdown cells in tutorial + notebooks; preserve the spelling required by code, APIs, and quoted external text. - We recommend hard wrapping prose in markdown so that reading it becomes easier in any editor. diff --git a/docs/user_guide/examples/tutorial_Argofloats.ipynb b/docs/user_guide/examples/tutorial_Argofloats.ipynb index 0219ca326a..be3ac1c94a 100644 --- a/docs/user_guide/examples/tutorial_Argofloats.ipynb +++ b/docs/user_guide/examples/tutorial_Argofloats.ipynb @@ -166,7 +166,7 @@ "source": [ "Now we can plot the trajectory of the Argo float with some simple calls to {py:obj}`parcels.read_particlefile()` and `matplotlib`.\n", "\n", - "First plot the depth as a function of time, with the temperature as color (only on the upcast)." + "First plot the depth as a function of time, with the temperature as colour (only on the upcast)." ] }, { @@ -194,7 +194,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can also make a 3D plot of the trajectory colored by temperature." + "We can also make a 3D plot of the trajectory coloured by temperature." ] }, { diff --git a/docs/user_guide/examples/tutorial_delaystart.ipynb b/docs/user_guide/examples/tutorial_delaystart.ipynb index 5568bc3613..130751dc5f 100644 --- a/docs/user_guide/examples/tutorial_delaystart.ipynb +++ b/docs/user_guide/examples/tutorial_delaystart.ipynb @@ -79,7 +79,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Defining the initial times of particles is done when the {py:obj}`parcels.ParticleSet` is defined. Although `t` and `z` are optional arguments (with FieldSet t=0 and z=0 as defaults), it is good practice to define them explicitly to ensure expected behavior. The simplest way to delay the start of a particle is to use the `t` argument for each particle." + "Defining the initial times of particles is done when the {py:obj}`parcels.ParticleSet` is defined. Although `t` and `z` are optional arguments (with FieldSet t=0 and z=0 as defaults), it is good practice to define them explicitly to ensure expected behaviour. The simplest way to delay the start of a particle is to use the `t` argument for each particle." ] }, { @@ -363,7 +363,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now, for some applications, this behavior may be undesirable; for example when particles need to be analyzed at a same age (instead of at a same time). In that case, we recommend either changing `outputdt` so that it is a common divisor of all start times; or by [writing directly in the kernel](./tutorial_write_in_kernel.ipynb); or by doing multiple Parcels runs with subsets of the original `ParticleSet` (e.g., in the example above, one run with the Particles that start at `t=[0, 2]` and one with the Particle at `t=[1]`). In that case, you will get two files:\n" + "Now, for some applications, this behaviour may be undesirable; for example when particles need to be analysed at a same age (instead of at a same time). In that case, we recommend either changing `outputdt` so that it is a common divisor of all start times; or by [writing directly in the kernel](./tutorial_write_in_kernel.ipynb); or by doing multiple Parcels runs with subsets of the original `ParticleSet` (e.g., in the example above, one run with the Particles that start at `t=[0, 2]` and one with the Particle at `t=[1]`). In that case, you will get two files:\n" ] }, { diff --git a/docs/user_guide/examples/tutorial_dt_integrators.ipynb b/docs/user_guide/examples/tutorial_dt_integrators.ipynb index df0f7ad0db..ce67d3ea0f 100644 --- a/docs/user_guide/examples/tutorial_dt_integrators.ipynb +++ b/docs/user_guide/examples/tutorial_dt_integrators.ipynb @@ -468,7 +468,7 @@ "id": "20", "metadata": {}, "source": [ - "We can see that in our simulation advecting particles for 7 days, the effect of `dt` on the precision of our simulation is approximately linear. The precision for a simulation with a timestep of 20 minutes is order of magnitude ~100 m. The effect on the time it takes to run a simulation is not linear in our case however; it increases sharply as we decrease our timestep. This may be optimized using more efficient chunking." + "We can see that in our simulation advecting particles for 7 days, the effect of `dt` on the precision of our simulation is approximately linear. The precision for a simulation with a timestep of 20 minutes is order of magnitude ~100 m. The effect on the time it takes to run a simulation is not linear in our case however; it increases sharply as we decrease our timestep. This may be optimised using more efficient chunking." ] }, { diff --git a/docs/user_guide/examples/tutorial_interpolation.ipynb b/docs/user_guide/examples/tutorial_interpolation.ipynb index d706a5109e..a5ac9088a9 100644 --- a/docs/user_guide/examples/tutorial_interpolation.ipynb +++ b/docs/user_guide/examples/tutorial_interpolation.ipynb @@ -148,7 +148,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "And then we can show each of the four interpolation methods, by plotting the interpolated values on the `Particles` locations (circles) on top of the `Field` values (background colors)\n" + "And then we can show each of the four interpolation methods, by plotting the interpolated values on the `Particles` locations (circles) on top of the `Field` values (background colours)" ] }, { @@ -255,10 +255,10 @@ "source": [ "## Interpolators on unstructured grids\n", "Parcels v4 supports the use of general circulation model output that is defined on unstructured grids. We include basic interpolators to help you get started, including\n", - "- `UxConstantFaceConstantZC` - this interpolator implements piecewise constant interpolation in both the lateral and vertical directions. It is appropriate for data that is registered to the face centers of the unstructured grid and centered on vertical layers.\n", - "- `UxLinearNodeConstantZC` - this interpolator implements barycentric interpolation in the lateral direction and piecewise constant in the vertical direction. It is appropriate for data that is registered to the corner vertices of the unstructured grid faces and centered on vertical layers.\n", + "- `UxConstantFaceConstantZC` - this interpolator implements piecewise constant interpolation in both the lateral and vertical directions. It is appropriate for data that is registered to the face centres of the unstructured grid and centred on vertical layers.\n", + "- `UxLinearNodeConstantZC` - this interpolator implements barycentric interpolation in the lateral direction and piecewise constant in the vertical direction. It is appropriate for data that is registered to the corner vertices of the unstructured grid faces and centred on vertical layers.\n", "- `UxLinearNodeLinearZF` - this interpolator implements barycentric interpolation in the lateral direction and piecewise linear interpolation in the vertical direction. It is appropriate for data that is registered to the corner vertices of the unstructured grid faces and on vertical layer interfaces.\n", - "- `UxConstantFaceLinearZF` - this interpolator implements piecewise constant interpolation in the lateral direction and piecewise linear interpolation in the vertical direction. It is appropriate for data that is registered to the face centers of the unstructured grid and centered on vertical layers\n", + "- `UxConstantFaceLinearZF` - this interpolator implements piecewise constant interpolation in the lateral direction and piecewise linear interpolation in the vertical direction. It is appropriate for data that is registered to the face centres of the unstructured grid and centred on vertical layers\n", "\n", "To get started, we use a very simple generated `UxArray.UxDataset` that is included with Parcels." ] @@ -466,7 +466,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In both plots the black lines show the edges that define the boundaries between the faces in the unstructured grid. For the node registered field, the background coloring is done using smooth shading based on the value of `T_node` at the corner nodes. For the face registered fields, each face is colored according to the value of `T_face` at the face center. The color of the particles is the value of the function that the particles take on via the corresponding interpolation method." + "In both plots the black lines show the edges that define the boundaries between the faces in the unstructured grid. For the node registered field, the background colouring is done using smooth shading based on the value of `T_node` at the corner nodes. For the face registered fields, each face is coloured according to the value of `T_face` at the face centre. The colour of the particles is the value of the function that the particles take on via the corresponding interpolation method." ] }, { diff --git a/docs/user_guide/examples/tutorial_sampling.ipynb b/docs/user_guide/examples/tutorial_sampling.ipynb index 430f29ebd0..8905d8569a 100644 --- a/docs/user_guide/examples/tutorial_sampling.ipynb +++ b/docs/user_guide/examples/tutorial_sampling.ipynb @@ -74,7 +74,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - " Ten particles are initialized at the surface in the center of our domain, at the initial time step." + " Ten particles are initialized at the surface in the centre of our domain, at the initial time step." ] }, { diff --git a/docs/user_guide/examples/tutorial_stuck_particles.ipynb b/docs/user_guide/examples/tutorial_stuck_particles.ipynb index 7875774a68..3b379a2fcc 100644 --- a/docs/user_guide/examples/tutorial_stuck_particles.ipynb +++ b/docs/user_guide/examples/tutorial_stuck_particles.ipynb @@ -88,11 +88,11 @@ "(a-grids)=\n", "## 1. A grids\n", "\n", - "Arakawa A grids are unstaggered grids where the velocities $u$, $v$ (and $w$), pressure and other tracers are defined at the same position (on so-called nodes). In numerical models, these nodes can be interpreted to be located **at the corner _or_ at the center of the grid cells**. This means that the cell boundaries, and therefore the solid-fluid boundaries can either be located at the nodes (**figure 1A**) or at 0.5 dx distance from the nodes (**figure 1B**) respectively.\n", + "Arakawa A grids are unstaggered grids where the velocities $u$, $v$ (and $w$), pressure and other tracers are defined at the same position (on so-called nodes). In numerical models, these nodes can be interpreted to be located **at the corner _or_ at the centre of the grid cells**. This means that the cell boundaries, and therefore the solid-fluid boundaries can either be located at the nodes (**figure 1A**) or at 0.5 dx distance from the nodes (**figure 1B**) respectively.\n", "\n", "Many ocean models natively run on a C grid, because boundary conditions are easier to implement there (see [C grid](#c-grids)). Sometimes, the C-grid output of these models is interpolated onto an A grid. This is the case for all(?) the data available from [Copernicus Marine Data Store](https://data.marine.copernicus.eu/products), which provide the data on a rectilinear A grid velocity field.\n", "\n", - "To visualize this, in **figure 1** we show the nodes and cells of a coastal region. The ocean cells and nodes are in red and the land cells and nodes are in white.\n" + "To visualize this, in **figure 1** we show the nodes and cells of a coastal region. The ocean cells and nodes are in red and the land cells and nodes are in white." ] }, { @@ -263,7 +263,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Figure 1** shows how _land_ grid points and boundaries can be interpreted, depending on whether you assume a grid node is at the center or corner of a cell. When they are at the corner of a cell, boundaries are interpreted as the edges between two nodes. This is especially visible in the upper center of the figures, where the white nodes with surrounding ocean can be assumed to only have a line of _land_ between them (**figure 1A**), or as entire _land_ cells (**figure 1B**).\n" + "**Figure 1** shows how _land_ grid points and boundaries can be interpreted, depending on whether you assume a grid node is at the centre or corner of a cell. When they are at the corner of a cell, boundaries are interpreted as the edges between two nodes. This is especially visible in the upper centre of the figures, where the white nodes with surrounding ocean can be assumed to only have a line of _land_ between them (**figure 1A**), or as entire _land_ cells (**figure 1B**)." ] }, { @@ -426,7 +426,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In **figure 2A** you can see how particles released in a 3x3 grid keep moving toward the boundary between two _land_ nodes. The ratio of the $u$ and $v$ components stays at a similar value due to both being linearly interpolated to zero at the boundary. Note that the interpretation of nodes at the center of grid cells (**figure 2B**) is clearly incompatible with Parcels interpolation.\n" + "In **figure 2A** you can see how particles released in a 3x3 grid keep moving toward the boundary between two _land_ nodes. The ratio of the $u$ and $v$ components stays at a similar value due to both being linearly interpolated to zero at the boundary. Note that the interpretation of nodes at the centre of grid cells (**figure 2B**) is clearly incompatible with Parcels interpolation." ] }, { @@ -459,9 +459,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "On staggered grids, different types of boundary conditions can be satisfied simultaneously. On a C-grid, the velocities are defined on the cell-edges normal to the velocity-component and pressure, temperature and tracers are defined at the cell centers. This way, a [Dirichlet boundary condition](https://en.wikipedia.org/wiki/Dirichlet_boundary_condition) can be used for the velocities, while a [Neumann boundary condition](https://en.wikipedia.org/wiki/Neumann_boundary_condition) can be satisfied for the gradient of pressure.\n", + "On staggered grids, different types of boundary conditions can be satisfied simultaneously. On a C-grid, the velocities are defined on the cell-edges normal to the velocity-component and pressure, temperature and tracers are defined at the cell centres. This way, a [Dirichlet boundary condition](https://en.wikipedia.org/wiki/Dirichlet_boundary_condition) can be used for the velocities, while a [Neumann boundary condition](https://en.wikipedia.org/wiki/Neumann_boundary_condition) can be satisfied for the gradient of pressure.\n", "\n", - "Here we investigate how Parcels interprets the the boundaries on a C grid. For background information, see [Delanmeter & Van Sebille (2019)](https://gmd.copernicus.org/articles/12/3571/2019/). First we show how the velocities are staggered and how the velocity input necessary to create a `FieldSet` results in the definition of boundaries in Parcels. This example uses a NEMO dataset but many of the relevant C grid assumptions are similar in other models, such as MITgcm.\n" + "Here we investigate how Parcels interprets the the boundaries on a C grid. For background information, see [Delanmeter & Van Sebille (2019)](https://gmd.copernicus.org/articles/12/3571/2019/). First we show how the velocities are staggered and how the velocity input necessary to create a `FieldSet` results in the definition of boundaries in Parcels. This example uses a NEMO dataset but many of the relevant C grid assumptions are similar in other models, such as MITgcm." ] }, { diff --git a/docs/user_guide/getting_started/tutorial_output.ipynb b/docs/user_guide/getting_started/tutorial_output.ipynb index 7c4775f05b..4dfffb0825 100644 --- a/docs/user_guide/getting_started/tutorial_output.ipynb +++ b/docs/user_guide/getting_started/tutorial_output.ipynb @@ -45,7 +45,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "First we need to create some Parcels output to analyze. We simulate a set of particles using the setup described in the [Delay start tutorial](../examples/tutorial_delaystart.ipynb). We will also add some user defined metadata to the output file." + "First we need to create some Parcels output to analyse. We simulate a set of particles using the setup described in the [Delay start tutorial](../examples/tutorial_delaystart.ipynb). We will also add some user defined metadata to the output file." ] }, { diff --git a/docs/user_guide/getting_started/tutorial_quickstart.md b/docs/user_guide/getting_started/tutorial_quickstart.md index d5922673af..6ba51839a5 100644 --- a/docs/user_guide/getting_started/tutorial_quickstart.md +++ b/docs/user_guide/getting_started/tutorial_quickstart.md @@ -157,7 +157,7 @@ pset.execute( ## Read output -To start analyzing the trajectories computed by Parcels, we can open the {py:obj}`parcels.ParticleFile` using the {py:func}`parcels.read_particlefile()` utility, which itself uses `polars`: +To start analysing the trajectories computed by Parcels, we can open the {py:obj}`parcels.ParticleFile` using the {py:func}`parcels.read_particlefile()` utility, which itself uses `polars`: ```{code-cell} df = parcels.read_particlefile("output-quickstart.parquet") @@ -176,7 +176,7 @@ import matplotlib.dates as mdates # convert time to matplotlib date format for plotting time_values = mdates.date2num(df["t"].to_list()) -# plot positions and color particles by time +# plot positions and colour particles by time scatter = plt.scatter(df['x'], df['y'], c=time_values) plt.scatter(df['x'][:npart], df['y'][:npart], facecolors="none", edgecolors='r') # starting positions plt.scatter(lon, lat, facecolors="none", edgecolors='r') # starting positions diff --git a/pixi.toml b/pixi.toml index 4b699435b1..cb4211ae17 100644 --- a/pixi.toml +++ b/pixi.toml @@ -130,6 +130,7 @@ sphinx-autobuild = "*" sphinxcontrib-mermaid = "*" sphinx-design = "*" sphinx-autoapi = "*" +vale = "*" [feature.docs.tasks] docs-only-clean = { cmd = "make clean", cwd = "docs", description = "Cleans the documentation folder of build artifacts." } @@ -142,6 +143,7 @@ docs-watch = { cmd = "make livehtml", cwd = "docs", description = "Build and aut "docs-only-clean", ] } docs-linkcheck = { cmd = "make linkcheck", cwd = "docs", description = "Verify all links in documentation don't 404." } +docs-spelling = { cmd = "python tools/lint_british_english.py docs/user_guide/examples/tutorial*.ipynb docs/user_guide/examples/tutorial*.md docs/user_guide/getting_started/tutorial*.ipynb docs/user_guide/getting_started/tutorial*.md", description = "Check tutorial prose uses British English." } [feature.pre-commit.dependencies] pre_commit = "*" diff --git a/tools/lint_british_english.py b/tools/lint_british_english.py new file mode 100644 index 0000000000..ab13071673 --- /dev/null +++ b/tools/lint_british_english.py @@ -0,0 +1,38 @@ +"""Lint tutorial Markdown and notebook prose with Vale.""" + +import argparse +import json +import subprocess +import tempfile +from pathlib import Path + + +def extract_markdown(notebook_path: Path, output_path: Path) -> None: + notebook = json.loads(notebook_path.read_text()) + markdown_cells = ("".join(cell["source"]) for cell in notebook["cells"] if cell["cell_type"] == "markdown") + output_path.write_text("\n\n".join(markdown_cells)) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("paths", nargs="+", type=Path) + arguments = parser.parse_args() + + with tempfile.TemporaryDirectory() as temporary_directory: + temporary_path = Path(temporary_directory) + vale_paths = [] + for path in arguments.paths: + if path.suffix == ".ipynb": + extracted_path = temporary_path / path.with_suffix(".md").name + extract_markdown(path, extracted_path) + vale_paths.append(extracted_path) + else: + vale_paths.append(path) + + result = subprocess.run(["vale", *map(str, vale_paths)], check=False) + + return result.returncode + + +if __name__ == "__main__": + raise SystemExit(main())