Coding style#

OOWL code should be simple, explicit, predictable, and scientifically interpretable. Prefer the smallest implementation that clearly expresses the underlying idea.

Ruff controls formatting, imports, and other mechanical style. This guide covers decisions that tools cannot make. Must marks a project requirement, and should marks the normal choice when judgment is involved. The domain model is described separately in the design philosophy.

Understand the existing code#

Before editing, inspect the relevant implementation, public API, tests, and user documentation. Search for existing concepts and helpers before introducing a new one. Follow nearby conventions when several approaches are equally clear.

Put behavior with the concept that owns it, so boundary logic belongs under oowl.boundaries, placement under oowl.layouts, and so on. Reuse OOWL’s domain objects instead of creating a parallel representation. Research scripts, fitting workflows, and one-off data processing do not belong in the runtime package.

Consistency is useful, but it does not justify copying a poor pattern. If the surrounding code has a broader problem, keep the requested change focused unless fixing that problem is necessary for a clean implementation.

Keep implementations simple#

Use a function for a calculation or transformation that has no meaningful state. Use a class for a domain concept with its own data and behavior. Use a dataclass for a genuine data record, not merely to group unrelated functions.

Do not introduce an interface, factory, registry, plugin system, helper, or configuration option without a current need. Extract a helper when it removes duplication, clarifies the main calculation, or isolates logic worth testing. An explicit five-line calculation is often clearer than a generic abstraction used once.

Generalize when doing so makes the current implementation simpler. Do not design around hypothetical future requirements. Prefer existing dependencies and the Python standard library. A new runtime dependency should replace substantial or fragile local code, not provide a small convenience.

Design clear APIs#

Names should describe scientific or geometric meaning. Use OOWL’s vocabulary consistently and avoid unexplained abbreviations in public APIs. Short mathematical names are acceptable inside a small calculation when their meaning is clear from the equation and context.

Public functions and methods must be type annotated. Type private helpers when the annotation clarifies a non-obvious input or result, but avoid typing machinery that obscures straightforward numerical code.

Use ordinary Python values in public APIs when that keeps calls simple. Make secondary options keyword-only when positional calls would be unclear or fragile. Avoid boolean arguments whose meaning is hidden at the call site. Do not expose a parameter unless users can understand what changing it does.

Return an existing OOWL object when the result has OOWL semantics. Internal calculations should normally use NumPy arrays rather than wrapping every intermediate value in another class. Convert array-like input near the boundary of a calculation and then use one representation consistently.

Public objects must be deliberately exported from the appropriate package. Private implementation details begin with an underscore. Do not make a helper public only so a test can call it, and avoid mutable default arguments.

Write clear numerical and scientific code#

OOWL uses SI units, except that public angles are expressed in degrees. Convert angles close to the trigonometric calculation, and document a different convention when required by an external format.

Important array shapes should be explicit. Turbine positions, for example, have shape (N, 2). Empty results should preserve the same dimensional meaning as populated results. Convert numerical input with an explicit floating-point dtype when the calculation requires it, and reject unexpected NaN or infinity.

Prefer NumPy vectorization when it is clearer or materially faster. A short Python loop is better than complicated broadcasting that hides the method. Use SciPy and Shapely for behavior they already implement well rather than recreating approximate versions locally.

Numerical tolerances must fit the calculation. Geometry, probabilities, coordinate transforms, and fitted models operate at different scales, so one universal tolerance is inappropriate. Use pytest.approx, np.allclose, or np.testing.assert_allclose with explicit tolerances when precision matters. Never loosen a tolerance merely to make a failing test pass.

The source should reveal the scientific idea. Use names that correspond to the quantities in the method and keep equations near the explanation or source that defines them. Explain constants taken from literature, fitting, or numerical analysis. Cite a paper, standard, dataset, or authoritative implementation where a future contributor will need it.

Comments should explain why an equation, special case, convention, assumption, or limitation exists. Do not translate an obvious line of code into English. Public model parameters should have understandable effects. Internal optimizer settings and fitted constants should remain internal unless users have a reason to control them.

Validate behavior at public boundaries#

Validate public input before NumPy or Shapely turns it into an obscure error. Internal helpers may rely on validation already performed by their public caller.

Use TypeError for the wrong kind of object and ValueError for an invalid value, range, or shape. A numeric parameter given a string, None, a boolean or a container raises TypeError, and a real number that is non-finite, out of range or not whole where a count is due raises ValueError. The helpers in oowl/_validation.py apply this rule. Error messages should identify the parameter and its requirement. Do not silently discard invalid values, repair geometry, reinterpret coordinates, replace NaNs, or substitute a fallback. If such behavior is useful, make it explicit.

Avoid broad exception handling unless the code can recover in a defined way or add useful context before re-raising.

Randomness, metadata, and export#

Stochastic code must use numpy.random.Generator through OOWL’s random helpers, never global NumPy state or Python’s module-level random functions. Resolve randomness once, record the resolved seed, and use deterministic child seeds for independent operations. An explicit seed must replay the result on the supported implementation.

Tests should normally verify that the same seed reproduces the same result and, when meaningful, that a different seed changes it. Do not test an unseeded call against one particular sample.

Metadata must use JSON-compatible values and contain only information needed to understand or reproduce a result. Convert NumPy arrays and scalars before storing them. Do not record every intermediate variable. Exporters must map data explicitly into windIO or another target schema rather than copying metadata wholesale.

Packaged fitted models are reviewed scientific outputs. Their model-specific documentation should describe the source data, preprocessing, assumptions, fitting, and validation. Runtime artifacts must use a documented, versioned, pickle-free format. Raw data, builders, studies, and fitting-only dependencies remain outside the installed package.

Performance and dependencies#

Measure before optimizing. Prefer clear code until measurement identifies a meaningful bottleneck.

In code shown to matter for runtime, avoid unnecessary repeated geometry operations, allocations, conversions, and Python-level work. When an optimized implementation is difficult to inspect, keep a simple reference calculation in a test where practical. Add caching only after considering when cached values become invalid.

Test and document observable behavior#

Tests should describe the public contract, not reproduce the implementation. Cover the normal case, important invalid inputs, and edge cases introduced by the change. Useful numerical invariants include shape, finiteness, normalization, containment, exact turbine count, and reproducibility. A bug fix needs a regression test that fails for the original bug.

Use exact expected values when they are meaningful, but do not rely only on snapshots when the real contract is an invariant. Never skip a failing test, suppress a warning, weaken an assertion, or disable a check unless the old expectation has been shown to be wrong.

Public modules, classes, functions, and methods need concise docstrings. Begin with what the object represents or does. Document non-obvious units, shapes, defaults, randomness, return behavior, and important errors. Tutorials belong in the user documentation.

Documentation examples must use public APIs and fixed seeds. Each code cell should teach one main outcome. Canonical notebooks are committed without stored outputs or execution counts and executed during the documentation build. Documentation prose uses neither semicolons, em dashes, nor colons. Docstrings follow the same rule for their prose, keeping colons only where Sphinx needs them, as in parameter and role syntax. Prose, docstrings, comments and code names use US spelling (center, color, neighbor, optimize, behavior).

Keep changes focused#

Make the smallest coherent change that solves the problem. Do not use a feature as an opportunity to rename unrelated APIs, reorganize neighboring modules, rewrite unrelated tests, regenerate unrelated notebooks, or update dependencies. A local refactor is appropriate when the implementation needs it or when it makes the changed behavior substantially clearer or easier to test.

Before version 1.0, prefer a clean, documented API change over an automatic compatibility alias. Update callers, tests, documentation, and the changelog together.

Before submitting, check the following.

  • the implementation follows OOWL’s domain model and is no more complicated than necessary

  • public inputs, units, errors, randomness, and results are clear

  • scientific assumptions and non-obvious constants are explained

  • tests cover the changed behavior and relevant invariants

  • user-visible behavior is documented

  • no unrelated abstractions, dependencies, or files were introduced

The setup, validation commands, and merge-request workflow are listed in Contributing.