Best Coding Practices
In this section, we provide some general guidelines for writing and structuring code in NDSL. While these practices are not necessarily required, we strongly encourage users to adhere to them as they are important for readability, maintainability, collaboration, and performance.
Docstrings
When writing code using NDSL, it is important that your code is well-documented so that others can easily understand the purpose of your code without having to dig into the implementation details. We strongly encourage the use of docstrings to document your code.
Docstrings are strings written immediately after the definition of an NDSL function, stencil, or
class. They are enclosed in triple quotes ("""
or '''
) and can span multiple lines. A good
docstring should include information about what the function, stencil, or class does. It should also
include a summary of what the code does and a list of parameters, their type, and a description.
See below for an example of how to write docstrings for a GT4Py function.
@gtscript.function
def sign(
a: Float,
b: Float,
):
"""
Function that returns the magnitude of one argument and the sign of another.
Inputs:
a [Float]: Argument of which the magnitude is needed [unitless]
b [Float]: Argument of which the sign is needed [unitless]
Returns:
result [Float]: The magnitude of a and sign of b [unitless]
"""
if b >= 0.0:
result = abs(a)
else:
result = -abs(a)
return result
Temporaries
To create and store temporary fields in NDSL, which are not explicitly defined within a stencil or
class, we strongly encourage the use of NDSL dataclasses
. A dataclass
is a Python class
that is used to store or hold data. While the use of dataclasses
are not required, we strongly
encourage our users to store temporaries in a dataclass
for cleanliness and readability purposes.
See below for an example of how to create a dataclass
to hold temporary fields.
from dataclasses import dataclass
from ndsl import Quantity, QuantityFactory
from ndsl.constants import X_DIM, Y_DIM, Z_DIM
@dataclass
class Temporaries:
ssthl0: Quantity
ssqt0: Quantity
ssu0: Quantity
ssv0: Quantity
@classmethod
def make(cls, quantity_factory: QuantityFactory):
# FloatFields
ssthl0 = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")
ssqt0 = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")
ssu0 = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")
ssv0 = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")
return cls(
ssthl0,
ssqt0,
ssu0,
ssv0,
)
Object-oriented Coding
Object-oriented coding is strongly encouraged when writing code using NDSL. GIVE A FEW REASONS WHY WE PREFER OBJECT ORIENTED CODING.
INTRODUCE CLASSES AND WHY WE USE THEM (someone else should probably do this).
General structure of NDSL repositories
Now that we have covered pretty much all of the topics needed to start learning and developing code in NDSL, it's time to talk about how a repository containing NDSL code should be structured.
In this example, we've created a mock-up repository which contains NDSL code to convert
temperature from Fahrenheit to Kelvin and then back to Fahrenheit. We've named our mock-up
repository tutorial
, which contains four Python scripts: driver.py
, stencils.py
,
constants.py
, and temporaries.py
.
Each script has a unique purpose. For example, driver.py
contains code to create and call the
class
that initializes the NDSL stencils.
from ndsl import StencilFactory
from ndsl.boilerplate import get_factories_single_tile
from ndsl.constants import X_DIM, Y_DIM, Z_DIM
from ndsl.dsl.typing import FloatField
from pyMoist.tutorial.stencils import convert_F_to_K, convert_K_to_F
from pyMoist.tutorial.temporaries import Temporaries
import random
class Temperature_Conversion:
def __init__(self, stencil_factory: StencilFactory):
"""
Class to convert temperatures from Fahrenheit to Kelvin and then back to Fahrenheit
Parameters:
stencil_factory (StencilFactory): Factory for creating stencil computations
"""
# Build stencils
self.convert_F_to_K = stencil_factory.from_dims_halo(
func=convert_F_to_K,
compute_dims=[X_DIM, Y_DIM, Z_DIM],
)
self.convert_K_to_F = stencil_factory.from_dims_halo(
func=convert_K_to_F,
compute_dims=[X_DIM, Y_DIM, Z_DIM],
)
self.temporaries = Temporaries.make(quantity_factory)
def __call__(
self,
temp_F: FloatField,
temp_K: FloatField,
):
self.convert_F_to_K(
temp_F,
temp_K,
)
self.convert_K_to_F(
temp_K,
self.temporaries.temp_C,
temp_F,
)
if __name__ == "__main__":
# Setup domain and generate factories
domain = (5, 5, 10)
nhalo = 0
stencil_factory, quantity_factory = get_factories_single_tile(
domain[0],
domain[1],
domain[2],
nhalo,
backend="debug",
)
# Initialize quantities
temp_F = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")
for i in range(temp_F.field.shape[0]):
for j in range(temp_F.field.shape[1]):
for k in range(temp_F.field.shape[2]):
temp_F.field[i, j, k] = round(random.uniform(70, 90), 2)
temp_K = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")
# Build stencil
code = Temperature_Conversion(stencil_factory)
# Check input data
print(temp_F.field[0, 0, :])
# Execute stencil
code(temp_F, temp_K)
# Check output data
print(temp_K.field[0, 0, :])
print(temp_F.field[0, 0, :])
stencils.py
contains both NDSL functions and stencils that do the temperature conversion.
from ndsl.dsl.gt4py import (
computation,
interval,
PARALLEL,
)
import gt4py.cartesian.gtscript as gtscript
from ndsl.dsl.typing import FloatField, Float
import pyMoist.tutorial.constants as constants
@gtscript.function
def convert_F_to_C(
t_F: Float,
):
"""
Function to convert Fahrenheit to Celsius.
Inputs:
t_F (Float): Temperature in Fahrenheit (degrees)
Returns:
t_C (Float): Temperature in Celsius (degrees)
"""
t_C = (t_F - 32) * (5 / 9)
return t_C
@gtscript.function
def convert_C_to_F(
t_C: Float,
):
"""
Function to convert Celsius to Fahrenheit.
Inputs:
t_C (Float): Temperature in Celsius (degrees)
Returns:
t_F (Float): Temperature in Fahrenheit (degrees)
"""
t_F = (t_C * (9 / 5)) + 32
return t_F
def convert_F_to_K(
temp_F: FloatField,
temp_K: FloatField,
):
"""
Stencil to convert temperature from Fahrenheit to Kelvin.
Temperature is first converted to Celsius, then Celsius to Kelvin.
Inputs:
temp_F (FloatField): Temperature in degrees Fahrenheit
Outputs:
temp_K (FloatField): Temperature in Kelvin (unitless)
"""
with computation(PARALLEL), interval(...):
temp_C = convert_F_to_C(temp_F)
temp_K = temp_C + constants.absolute_zero
def convert_K_to_F(
temp_K: FloatField,
temp_C: FloatField,
temp_F: FloatField,
):
"""
Stencil to convert temperature from Kelvin to Fahrenheit.
Temperature is first converted to Celsius, then Celsius to Fahrenheit.
Inputs:
temp_K (FloatField): Temperature in Kelvin (unitless)
Outputs:
temp_F (FloatField): Temperature in degrees Fahrenheit
"""
with computation(PARALLEL), interval(...):
temp_C = temp_K - constants.absolute_zero
temp_F = convert_C_to_F(temp_C)
temporaries.py
contains a dataclass
that holds any temporary fields used in the stencil
computations
from dataclasses import dataclass
from ndsl import Quantity, QuantityFactory
from ndsl.constants import X_DIM, Y_DIM, Z_DIM
@dataclass
class Temporaries:
temp_C: Quantity
@classmethod
def make(cls, quantity_factory: QuantityFactory):
# FloatFields
temp_C = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")
return cls(
temp_C,
)
constants.py
contains any constants needed in the functions and stencils
It is important to structure your NDSL repositories in a similar fashion not only for cleanliness and readability, but for scalability and debugging purposes as well.