SCHISM Backend Framework Tutorial#

This tutorial guides you through using the SCHISM backend framework to execute SCHISM simulations with Docker containers, automatic image building, and comprehensive testing.

Getting Started#

Prerequisites#

Before starting, ensure you have:

  • Docker installed and running

  • ROMPY installed with backend framework support

  • SCHISM boundary conditions examples (included with ROMPY)

Quick Start#

The easiest way to get started is with the boundary conditions examples:

# Navigate to the SCHISM examples directory
cd rompy/notebooks/schism

# Run a single tidal example (dry run)
python run_boundary_conditions_examples.py --single basic_tidal --dry-run

# Run all tidal examples
python run_boundary_conditions_examples.py --tidal

# Run a complete test suite
python test_backend_examples.py

Basic Tutorial#

Step 1: Understanding the Framework#

The SCHISM backend framework consists of three main components:

  1. ModelRun - ROMPY’s model execution framework

  2. DockerConfig - Backend configuration for Docker execution

  3. Boundary Conditions - SCHISM-specific configuration system

Here’s how they work together:

from rompy.model import ModelRun
from rompy.backends import DockerConfig
from rompy.core.time import TimeRange
from datetime import datetime
from pathlib import Path

# 1. Create a model run
model_run = ModelRun(
    run_id="my_schism_example",
    period=TimeRange(
        start=datetime(2023, 1, 1),
        end=datetime(2023, 1, 2),
        interval="1H"
    ),
    output_dir="schism_output",
    delete_existing=True
)

# 2. Configure Docker backend
docker_config = DockerConfig(
    dockerfile=Path("Dockerfile"),
    build_context=Path("docker/schism"),
    timeout=3600,
    cpu=8,
    memory="4g"
)

# 3. Generate configuration files
model_run.generate()

# 4. Execute simulation
success = model_run.run(backend=docker_config)

Step 2: Creating Your First SCHISM Configuration#

Create a YAML configuration file for a simple tidal simulation:

# my_tidal_example.yaml
run_id: my_first_schism_run
period:
  start: 2023-01-01T00:00:00
  end: 2023-01-01T12:00:00
  interval: 1H
output_dir: my_schism_output
delete_existing: true

config:
  model_type: schism
  grid:
    grid_type: schism
    hgrid:
      model_type: data_blob
      source: tests/schism/test_data/hgrid.gr3
    drag: 2.5e-3

  data:
    data_type: schism
    boundary_conditions:
      data_type: boundary_conditions
      setup_type: tidal
      tidal_data:
        tidal_database: tests/schism/test_data/tides
        tidal_model: 'OCEANUM-atlas'
        constituents: [M2, S2, N2]
      boundaries:
        0:
          elev_type: 3  # HARMONIC
          vel_type: 3   # HARMONIC
          temp_type: 0  # NONE
          salt_type: 0  # NONE

  nml:
    param:
      core:
        dt: 150.0
        ibc: 1  # Barotropic
        ibtp: 0  # No tracer transport
        nspool: 24
        ihfskip: 1152
      schout:
        iof_hydro__1: 1  # elevation
        iof_hydro__26: 1  # velocity vector

Step 3: Running Your Configuration#

Now create a Python script to execute your configuration:

# run_my_example.py
import yaml
from rompy.model import ModelRun
from rompy.backends import DockerConfig
from pathlib import Path

def main():
    # Load configuration
    with open("my_tidal_example.yaml", "r") as f:
        config = yaml.safe_load(f)

    # Create model run
    model_run = ModelRun(**config)

    # Configure Docker backend
    docker_config = DockerConfig(
        dockerfile=Path("Dockerfile"),
        build_context=Path("docker/schism"),
        timeout=3600,
        cpu=8,
        memory="4g",
        executable="bash -c 'cd /tmp/schism && mpirun --allow-run-as-root -n 8 schism_v5.13.0 4'",
        volumes=[f"{Path.cwd() / 'my_schism_output'}:/tmp/schism:rw"],
        env_vars={
            "OMPI_ALLOW_RUN_AS_ROOT": "1",
            "OMPI_ALLOW_RUN_AS_ROOT_CONFIRM": "1"
        },
        remove_container=True
    )

    try:
        # Generate configuration files
        print("Generating SCHISM configuration...")
        model_run.generate()

        # Execute simulation
        print("Running SCHISM simulation...")
        success = model_run.run(backend=docker_config)

        if success:
            print("✅ Simulation completed successfully!")

            # Check for output files
            output_dir = Path("my_schism_output")
            outputs = list(output_dir.glob("**/outputs/*.nc"))
            print(f"Generated {len(outputs)} output files")

        else:
            print("❌ Simulation failed")

    except Exception as e:
        print(f"❌ Error: {e}")

if __name__ == "__main__":
    main()

Step 4: Execute Your Example#

Run your example:

python run_my_example.py

Expected output:

Generating SCHISM configuration...
Running SCHISM simulation...
✅ Simulation completed successfully!
Generated 1 output files

Intermediate Tutorial#

Step 5: Adding Wave Coupling#

Extend your configuration to include wave coupling:

# Add to your existing configuration
config:
  data:
    wave:
      buffer: 0.0
      coords:
        t: time
        x: lon
        y: lat
        z: depth
      id: wavedata
      source:
        catalog_uri: tests/data/catalog.yaml
        dataset_id: ausspec
        model_type: intake

  nml:
    param:
      opt:
        ihot: 0
        nstep_wwm: 4
      schout:
        iof_wwm__1: 1   # significant wave height
        iof_wwm__9: 1   # peak wave period
        iof_wwm__18: 1  # peak wave direction
    wwminput:
      proc:
        deltc: 600

Step 6: Hybrid Boundary Conditions#

Configure hybrid boundaries that combine tidal and ocean data:

config:
  data:
    boundary_conditions:
      data_type: boundary_conditions
      setup_type: hybrid
      tidal_data:
        tidal_database: tests/schism/test_data/tides
        tidal_model: 'OCEANUM-atlas'
        constituents: [M2, S2, N2]
      boundaries:
        0:
          elev_type: 5  # HARMONICEXTERNAL
          vel_type: 5   # HARMONICEXTERNAL
          temp_type: 4  # EXTERNAL
          salt_type: 4  # EXTERNAL
          elev_source:
            data_type: boundary
            source:
              model_type: file
              uri: tests/schism/test_data/hycom.nc
            variables: [surf_el]
            coords:
              t: time
              x: xlon
              y: ylat
          vel_source:
            data_type: boundary
            source:
              model_type: file
              uri: tests/schism/test_data/hycom.nc
            variables: [water_u, water_v]
            coords:
              t: time
              x: xlon
              y: ylat
              z: depth

Step 7: Adding Hotstart Generation#

Generate initial conditions from your ocean data:

config:
  data:
    boundary_conditions:
      hotstart_config:
        enabled: true
        temp_var: temperature
        salt_var: salinity
        output_filename: hotstart.nc

Advanced Tutorial#

Step 8: Custom Docker Configuration#

Create advanced Docker configurations for specific needs:

# High-performance configuration
docker_config = DockerConfig(
    dockerfile=Path("Dockerfile.optimized"),
    build_context=Path("docker/schism"),
    build_args={
        "SCHISM_VERSION": "v5.13.0",
        "ENABLE_OPTIMIZATION": "ON"
    },
    cpu=16,
    memory="16g",
    timeout=7200,
    volumes=[
        f"{output_dir}:/tmp/schism:rw",
        "/tmp:/tmp_host:rw"
    ],
    env_vars={
        "OMPI_ALLOW_RUN_AS_ROOT": "1",
        "OMPI_ALLOW_RUN_AS_ROOT_CONFIRM": "1",
        "OMP_NUM_THREADS": "16"
    }
)

Step 9: Multiple Boundary Types#

Configure complex scenarios with multiple boundary types:

config:
  data:
    boundary_conditions:
      data_type: boundary_conditions
      setup_type: mixed
      tidal_data:
        tidal_database: tests/schism/test_data/tides
        tidal_model: 'OCEANUM-atlas'
        constituents: [M2, S2, N2]
      boundaries:
        0:  # Ocean boundary (tidal + external)
          elev_type: 5
          vel_type: 5
          temp_type: 4
          salt_type: 4
          # ... data sources
        1:  # River boundary (constant flow)
          elev_type: 0
          vel_type: 2
          temp_type: 2
          salt_type: 2
          const_flow: -100.0
          const_temp: 15.0
          const_salt: 0.1
        2:  # Nested boundary (relaxation)
          elev_type: 5
          vel_type: 7
          temp_type: 4
          salt_type: 4
          inflow_relax: 0.8
          outflow_relax: 0.2

Step 10: Automated Testing#

Create automated testing for your configurations:

# test_my_configurations.py
import pytest
import yaml
from pathlib import Path
from rompy.model import ModelRun
from rompy.backends import DockerConfig

class TestSchismConfigurations:

    def test_basic_tidal_config(self):
        """Test basic tidal configuration."""
        config_file = Path("my_tidal_example.yaml")
        assert config_file.exists()

        with open(config_file, "r") as f:
            config = yaml.safe_load(f)

        model_run = ModelRun(**config)
        assert model_run.run_id == "my_first_schism_run"

    def test_docker_config_creation(self):
        """Test Docker configuration creation."""
        docker_config = DockerConfig(
            dockerfile=Path("Dockerfile"),
            build_context=Path("docker/schism"),
            cpu=8,
            memory="4g"
        )

        assert docker_config.dockerfile == Path("Dockerfile")
        assert docker_config.cpu == 8
        assert docker_config.memory == "4g"

    def test_dry_run_execution(self):
        """Test configuration generation without execution."""
        with open("my_tidal_example.yaml", "r") as f:
            config = yaml.safe_load(f)

        model_run = ModelRun(**config)

        # Test configuration generation
        try:
            model_run.generate()
            assert True, "Configuration generation succeeded"
        except Exception as e:
            pytest.fail(f"Configuration generation failed: {e}")

if __name__ == "__main__":
    pytest.main([__file__, "-v"])

Production Usage#

Step 11: Batch Processing#

Run multiple configurations in batch:

# batch_runner.py
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import yaml
from rompy.model import ModelRun
from rompy.backends import DockerConfig

def run_configuration(config_file):
    """Run a single configuration file."""
    try:
        with open(config_file, "r") as f:
            config = yaml.safe_load(f)

        model_run = ModelRun(**config)

        docker_config = DockerConfig(
            dockerfile=Path("Dockerfile"),
            build_context=Path("docker/schism"),
            timeout=3600,
            cpu=8,
            memory="4g"
        )

        model_run.generate()
        success = model_run.run(backend=docker_config)

        return config_file.name, success

    except Exception as e:
        return config_file.name, False

def main():
    # Find all configuration files
    config_files = list(Path(".").glob("*_example.yaml"))

    # Run configurations in parallel
    with ThreadPoolExecutor(max_workers=3) as executor:
        results = list(executor.map(run_configuration, config_files))

    # Report results
    for config_name, success in results:
        status = "✅" if success else "❌"
        print(f"{status} {config_name}")

if __name__ == "__main__":
    main()

Step 12: Monitoring and Logging#

Add comprehensive monitoring to your runs:

# monitored_runner.py
import logging
import time
from pathlib import Path
from rompy.model import ModelRun
from rompy.backends import DockerConfig

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('schism_runs.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

def run_with_monitoring(config_file):
    """Run configuration with detailed monitoring."""
    start_time = time.time()

    try:
        logger.info(f"Starting run: {config_file}")

        with open(config_file, "r") as f:
            config = yaml.safe_load(f)

        model_run = ModelRun(**config)

        logger.info(f"Run ID: {model_run.run_id}")
        logger.info(f"Period: {model_run.period.start} to {model_run.period.end}")

        docker_config = DockerConfig(
            dockerfile=Path("Dockerfile"),
            build_context=Path("docker/schism"),
            timeout=3600,
            cpu=8,
            memory="4g"
        )

        # Generate configuration
        logger.info("Generating configuration files...")
        model_run.generate()

        # Execute simulation
        logger.info("Starting SCHISM simulation...")
        success = model_run.run(backend=docker_config)

        execution_time = time.time() - start_time

        if success:
            logger.info(f"✅ Run completed successfully in {execution_time:.1f}s")

            # Check output files
            output_dir = Path(config["output_dir"])
            outputs = list(output_dir.glob("**/outputs/*.nc"))
            logger.info(f"Generated {len(outputs)} output files")

            return True
        else:
            logger.error(f"❌ Run failed after {execution_time:.1f}s")
            return False

    except Exception as e:
        execution_time = time.time() - start_time
        logger.error(f"❌ Run crashed after {execution_time:.1f}s: {e}")
        return False

Troubleshooting#

Common Issues and Solutions#

Docker Build Failures:

# Check Docker daemon
docker version

# Verify Dockerfile exists
ls -la docker/schism/Dockerfile

# Test manual build
cd docker/schism
docker build -t test-schism .

Configuration Errors:

# Validate YAML syntax
import yaml
with open("my_config.yaml", "r") as f:
    config = yaml.safe_load(f)
print("✅ YAML is valid")

# Test ModelRun creation
from rompy.model import ModelRun
model_run = ModelRun(**config)
print("✅ ModelRun created successfully")

Resource Issues:

# Check system resources
import psutil
print(f"Available CPUs: {psutil.cpu_count()}")
print(f"Available Memory: {psutil.virtual_memory().available / 1024**3:.1f} GB")

# Adjust Docker configuration accordingly
docker_config = DockerConfig(
    cpu=min(8, psutil.cpu_count()),
    memory=f"{min(4, psutil.virtual_memory().available // 1024**3)}g"
)

File Path Issues:

# Use absolute paths for volume mounts
from pathlib import Path
output_dir = Path("my_output").absolute()

docker_config = DockerConfig(
    volumes=[f"{output_dir}:/tmp/schism:rw"]
)

Best Practices Summary#

  1. Start Simple - Begin with basic tidal configurations

  2. Test Configurations - Use dry runs to validate before execution

  3. Monitor Resources - Set appropriate CPU and memory limits

  4. Use Absolute Paths - Avoid path resolution issues

  5. Enable Logging - Track execution progress and errors

  6. Clean Up - Remove containers after execution

  7. Version Control - Track configuration changes

  8. Document Runs - Record parameters and results

Next Steps#

After completing this tutorial, you should be able to:

  • Create and run basic SCHISM configurations

  • Use the Docker backend framework effectively

  • Configure complex boundary conditions

  • Implement automated testing

  • Monitor and troubleshoot runs

For more advanced features, see:

Happy modeling with SCHISM and ROMPY! 🌊