Code Development Guide
All of our curated code is in Julia. Only maintainers should push directly to #master
. Others should work off a branch or fork. When you have parts that are ready to contribute submit a pull request so the maintainers can review/discuss it.
Main Requirements
- Code must be in Julia package format so someone else can easily install it and it must support the latest stable version of Julia. To create a package I recommend using PkgTemplates as it’ll automate a lot of the mundane details. Here is a typical template:
t = Template(user="byuflowlab", dir=".", plugins=[GitHubPages()])
Setting the user creates the correct url’s, I usually prefer a local directory for development, and the later is helpful if you are going to use Documenter.jl.
-
Good documentation is a must. Documenter.jl is the standard format in Julia, but other formats can also work well. Try to address the four separate purposes of documentation. That usually means a “Quick Start” tutorial aimed at beginners, some how-to examples to do more advanced things, a reference guide for the API, and a theory document.
- Create CI tests:
- Set up unit tests. The pkgtemplate will already have a script started for you. Generally, you should test against known solutions (e.g., analytic solutions). Often we compare floating point numbers so
==
is too restrictive. The isapprox function, with a specified tolerance is useful for this.
- Create a Github Action. Use the julia-runtest action. See the test script for FLOWMath.jl, which has a few modifications (only reruns tests when .jl files are pushed, sets the versions correctly).
- Add a badge to your README to show test status.
- Write generic code so as to support algorithmic differentiation. Almost everything we use ends up in an optimization at some point, and we will want to have numerically exact gradients. Check that ForwardDiff and ReverseDiff work with your code. To test, wrap your code in a function
f = wrapper(x)
that takes in a vector x
for the inputs and returns a vector of outputs. Then call J = ForwardDiff.jacobian(wrapper, x)
. If it works, check it against finite differencing. These checks should go in your unit tests.
Often the main fix needed is that your code is specialized for floats so the dual numbers can’t propgate:
- instead of
0.0
as a default parameter use zero(x)
- instead of
zeros(n)
or Vector{Float64}(undef, n)
use zeros(typeof(x), n)
or Vector{typeof(x)}(undef, n)
- use parametric types on your structs (discussed below).
- Send your code to a peer (and generally me as well) for review. At a minimum they will review it as a user (can they install and understand how to use it), and if appropriate may also review as a developer (is the code clear, are the methods appropriate).
Other Guidelines
- Become familiar with the Julia performance tips
- Don’t add type annotations for functions arguments unless needed for dispatch. You can note recommended types in documentation but adding types does not improve performance, and it limits flexibility of usage. Even if you have a specific struct type or abstract type, don’t restrict it as someone will surely want to use their own type that uses your interface (duck-typing).
- Do add type annotations for structs. This is necessary for performance. But you should do it parametrically. Usually you use need one annotation (TF: type float), but below is a more general case with different types, an abstractarray.
struct MyType{TF, TI, TA<:AbstractVector{TF}}
a::TF
b::TI
c::Vector{TF}
d::TA
end
- Usually avoid mutable structs and dictionaries. Besides the performance hit, these objects are stateful meaning you can easily have side effects that are hard to test for and are the source for lots of bugs. (there are exceptions when thought through carefully).
- If you do modify arguments to a function add ! to the end of the function name and make it very clear what is being modified.
- Use standard style. Here is a good guide. Most importantly, use common sense.
- Use @code_warntype to check for type stability.