This document defines the coding standards and policies for the SUEWS (Surface Urban Energy and Water Balance Scheme) project. These guidelines apply to both human developers and AI assistants (including Claude Code) when contributing to the project.
- General Principles
- Python Guidelines
- Fortran Guidelines
- Documentation Standards
- Testing Requirements
- Version Control Practices
- Use British English for all documentation, comments, and user-facing text
- Variable names may use common scientific abbreviations (e.g.,
tempfor temperature)
- Keep related functionality together in modules
- Separate concerns between configuration, physics, and utilities
- Use clear, descriptive file names that indicate purpose
- Write self-documenting code with clear variable names
- Add comments only when the code's purpose is not immediately obvious
- Prefer clarity over cleverness
- Follow the principle of least surprise
Order imports as follows:
# 1. Standard library imports
import os
import sys
from pathlib import Path
from typing import Dict, List, Optional
# 2. Third-party imports
import numpy as np
import pandas as pd
# 3. Local imports (use relative imports)
from .supy_driver import suews_driver as sd
from ._load import df_var_info| Type | Convention | Example |
|---|---|---|
| Functions | snake_case | run_supy, check_forcing |
| Variables | snake_case | df_forcing, dict_state |
| Classes | PascalCase | SUEWSConfig, BaseModel |
| Constants | UPPER_CASE | DEFAULT_TIMESTEP, NSURF |
| Private items | Leading underscore | _internal_function |
| Module names | lowercase with underscores | data_model.py |
Special Prefixes:
- DataFrames:
df_(e.g.,df_forcing) - Dictionaries:
dict_(e.g.,dict_state) - Lists:
list_(e.g.,list_grid) - Series:
ser_(e.g.,ser_var) - Paths:
path_(e.g.,path_runcontrol)
Always use type hints for function signatures:
from typing import Tuple, Optional, Union
def run_supy_ser(
df_forcing: pd.DataFrame,
df_state_init: pd.DataFrame,
save_state: bool = False,
chunk_day: int = 3660,
) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""Perform supy simulation."""
...Use NumPy-style docstrings for all public functions and classes:
def function_name(param1: type, param2: type) -> return_type:
"""Brief description of function.
Longer description if needed.
Parameters
----------
param1 : type
Description of param1.
param2 : type
Description of param2.
Returns
-------
return_type
Description of return value.
Examples
--------
>>> example_usage()
expected_output
"""Use structured error handling with informative messages:
try:
result = risky_operation()
except SpecificError as e:
logger_supy.error(f"Operation failed: {e}")
raise
except Exception:
logger_supy.exception("Unexpected error in operation")
raise- Path Handling: Use
pathlib.Pathinstead ofos.path - Logging: Use
logger_supyinstead ofprint() - Deep Copying: Use
copy.deepcopy()for mutable state - Configuration: Keep configuration parsing separate from implementation
- Validation: Validate inputs early and provide clear error messages
Complete naming conventions: See FORTRAN_NAMING_CONVENTIONS.md for comprehensive guidelines.
MODULE suews_<category>_<name>
USE other_module
IMPLICIT NONE
! Parameters
REAL(KIND(1D0)), PARAMETER :: CONSTANT_NAME = value
! Type definitions
TYPE, PUBLIC :: TypeName
REAL(KIND(1D0)) :: component = 0.0D0 ! Always initialise
END TYPE TypeName
CONTAINS
! Subroutines and functions
END MODULE suews_<category>_<name>Effective from October 2025 - All new code must follow these standards:
| Type | Convention | Example |
|---|---|---|
| Files | suews_<category>_<name>.f95 |
suews_phys_snow.f95 |
| Modules | Match file name: suews_<category>_<name> |
suews_phys_snow |
| Subroutines | snake_case | update_snow_state |
| Functions | snake_case | calc_density |
| Parameters | UPPERCASE_UNDERSCORES | STEFAN_BOLTZMANN |
| Variables | lowercase_underscores | air_temperature |
| Types | snake_case with _t suffix |
snow_state_t |
| Type components | lowercase_underscores | surface_temperature |
Key principles:
- One case style: Everything uses snake_case (except UPPERCASE for constants)
- File naming is already consistent (✅ established pattern)
- Module names should match file names for easy discovery
- One module per file preferred; use suffixes (
_const,_types,_ops) if multiple needed - Consistent with scientific Python conventions (NumPy, SciPy, pandas)
- Always document physical units in comments:
! [K],! [W m-2]
Migration: Legacy code uses multiple patterns. See FORTRAN_NAMING_CONVENTIONS.md for migration strategy and backward compatibility approach.
! Always use explicit precision
REAL(KIND(1D0)) :: temperature = 0.0D0 ! [K] Always include units
INTEGER :: surface_type = 0 ! Always initialise
CHARACTER(LEN=50) :: site_name = '' ! Specify length
! Arrays with clear dimensions
REAL(KIND(1D0)), DIMENSION(nsurf) :: surface_fraction! Module header with change log
! Original: sg feb 2012
! Modified: lj jun 2012 - added snow calculations
! Modified: hw oct 2014 - restructured for clarity
!===============================================
! Subroutine: calculate_something
! Purpose: Brief description
! Input: var1 - description [units]
! var2 - description [units]
! Output: result - description [units]
!===============================================
SUBROUTINE calculate_something(var1, var2, result)Never use exact equality for floating-point numbers:
! Wrong
IF (H == 0.0) THEN
! Correct
REAL(KIND(1D0)), PARAMETER :: eps_fp = 1.0E-12
IF (ABS(H) <= eps_fp) THEN- Always use
IMPLICIT NONE - Initialise all variables in type definitions
- Document physical units in comments
- Use ASSOCIATE blocks for clarity
- Define array dimensions as parameters
- Group related functionality in modules
- Comments should explain why, not what
- Update comments when code changes
- Remove commented-out code before committing
- Use British English in all comments
Always document units in square brackets:
REAL(KIND(1D0)) :: rainfall = 0.0D0 ! [mm h-1]
REAL(KIND(1D0)) :: temperature = 0.0D0 ! [K]Each major component should have a README explaining:
- Purpose and functionality
- Dependencies
- Usage examples
- Key algorithms or references
Tests are organised by functionality:
test/
├── core/ # Essential functionality tests
├── data_model/ # Configuration and validation tests
├── physics/ # Scientific validation tests
├── io_tests/ # Input/output tests
└── fixtures/ # Test data and configurations
- Files:
test_*.py(e.g.,test_supy.py,test_benchmark.py) - Classes:
TestFeatureNameusingunittest.TestCase - Methods: Descriptive names explaining what's tested
def test_energy_balance_closure(self): """Test that energy balance is conserved.""" def test_yaml_loading_no_spurious_warnings(self): """Test that loading YAML doesn't produce warnings."""
class TestSuPy(TestCase):
def setUp(self):
"""Set up test fixtures."""
warnings.simplefilter("ignore", category=ImportWarning)
self.df_input = sp.load_SampleData()
def test_normal_operation(self):
"""Test feature under normal conditions."""
result = sp.run_supy(self.df_input)
self.assertIsNotNone(result)
def tearDown(self):
"""Clean up after tests."""
# Cleanup if neededdef test_sample_output_validation():
"""Validate sample output against expected results."""
df_input = sp.load_SampleData()
result = sp.run_supy(df_input)
assert result is not None- unittest:
self.assertTrue(),self.assertEqual(),self.assertAlmostEqual() - pytest: Simple
assertstatements - Pandas:
pd.testing.assert_frame_equal()for DataFrames - NumPy:
np.testing.assert_array_almost_equal()for arrays - Physical bounds: Always check outputs are within realistic ranges
- Sample data: Use
sp.load_SampleData()for consistent test data - Fixtures: Store test data in
test/fixtures/directory - Temporary files: Use
tempfile.TemporaryDirectory()for file operations - Benchmark data: Store expected results in
.pklfiles for regression testing
When testing scientific functionality:
def test_energy_balance_closure(self):
"""Test energy balance: Rn = QH + QE + QS + QF."""
result = sp.run_supy(df_input)
# Extract fluxes
rn = result.SUEWS['Rn']
qh = result.SUEWS['QH']
qe = result.SUEWS['QE']
qs = result.SUEWS['QS']
qf = result.SUEWS['QF']
# Check energy balance within tolerance
balance = rn - (qh + qe + qs - qf)
tolerance = 10.0 # W/m²
self.assertTrue(
abs(balance.mean()) < tolerance,
f"Energy balance error: {balance.mean():.2f} W/m²"
)Ensure tests don't pollute each other:
- Load fresh data in each test
- Use
setUp()andtearDown()methods - Be aware of Fortran state persistence issues
- Run tests individually and in suite to detect pollution
- Descriptive names: Test names should explain what's being tested
- Docstrings: Add docstrings explaining the test purpose
- One concept per test: Each test should verify one specific behaviour
- Fast tests: Keep individual tests fast (< 1 second where possible)
- Reproducibility: Tests should produce identical results on repeated runs
- Platform awareness: Consider platform-specific behaviour
- Warning handling: Suppress expected warnings, fail on unexpected ones
# Run all tests
make test
# Run specific test file
python -m pytest test/test_supy.py -v
# Run with coverage
python -m pytest test --cov=supy
# Check for state pollution
python -m pytest test/test_specific.py -v # Run alone
python -m pytest test -v # Run in suiteCritical tests run first via conftest.py:
- Sample output validation
- Benchmark tests
- Core functionality This ensures fundamental features are tested before complex scenarios.
Follow conventional commit format:
type(scope): brief description
Longer explanation if needed.
Fixes #123
Types: feat, fix, docs, style, refactor, test, chore
Use descriptive branch names:
feature/add-new-physics-optionfix/qe-qh-discrepancydocs/update-user-guide
- Update relevant documentation
- Add/update tests
- Run full test suite
- Update CHANGELOG if applicable:
- [feature]: New user-facing functionality
- [bugfix]: Bug fixes (create GitHub issue)
- [change]: User-facing behaviour changes
- [maintenance]: Internal/dev tooling (including Claude Code aspects)
- [doc]: Documentation updates
- Request review from appropriate team members
Configuration objects should be handled at high levels:
# Good: High-level extracts configuration
def high_level_method(self):
freq_s = self._config.output.freq.value if self._config else 3600
save_supy(df_output, df_state, freq_s=freq_s)
# Bad: Low-level accepts configuration
def save_supy(df_output, df_state, config): # Don't do this: including config in the function signature is bad practiceUse Pydantic for configuration with proper validation:
class PhysicsConfig(BaseModel):
scheme: Literal["W16", "K75"] = Field(
default="W16",
description="Physics scheme selection"
)
@field_validator("scheme")
def validate_scheme(cls, v):
if v not in ["W16", "K75"]:
raise ValueError(f"Unknown scheme: {v}")
return vSUEWS uses Ruff for Python formatting and linting.
Installation:
pip install ruffUsage:
# Format code
ruff format .
# Lint code
ruff check .
# Fix auto-fixable issues
ruff check --fix .Pre-commit Integration:
# Install pre-commit hooks
pip install pre-commit
pre-commit install
# Run manually
pre-commit run --all-filesSUEWS uses fprettify for Fortran formatting.
Installation:
pip install fprettifyUsage:
# Format a single file
fprettify src/suews/src/suews_phys_snow.f95
# Format all Fortran files
find src/suews/src -name "*.f95" -o -name "*.f90" | xargs fprettify --indent 3 --line-length 132
# Check without modifying
fprettify --diff src/suews/src/suews_phys_snow.f95Recommended fprettify settings:
fprettify \
--indent 3 \ # 3 spaces for indentation
--line-length 132 \ # Allow longer lines for scientific code
--whitespace 2 \ # Adjust whitespace around operators
--strict-indent \ # Strict indentation rules
--enable-decl \ # Format declarations
--case 1 1 1 1 \ # Keywords lowercase, intrinsics lowercase
src/suews/src/*.f95VS Code:
- Install "Ruff" extension for Python
- Install "Modern Fortran" extension with fprettify support
- Configure settings.json:
{
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[fortran]": {
"editor.formatOnSave": true,
"fprettify.arguments": [
"--indent", "3",
"--line-length", "132",
"--whitespace", "2"
]
}
}Automated Formatting Philosophy: Let machines handle formatting so developers can focus on functionality.
The master branch is automatically formatted after every merge:
- Triggers: On push to master containing Python or Fortran files
- Actions:
- Formats Python code with ruff
- Formats Fortran code with fprettify
- Only creates commit if changes are needed
- Uses
[skip ci]to avoid build loops
- Benefits:
- Zero friction for contributors
- Guaranteed consistency on master
- Clear formatting commits in history
While formatting is optional for contributors, these tools are available:
make format # Format all code locally
make lint # Check code style without modifying
# Or use pre-commit hooks (optional but recommended)
pip install pre-commit
pre-commit installNote: The master branch is the single source of truth for code formatting. All code merged to master will be automatically formatted to ensure consistency.
- All code must pass automated checks before merging
- Pre-commit hooks enforce formatting locally
- GitHub Actions verify formatting in CI
- Code reviews should verify adherence to these guidelines
- AI assistants should reference this document when generating code
- Guidelines should be updated through team consensus