Skip to content

SCHISM Boundary Conditions

Overview

The SCHISM boundary conditions system provides a unified interface for configuring all types of boundary conditions in SCHISM simulations. This system replaces the previous separate tidal and ocean configurations with a single, flexible approach that supports:

  • Harmonic boundaries - Pure harmonic tidal forcing using tidal constituents
  • Hybrid boundaries - Combined harmonic and external data forcing
  • River boundaries - Constant or time-varying river inputs
  • Nested boundaries - Coupling with parent model outputs
  • Custom configurations - Flexible mixing of different boundary types

Key Classes

SCHISMDataBoundaryConditions

The main class for configuring boundary conditions. This unified interface handles all boundary types and their associated data sources.

SCHISMDataBoundaryConditions

Bases: RompyBaseModel

This class configures all boundary conditions for SCHISM including tidal, ocean, river, and nested model boundaries.

It provides a unified interface for specifying boundary conditions and their data sources, replacing the separate tides and ocean configurations.

Source code in rompy_schism/data.py
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
class SCHISMDataBoundaryConditions(RompyBaseModel):
    """
    This class configures all boundary conditions for SCHISM including tidal,
    ocean, river, and nested model boundaries.

    It provides a unified interface for specifying boundary conditions and their
    data sources, replacing the separate tides and ocean configurations.
    """

    # Allow arbitrary types for schema generation
    model_config = ConfigDict(arbitrary_types_allowed=True)

    data_type: Literal["boundary_conditions"] = Field(
        default="boundary_conditions",
        description="Model type discriminator",
    )

    # Tidal dataset specification
    tidal_data: Optional[TidalDataset] = Field(
        None,
        description="Tidal forcing dataset",
    )

    # Boundary configurations with integrated data sources
    boundaries: Dict[int, BoundarySetupWithSource] = Field(
        default_factory=dict,
        description="Boundary configuration by boundary index",
    )

    # Predefined configuration types
    setup_type: Optional[Literal["tidal", "hybrid", "river", "nested"]] = Field(
        None, description="Predefined boundary setup type"
    )

    # Hotstart configuration
    hotstart_config: Optional[HotstartConfig] = Field(
        None, description="Configuration for hotstart file generation"
    )

    @model_validator(mode="before")
    @classmethod
    def convert_numpy_types(cls, data):
        """Convert any numpy values to Python native types"""
        if not isinstance(data, dict):
            return data

        for key, value in list(data.items()):
            if isinstance(value, (np.bool_, np.integer, np.floating, np.ndarray)):
                data[key] = to_python_type(value)
        return data

    @model_validator(mode="after")
    def validate_tidal_data(self):
        """Ensure tidal data is provided when needed for TIDAL or TIDALSPACETIME boundaries."""
        boundaries = self.boundaries or {}
        needs_tidal_data = False

        # Check setup_type first
        if self.setup_type in ["tidal", "hybrid"]:
            needs_tidal_data = True

        # Then check individual boundaries
        for setup in boundaries.values():
            if (
                hasattr(setup, "elev_type")
                and setup.elev_type
                in [ElevationType.HARMONIC, ElevationType.HARMONICEXTERNAL]
            ) or (
                hasattr(setup, "vel_type")
                and setup.vel_type
                in [VelocityType.HARMONIC, VelocityType.HARMONICEXTERNAL]
            ):
                needs_tidal_data = True
                break

        if needs_tidal_data and not self.tidal_data:
            raise ValueError(
                "Tidal data is required for HARMONIC or HARMONICEXTERNAL boundary types but was not provided"
            )

        return self

    @model_validator(mode="after")
    def validate_setup_type(self):
        """Validate setup type specific requirements."""
        # Skip validation if setup_type is not set
        if not self.setup_type:
            return self

        if self.setup_type in ["tidal", "hybrid"]:
            if not self.tidal_data:
                raise ValueError(
                    "tidal_data is required for tidal or hybrid setup_type"
                )

        elif self.setup_type == "river":
            if self.boundaries:
                has_flow = any(
                    hasattr(s, "const_flow") and s.const_flow is not None
                    for s in self.boundaries.values()
                )
                if not has_flow:
                    raise ValueError(
                        "At least one boundary should have const_flow for river setup_type"
                    )

        elif self.setup_type == "nested":
            if self.boundaries:
                for idx, setup in self.boundaries.items():
                    if (
                        hasattr(setup, "vel_type")
                        and setup.vel_type == VelocityType.RELAXED
                    ):
                        if not hasattr(setup, "inflow_relax") or not hasattr(
                            setup, "outflow_relax"
                        ):
                            logger.warning(
                                f"inflow_relax and outflow_relax are recommended for nested setup_type in boundary {idx}"
                            )
        else:
            raise ValueError(
                f"Unknown setup_type: {self.setup_type}. Expected one of: tidal, hybrid, river, nested"
            )

        return self

    def _create_boundary_config(self, grid):
        """Create a TidalBoundary object based on the configuration."""
        # Get tidal data paths
        if self.tidal_data:
            if (
                hasattr(self.tidal_data, "tidal_database")
                and self.tidal_data.tidal_database
            ):
                str(self.tidal_data.tidal_database)

        # Ensure boundary information is computed
        if hasattr(grid.pylibs_hgrid, "compute_bnd"):
            grid.pylibs_hgrid.compute_bnd()
        else:
            logger.warning(
                "Grid object doesn't have compute_bnd method. Boundary information may be missing."
            )

        # Create a new TidalBoundary with all the configuration
        # Ensure boundary information is computed before creating the boundary
        if not hasattr(grid.pylibs_hgrid, "nob") or not hasattr(
            grid.pylibs_hgrid, "nobn"
        ):
            logger.info("Computing boundary information before creating TidalBoundary")
            # First try compute_bnd if available
            if hasattr(grid.pylibs_hgrid, "compute_bnd"):
                grid.pylibs_hgrid.compute_bnd()

            # Then try compute_all if nob is still missing
            if not hasattr(grid.pylibs_hgrid, "nob") and hasattr(
                grid.pylibs_hgrid, "compute_all"
            ):
                if hasattr(grid.pylibs_hgrid, "compute_all"):
                    grid.pylibs_hgrid.compute_all()

        # Verify boundary attributes are available
        if not hasattr(grid.pylibs_hgrid, "nob"):
            logger.error("Failed to set 'nob' attribute on grid.pylibs_hgrid")
            raise AttributeError(
                "Missing required 'nob' attribute on grid.pylibs_hgrid"
            )

        # Create TidalBoundary with pre-computed grid to avoid losing boundary info
        # Get the grid path for TidalBoundary
        grid_path = (
            str(grid.hgrid.path)
            if hasattr(grid, "hgrid") and hasattr(grid.hgrid, "path")
            else None
        )
        if grid_path is None:
            # Create a temporary file with the grid if needed
            import tempfile

            temp_file = tempfile.NamedTemporaryFile(suffix=".gr3", delete=False)
            temp_path = temp_file.name
            temp_file.close()
            grid.pylibs_hgrid.write_hgrid(temp_path)
            grid_path = temp_path

        boundary = BoundaryHandler(grid_path=grid_path, tidal_data=self.tidal_data)

        # Replace the TidalBoundary's grid with our pre-computed one to preserve boundary info
        boundary.grid = grid.pylibs_hgrid

        # Configure each boundary segment
        for idx, setup in self.boundaries.items():
            boundary_config = setup.to_boundary_config()
            boundary.set_boundary_config(idx, boundary_config)

        return boundary

    def get(
        self,
        destdir: str | Path,
        grid: SCHISMGrid,
        time: TimeRange,
    ) -> Dict[str, str]:
        """
        Process all boundary data and generate necessary input files.

        Parameters
        ----------
        destdir : str | Path
            Destination directory
        grid : SCHISMGrid
            SCHISM grid instance
        time : TimeRange
            Time range for the simulation

        Returns
        -------
        Dict[str, str]
            Paths to generated files
        """
        # Processing boundary conditions

        # Convert destdir to Path object
        destdir = Path(destdir)

        # Create destdir if it doesn't exist
        if not destdir.exists():
            logger.info(f"Creating destination directory: {destdir}")
            destdir.mkdir(parents=True, exist_ok=True)

        # # 1. Process tidal data if needed
        if self.tidal_data:
            logger.info(
                f"{ARROW} Processing tidal constituents: {', '.join(self.tidal_data.constituents) if hasattr(self.tidal_data, 'constituents') else 'default'}"
            )
            self.tidal_data.get(grid)

        # 2. Create boundary condition file (bctides.in)
        boundary = self._create_boundary_config(grid)

        # Set start time and run duration
        start_time = time.start
        if time.end is not None and time.start is not None:
            run_days = (
                time.end - time.start
            ).total_seconds() / 86400.0  # Convert to days
        else:
            run_days = 1.0  # Default to 1 day if time is not properly specified
        boundary.set_run_parameters(start_time, run_days)

        # Generate bctides.in file
        bctides_path = destdir / "bctides.in"
        logger.info(f"{ARROW} Generating boundary condition file: bctides.in")

        # Ensure grid object has complete boundary information before writing
        if hasattr(grid.pylibs_hgrid, "compute_all"):
            grid.pylibs_hgrid.compute_all()

        # Double-check all required attributes are present
        required_attrs = ["nob", "nobn", "iobn"]
        missing_attrs = [
            attr
            for attr in required_attrs
            if not (grid.pylibs_hgrid and hasattr(grid.pylibs_hgrid, attr))
        ]
        if missing_attrs:
            error_msg = (
                f"Grid is missing required attributes: {', '.join(missing_attrs)}"
            )
            logger.error(error_msg)
            raise AttributeError(error_msg)

        # Write the boundary file - no fallbacks
        boundary.write_boundary_file(bctides_path)
        logger.info(f"{ARROW} Boundary conditions written successfully")

        # 3. Process ocean data based on boundary configurations
        processed_files = {"bctides": str(bctides_path)}

        # Collect variables to process and source information for logging
        variables_to_process = []
        source_files = set()
        for idx, setup in self.boundaries.items():
            if (
                setup.elev_type
                in [ElevationType.EXTERNAL, ElevationType.HARMONICEXTERNAL]
                and setup.elev_source
            ):
                variables_to_process.append("elevation")
                if hasattr(setup.elev_source, "source") and hasattr(
                    setup.elev_source.source, "uri"
                ):
                    source_files.add(str(setup.elev_source.source.uri))
            if (
                setup.vel_type
                in [
                    VelocityType.EXTERNAL,
                    VelocityType.HARMONICEXTERNAL,
                    VelocityType.RELAXED,
                ]
                and setup.vel_source
            ):
                variables_to_process.append("velocity")
                if hasattr(setup.vel_source, "source") and hasattr(
                    setup.vel_source.source, "uri"
                ):
                    source_files.add(str(setup.vel_source.source.uri))
            if setup.temp_type == TracerType.EXTERNAL and setup.temp_source:
                variables_to_process.append("temperature")
                if hasattr(setup.temp_source, "source") and hasattr(
                    setup.temp_source.source, "uri"
                ):
                    source_files.add(str(setup.temp_source.source.uri))
            if setup.salt_type == TracerType.EXTERNAL and setup.salt_source:
                variables_to_process.append("salinity")
                if hasattr(setup.salt_source, "source") and hasattr(
                    setup.salt_source.source, "uri"
                ):
                    source_files.add(str(setup.salt_source.source.uri))

        if variables_to_process:
            unique_vars = list(
                dict.fromkeys(variables_to_process)
            )  # Remove duplicates while preserving order
            logger.info(f"{ARROW} Processing boundary data: {', '.join(unique_vars)}")
            if source_files:
                if len(source_files) == 1:
                    logger.info(f"  • Source: {list(source_files)[0]}")
                else:
                    logger.info(f"  • Sources: {len(source_files)} files")

        # Process each data source based on the boundary type
        for idx, setup in self.boundaries.items():
            # Process elevation data if needed
            if setup.elev_type in [
                ElevationType.EXTERNAL,
                ElevationType.HARMONICEXTERNAL,
            ]:
                if setup.elev_source:
                    if (
                        hasattr(setup.elev_source, "data_type")
                        and setup.elev_source.data_type == "boundary"
                    ):
                        # Process using SCHISMDataBoundary interface
                        setup.elev_source.id = "elev2D"  # Set the ID for the boundary
                        file_path = setup.elev_source.get(destdir, grid, time)
                    else:
                        # Process using DataBlob interface
                        file_path = setup.elev_source.get(str(destdir))
                    processed_files[f"elev_boundary_{idx}"] = file_path

            # Process velocity data if needed
            if setup.vel_type in [
                VelocityType.EXTERNAL,
                VelocityType.HARMONICEXTERNAL,
                VelocityType.RELAXED,
            ]:
                if setup.vel_source:
                    if (
                        hasattr(setup.vel_source, "data_type")
                        and setup.vel_source.data_type == "boundary"
                    ):
                        # Process using SCHISMDataBoundary interface
                        setup.vel_source.id = "uv3D"  # Set the ID for the boundary
                        file_path = setup.vel_source.get(destdir, grid, time)
                    else:
                        # Process using DataBlob interface
                        file_path = setup.vel_source.get(str(destdir))
                    processed_files[f"vel_boundary_{idx}"] = file_path

            # Process temperature data if needed
            if setup.temp_type == TracerType.EXTERNAL:
                if setup.temp_source:
                    if (
                        hasattr(setup.temp_source, "data_type")
                        and setup.temp_source.data_type == "boundary"
                    ):
                        # Process using SCHISMDataBoundary interface
                        setup.temp_source.id = "TEM_3D"  # Set the ID for the boundary
                        file_path = setup.temp_source.get(destdir, grid, time)
                    else:
                        # Process using DataBlob interface
                        file_path = setup.temp_source.get(str(destdir))
                    processed_files[f"temp_boundary_{idx}"] = file_path

            # Process salinity data if needed
            if setup.salt_type == TracerType.EXTERNAL:
                if setup.salt_source:
                    if (
                        hasattr(setup.salt_source, "data_type")
                        and setup.salt_source.data_type == "boundary"
                    ):
                        # Process using SCHISMDataBoundary interface
                        setup.salt_source.id = "SAL_3D"  # Set the ID for the boundary
                        file_path = setup.salt_source.get(destdir, grid, time)
                    else:
                        # Process using DataBlob interface
                        file_path = setup.salt_source.get(str(destdir))
                    processed_files[f"salt_boundary_{idx}"] = file_path

        # Generate hotstart file if configured
        if self.hotstart_config and self.hotstart_config.enabled:
            logger.info(f"{ARROW} Generating hotstart file")
            hotstart_path = self._generate_hotstart(destdir, grid, time)
            processed_files["hotstart"] = hotstart_path
            logger.info(f"  • Output: {hotstart_path}")

        # Log summary of processed files with more details
        boundary_data_files = [f for k, f in processed_files.items() if "boundary" in k]
        if boundary_data_files:
            logger.info(
                f"  • Files: {', '.join([Path(f).name for f in boundary_data_files])}"
            )

        return processed_files

    def _generate_hotstart(
        self,
        destdir: Union[str, Path],
        grid: SCHISMGrid,
        time: Optional[TimeRange] = None,
    ) -> str:
        """
        Generate hotstart file using boundary condition data sources.

        Args:
            destdir: Destination directory for the hotstart file
            grid: SCHISM grid object
            time: Time range for the data

        Returns:
            Path to the generated hotstart file
        """
        from rompy_schism.hotstart import SCHISMDataHotstart

        # Find a boundary that has both temperature and salinity sources
        temp_source = None
        salt_source = None

        for boundary_config in self.boundaries.values():
            if boundary_config.temp_source is not None:
                temp_source = boundary_config.temp_source
            if boundary_config.salt_source is not None:
                salt_source = boundary_config.salt_source

            # If we found both, we can proceed
            if temp_source is not None and salt_source is not None:
                break

        if temp_source is None or salt_source is None:
            raise ValueError(
                "Hotstart generation requires both temperature and salinity sources "
                "to be configured in boundary conditions"
            )

        # Create hotstart instance using the first available source
        # (assuming temp and salt sources point to the same dataset)
        # Include both temperature and salinity variables for hotstart generation
        temp_var_name = (
            self.hotstart_config.temp_var if self.hotstart_config else "temperature"
        )
        salt_var_name = (
            self.hotstart_config.salt_var if self.hotstart_config else "salinity"
        )

        # Log hotstart generation details
        logger.info(f"  • Variables: {temp_var_name}, {salt_var_name}")
        if hasattr(temp_source, "source") and hasattr(temp_source.source, "uri"):
            logger.info(f"  • Source: {temp_source.source.uri}")

        hotstart_data = SCHISMDataHotstart(
            source=temp_source.source,
            variables=[temp_var_name, salt_var_name],
            coords=getattr(temp_source, "coords", None),
            temp_var=temp_var_name,
            salt_var=salt_var_name,
            time_offset=(
                self.hotstart_config.time_offset if self.hotstart_config else 0.0
            ),
            time_base=(
                self.hotstart_config.time_base
                if self.hotstart_config
                else datetime(2000, 1, 1)
            ),
            output_filename=(
                self.hotstart_config.output_filename
                if self.hotstart_config
                else "hotstart.nc"
            ),
        )

        return hotstart_data.get(str(destdir), grid=grid, time=time)

Attributes

model_config class-attribute instance-attribute

model_config = ConfigDict(arbitrary_types_allowed=True)

data_type class-attribute instance-attribute

data_type: Literal['boundary_conditions'] = Field(default='boundary_conditions', description='Model type discriminator')

tidal_data class-attribute instance-attribute

tidal_data: Optional[TidalDataset] = Field(None, description='Tidal forcing dataset')

boundaries class-attribute instance-attribute

boundaries: Dict[int, BoundarySetupWithSource] = Field(default_factory=dict, description='Boundary configuration by boundary index')

setup_type class-attribute instance-attribute

setup_type: Optional[Literal['tidal', 'hybrid', 'river', 'nested']] = Field(None, description='Predefined boundary setup type')

hotstart_config class-attribute instance-attribute

hotstart_config: Optional[HotstartConfig] = Field(None, description='Configuration for hotstart file generation')

Functions

convert_numpy_types classmethod

convert_numpy_types(data)

Convert any numpy values to Python native types

Source code in rompy_schism/data.py
@model_validator(mode="before")
@classmethod
def convert_numpy_types(cls, data):
    """Convert any numpy values to Python native types"""
    if not isinstance(data, dict):
        return data

    for key, value in list(data.items()):
        if isinstance(value, (np.bool_, np.integer, np.floating, np.ndarray)):
            data[key] = to_python_type(value)
    return data

validate_tidal_data

validate_tidal_data()

Ensure tidal data is provided when needed for TIDAL or TIDALSPACETIME boundaries.

Source code in rompy_schism/data.py
@model_validator(mode="after")
def validate_tidal_data(self):
    """Ensure tidal data is provided when needed for TIDAL or TIDALSPACETIME boundaries."""
    boundaries = self.boundaries or {}
    needs_tidal_data = False

    # Check setup_type first
    if self.setup_type in ["tidal", "hybrid"]:
        needs_tidal_data = True

    # Then check individual boundaries
    for setup in boundaries.values():
        if (
            hasattr(setup, "elev_type")
            and setup.elev_type
            in [ElevationType.HARMONIC, ElevationType.HARMONICEXTERNAL]
        ) or (
            hasattr(setup, "vel_type")
            and setup.vel_type
            in [VelocityType.HARMONIC, VelocityType.HARMONICEXTERNAL]
        ):
            needs_tidal_data = True
            break

    if needs_tidal_data and not self.tidal_data:
        raise ValueError(
            "Tidal data is required for HARMONIC or HARMONICEXTERNAL boundary types but was not provided"
        )

    return self

validate_setup_type

validate_setup_type()

Validate setup type specific requirements.

Source code in rompy_schism/data.py
@model_validator(mode="after")
def validate_setup_type(self):
    """Validate setup type specific requirements."""
    # Skip validation if setup_type is not set
    if not self.setup_type:
        return self

    if self.setup_type in ["tidal", "hybrid"]:
        if not self.tidal_data:
            raise ValueError(
                "tidal_data is required for tidal or hybrid setup_type"
            )

    elif self.setup_type == "river":
        if self.boundaries:
            has_flow = any(
                hasattr(s, "const_flow") and s.const_flow is not None
                for s in self.boundaries.values()
            )
            if not has_flow:
                raise ValueError(
                    "At least one boundary should have const_flow for river setup_type"
                )

    elif self.setup_type == "nested":
        if self.boundaries:
            for idx, setup in self.boundaries.items():
                if (
                    hasattr(setup, "vel_type")
                    and setup.vel_type == VelocityType.RELAXED
                ):
                    if not hasattr(setup, "inflow_relax") or not hasattr(
                        setup, "outflow_relax"
                    ):
                        logger.warning(
                            f"inflow_relax and outflow_relax are recommended for nested setup_type in boundary {idx}"
                        )
    else:
        raise ValueError(
            f"Unknown setup_type: {self.setup_type}. Expected one of: tidal, hybrid, river, nested"
        )

    return self

get

get(destdir: str | Path, grid: SCHISMGrid, time: TimeRange) -> Dict[str, str]

Process all boundary data and generate necessary input files.

Parameters

destdir : str | Path Destination directory grid : SCHISMGrid SCHISM grid instance time : TimeRange Time range for the simulation

Returns

Dict[str, str] Paths to generated files

Source code in rompy_schism/data.py
def get(
    self,
    destdir: str | Path,
    grid: SCHISMGrid,
    time: TimeRange,
) -> Dict[str, str]:
    """
    Process all boundary data and generate necessary input files.

    Parameters
    ----------
    destdir : str | Path
        Destination directory
    grid : SCHISMGrid
        SCHISM grid instance
    time : TimeRange
        Time range for the simulation

    Returns
    -------
    Dict[str, str]
        Paths to generated files
    """
    # Processing boundary conditions

    # Convert destdir to Path object
    destdir = Path(destdir)

    # Create destdir if it doesn't exist
    if not destdir.exists():
        logger.info(f"Creating destination directory: {destdir}")
        destdir.mkdir(parents=True, exist_ok=True)

    # # 1. Process tidal data if needed
    if self.tidal_data:
        logger.info(
            f"{ARROW} Processing tidal constituents: {', '.join(self.tidal_data.constituents) if hasattr(self.tidal_data, 'constituents') else 'default'}"
        )
        self.tidal_data.get(grid)

    # 2. Create boundary condition file (bctides.in)
    boundary = self._create_boundary_config(grid)

    # Set start time and run duration
    start_time = time.start
    if time.end is not None and time.start is not None:
        run_days = (
            time.end - time.start
        ).total_seconds() / 86400.0  # Convert to days
    else:
        run_days = 1.0  # Default to 1 day if time is not properly specified
    boundary.set_run_parameters(start_time, run_days)

    # Generate bctides.in file
    bctides_path = destdir / "bctides.in"
    logger.info(f"{ARROW} Generating boundary condition file: bctides.in")

    # Ensure grid object has complete boundary information before writing
    if hasattr(grid.pylibs_hgrid, "compute_all"):
        grid.pylibs_hgrid.compute_all()

    # Double-check all required attributes are present
    required_attrs = ["nob", "nobn", "iobn"]
    missing_attrs = [
        attr
        for attr in required_attrs
        if not (grid.pylibs_hgrid and hasattr(grid.pylibs_hgrid, attr))
    ]
    if missing_attrs:
        error_msg = (
            f"Grid is missing required attributes: {', '.join(missing_attrs)}"
        )
        logger.error(error_msg)
        raise AttributeError(error_msg)

    # Write the boundary file - no fallbacks
    boundary.write_boundary_file(bctides_path)
    logger.info(f"{ARROW} Boundary conditions written successfully")

    # 3. Process ocean data based on boundary configurations
    processed_files = {"bctides": str(bctides_path)}

    # Collect variables to process and source information for logging
    variables_to_process = []
    source_files = set()
    for idx, setup in self.boundaries.items():
        if (
            setup.elev_type
            in [ElevationType.EXTERNAL, ElevationType.HARMONICEXTERNAL]
            and setup.elev_source
        ):
            variables_to_process.append("elevation")
            if hasattr(setup.elev_source, "source") and hasattr(
                setup.elev_source.source, "uri"
            ):
                source_files.add(str(setup.elev_source.source.uri))
        if (
            setup.vel_type
            in [
                VelocityType.EXTERNAL,
                VelocityType.HARMONICEXTERNAL,
                VelocityType.RELAXED,
            ]
            and setup.vel_source
        ):
            variables_to_process.append("velocity")
            if hasattr(setup.vel_source, "source") and hasattr(
                setup.vel_source.source, "uri"
            ):
                source_files.add(str(setup.vel_source.source.uri))
        if setup.temp_type == TracerType.EXTERNAL and setup.temp_source:
            variables_to_process.append("temperature")
            if hasattr(setup.temp_source, "source") and hasattr(
                setup.temp_source.source, "uri"
            ):
                source_files.add(str(setup.temp_source.source.uri))
        if setup.salt_type == TracerType.EXTERNAL and setup.salt_source:
            variables_to_process.append("salinity")
            if hasattr(setup.salt_source, "source") and hasattr(
                setup.salt_source.source, "uri"
            ):
                source_files.add(str(setup.salt_source.source.uri))

    if variables_to_process:
        unique_vars = list(
            dict.fromkeys(variables_to_process)
        )  # Remove duplicates while preserving order
        logger.info(f"{ARROW} Processing boundary data: {', '.join(unique_vars)}")
        if source_files:
            if len(source_files) == 1:
                logger.info(f"  • Source: {list(source_files)[0]}")
            else:
                logger.info(f"  • Sources: {len(source_files)} files")

    # Process each data source based on the boundary type
    for idx, setup in self.boundaries.items():
        # Process elevation data if needed
        if setup.elev_type in [
            ElevationType.EXTERNAL,
            ElevationType.HARMONICEXTERNAL,
        ]:
            if setup.elev_source:
                if (
                    hasattr(setup.elev_source, "data_type")
                    and setup.elev_source.data_type == "boundary"
                ):
                    # Process using SCHISMDataBoundary interface
                    setup.elev_source.id = "elev2D"  # Set the ID for the boundary
                    file_path = setup.elev_source.get(destdir, grid, time)
                else:
                    # Process using DataBlob interface
                    file_path = setup.elev_source.get(str(destdir))
                processed_files[f"elev_boundary_{idx}"] = file_path

        # Process velocity data if needed
        if setup.vel_type in [
            VelocityType.EXTERNAL,
            VelocityType.HARMONICEXTERNAL,
            VelocityType.RELAXED,
        ]:
            if setup.vel_source:
                if (
                    hasattr(setup.vel_source, "data_type")
                    and setup.vel_source.data_type == "boundary"
                ):
                    # Process using SCHISMDataBoundary interface
                    setup.vel_source.id = "uv3D"  # Set the ID for the boundary
                    file_path = setup.vel_source.get(destdir, grid, time)
                else:
                    # Process using DataBlob interface
                    file_path = setup.vel_source.get(str(destdir))
                processed_files[f"vel_boundary_{idx}"] = file_path

        # Process temperature data if needed
        if setup.temp_type == TracerType.EXTERNAL:
            if setup.temp_source:
                if (
                    hasattr(setup.temp_source, "data_type")
                    and setup.temp_source.data_type == "boundary"
                ):
                    # Process using SCHISMDataBoundary interface
                    setup.temp_source.id = "TEM_3D"  # Set the ID for the boundary
                    file_path = setup.temp_source.get(destdir, grid, time)
                else:
                    # Process using DataBlob interface
                    file_path = setup.temp_source.get(str(destdir))
                processed_files[f"temp_boundary_{idx}"] = file_path

        # Process salinity data if needed
        if setup.salt_type == TracerType.EXTERNAL:
            if setup.salt_source:
                if (
                    hasattr(setup.salt_source, "data_type")
                    and setup.salt_source.data_type == "boundary"
                ):
                    # Process using SCHISMDataBoundary interface
                    setup.salt_source.id = "SAL_3D"  # Set the ID for the boundary
                    file_path = setup.salt_source.get(destdir, grid, time)
                else:
                    # Process using DataBlob interface
                    file_path = setup.salt_source.get(str(destdir))
                processed_files[f"salt_boundary_{idx}"] = file_path

    # Generate hotstart file if configured
    if self.hotstart_config and self.hotstart_config.enabled:
        logger.info(f"{ARROW} Generating hotstart file")
        hotstart_path = self._generate_hotstart(destdir, grid, time)
        processed_files["hotstart"] = hotstart_path
        logger.info(f"  • Output: {hotstart_path}")

    # Log summary of processed files with more details
    boundary_data_files = [f for k, f in processed_files.items() if "boundary" in k]
    if boundary_data_files:
        logger.info(
            f"  • Files: {', '.join([Path(f).name for f in boundary_data_files])}"
        )

    return processed_files

BoundarySetupWithSource

Configures individual boundary segments with their data sources and boundary condition types.

BoundarySetupWithSource

Bases: BoundarySetup

Enhanced boundary setup that includes data sources.

This class extends BoundarySetup to provide a unified configuration for both boundary conditions and their data sources.

Source code in rompy_schism/data.py
class BoundarySetupWithSource(BoundarySetup):
    """
    Enhanced boundary setup that includes data sources.

    This class extends BoundarySetup to provide a unified configuration
    for both boundary conditions and their data sources.
    """

    elev_source: Optional[Union[DataBlob, DataGrid, SCHISMDataBoundary]] = Field(
        None, description="Data source for elevation boundary condition"
    )
    vel_source: Optional[Union[DataBlob, DataGrid, SCHISMDataBoundary]] = Field(
        None, description="Data source for velocity boundary condition"
    )
    temp_source: Optional[Union[DataBlob, DataGrid, SCHISMDataBoundary]] = Field(
        None, description="Data source for temperature boundary condition"
    )
    salt_source: Optional[Union[DataBlob, DataGrid, SCHISMDataBoundary]] = Field(
        None, description="Data source for salinity boundary condition"
    )

    @model_validator(mode="after")
    def validate_data_sources(self):
        """Ensure data sources are provided when needed for space-time boundary types."""
        # Check elevation data source
        if (
            self.elev_type in [ElevationType.EXTERNAL, ElevationType.HARMONICEXTERNAL]
            and self.elev_source is None
        ):
            logger.warning(
                "elev_source should be provided for EXTERNAL or HARMONICEXTERNAL elevation type"
            )

        # Check velocity data source
        if (
            self.vel_type
            in [
                VelocityType.EXTERNAL,
                VelocityType.HARMONICEXTERNAL,
                VelocityType.RELAXED,
            ]
            and self.vel_source is None
        ):
            logger.warning(
                "vel_source should be provided for EXTERNAL, HARMONICEXTERNAL, or RELAXED velocity type"
            )

        # Check temperature data source
        if self.temp_type == TracerType.EXTERNAL and self.temp_source is None:
            logger.warning(
                "temp_source should be provided for EXTERNAL temperature type"
            )

        # Check salinity data source
        if self.salt_type == TracerType.EXTERNAL and self.salt_source is None:
            logger.warning("salt_source should be provided for EXTERNAL salinity type")

        return self

Attributes

elev_source class-attribute instance-attribute

elev_source: Optional[Union[DataBlob, DataGrid, SCHISMDataBoundary]] = Field(None, description='Data source for elevation boundary condition')

vel_source class-attribute instance-attribute

vel_source: Optional[Union[DataBlob, DataGrid, SCHISMDataBoundary]] = Field(None, description='Data source for velocity boundary condition')

temp_source class-attribute instance-attribute

temp_source: Optional[Union[DataBlob, DataGrid, SCHISMDataBoundary]] = Field(None, description='Data source for temperature boundary condition')

salt_source class-attribute instance-attribute

salt_source: Optional[Union[DataBlob, DataGrid, SCHISMDataBoundary]] = Field(None, description='Data source for salinity boundary condition')

Functions

validate_data_sources

validate_data_sources()

Ensure data sources are provided when needed for space-time boundary types.

Source code in rompy_schism/data.py
@model_validator(mode="after")
def validate_data_sources(self):
    """Ensure data sources are provided when needed for space-time boundary types."""
    # Check elevation data source
    if (
        self.elev_type in [ElevationType.EXTERNAL, ElevationType.HARMONICEXTERNAL]
        and self.elev_source is None
    ):
        logger.warning(
            "elev_source should be provided for EXTERNAL or HARMONICEXTERNAL elevation type"
        )

    # Check velocity data source
    if (
        self.vel_type
        in [
            VelocityType.EXTERNAL,
            VelocityType.HARMONICEXTERNAL,
            VelocityType.RELAXED,
        ]
        and self.vel_source is None
    ):
        logger.warning(
            "vel_source should be provided for EXTERNAL, HARMONICEXTERNAL, or RELAXED velocity type"
        )

    # Check temperature data source
    if self.temp_type == TracerType.EXTERNAL and self.temp_source is None:
        logger.warning(
            "temp_source should be provided for EXTERNAL temperature type"
        )

    # Check salinity data source
    if self.salt_type == TracerType.EXTERNAL and self.salt_source is None:
        logger.warning("salt_source should be provided for EXTERNAL salinity type")

    return self

BoundaryHandler

Core boundary handler that extends BoundaryData and supports all SCHISM boundary types.

BoundaryHandler

Bases: BoundaryData

Handler for SCHISM boundary conditions.

This class extends BoundaryData to handle all SCHISM boundary condition types including tidal, river, nested, and hybrid configurations.

Source code in rompy_schism/boundary_core.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
class BoundaryHandler(BoundaryData):
    """Handler for SCHISM boundary conditions.

    This class extends BoundaryData to handle all SCHISM boundary condition types
    including tidal, river, nested, and hybrid configurations.
    """

    def __init__(
        self,
        grid_path: Union[str, Path],
        tidal_data: Optional[TidalDataset] = None,
        boundary_configs: Optional[Dict[int, BoundaryConfig]] = None,
        *args,
        **kwargs,
    ):
        """Initialize the boundary handler.

        Parameters
        ----------
        grid_path : str or Path
            Path to the SCHISM grid file
        tidal_data : TidalDataset, optional
            Tidal dataset containing specification of tidal forcing
        boundary_configs : dict, optional
            Configuration for each boundary, keyed by boundary index

        """
        super().__init__(grid_path, *args, **kwargs)

        self.tidal_data = tidal_data
        self.boundary_configs = boundary_configs if boundary_configs is not None else {}

        # For storing start time and run duration
        self._start_time = None
        self._rnday = None

        # Additional file paths for various boundary types
        self.temp_th_path = None  # Temperature time history
        self.temp_3d_path = None  # 3D temperature
        self.salt_th_path = None  # Salinity time history
        self.salt_3d_path = None  # 3D salinity
        self.flow_th_path = None  # Flow time history
        self.elev_st_path = None  # Space-time elevation
        self.vel_st_path = None  # Space-time velocity

    def set_boundary_config(self, boundary_index: int, config: BoundaryConfig):
        """Set configuration for a specific boundary.

        Parameters
        ----------
        boundary_index : int
            Index of the boundary
        config : BoundaryConfig
            Configuration for the boundary
        """
        self.boundary_configs[boundary_index] = config

    def set_boundary_type(
        self,
        boundary_index: int,
        elev_type: ElevationType,
        vel_type: VelocityType,
        temp_type: TracerType = TracerType.NONE,
        salt_type: TracerType = TracerType.NONE,
        **kwargs,
    ):
        """Set boundary types for a specific boundary.

        Parameters
        ----------
        boundary_index : int
            Index of the boundary
        elev_type : ElevationType
            Elevation boundary condition type
        vel_type : VelocityType
            Velocity boundary condition type
        temp_type : TracerType, optional
            Temperature boundary condition type
        salt_type : TracerType, optional
            Salinity boundary condition type
        **kwargs
            Additional parameters for the boundary configuration
        """
        config = BoundaryConfig(
            elev_type=elev_type,
            vel_type=vel_type,
            temp_type=temp_type,
            salt_type=salt_type,
            **kwargs,
        )
        self.set_boundary_config(boundary_index, config)

    def set_run_parameters(self, start_time, run_days):
        """Set start time and run duration.

        Parameters
        ----------
        start_time : datetime or list
            Start time for the simulation
        run_days : float
            Duration of the simulation in days
        """
        self._start_time = start_time
        self._rnday = run_days

    def get_flags_list(self) -> List[List[int]]:
        """Get list of boundary flags for Bctides.

        Returns
        -------
        list of list of int
            Boundary flags for each boundary
        """
        if not self.boundary_configs:
            return [[5, 5, 0, 0]]  # Default to tidal

        # Find max boundary without using default parameter
        if self.boundary_configs:
            # Convert keys to list and find max
            boundary_keys = list(self.boundary_configs.keys())
            max_boundary = max(boundary_keys) if boundary_keys else -1
        else:
            max_boundary = -1

        flags = []

        for i in range(int(max_boundary) + 1):
            if i in self.boundary_configs:
                config = self.boundary_configs[i]
                flags.append(
                    [
                        int(config.elev_type),
                        int(config.vel_type),
                        int(config.temp_type),
                        int(config.salt_type),
                    ]
                )
            else:
                flags.append([0, 0, 0, 0])

        return flags

    def get_constant_values(self) -> Dict[str, List[float]]:
        """Get constant values for boundaries.

        Returns
        -------
        dict
            Dictionary of constant values for each boundary type
        """
        result = {
            "ethconst": [],
            "vthconst": [],
            "tthconst": [],
            "sthconst": [],
            "tobc": [],
            "sobc": [],
            "inflow_relax": [],
            "outflow_relax": [],
            "eta_mean": [],
            "vn_mean": [],
            "temp_th_path": [],
            "temp_3d_path": [],
            "salt_th_path": [],
            "salt_3d_path": [],
            "flow_th_path": [],
            "elev_st_path": [],
            "vel_st_path": [],
        }

        if not self.boundary_configs:
            return result

        # Find max boundary without using default parameter
        if self.boundary_configs:
            # Convert keys to list and find max
            boundary_keys = list(self.boundary_configs.keys())
            max_boundary = max(boundary_keys) if boundary_keys else -1
        else:
            max_boundary = -1

        for i in range(int(max_boundary) + 1):
            if i in self.boundary_configs:
                config = self.boundary_configs[i]

                # Handle type 2 (constant) boundaries
                if config.elev_type == ElevationType.CONSTANT:
                    result["ethconst"].append(
                        config.ethconst if config.ethconst is not None else 0.0
                    )
                else:
                    result["ethconst"].append(0.0)

                if config.vel_type == VelocityType.CONSTANT:
                    result["vthconst"].append(
                        config.vthconst if config.vthconst is not None else 0.0
                    )
                else:
                    result["vthconst"].append(0.0)

                if config.temp_type == TracerType.CONSTANT:
                    result["tthconst"].append(
                        config.tthconst if config.tthconst is not None else 0.0
                    )
                else:
                    result["tthconst"].append(0.0)

                if config.salt_type == TracerType.CONSTANT:
                    result["sthconst"].append(
                        config.sthconst if config.sthconst is not None else 0.0
                    )
                else:
                    result["sthconst"].append(0.0)

                # Nudging factors for temperature and salinity
                result["tobc"].append(config.tobc if config.tobc is not None else 1.0)
                result["sobc"].append(config.sobc if config.sobc is not None else 1.0)

                # Temperature and salinity file paths
                result["temp_th_path"].append(config.temp_th_path)
                result["temp_3d_path"].append(config.temp_3d_path)
                result["salt_th_path"].append(config.salt_th_path)
                result["salt_3d_path"].append(config.salt_3d_path)

                # Flow time history path
                result["flow_th_path"].append(config.flow_th_path)

                # Space-time file paths
                result["elev_st_path"].append(config.elev_st_path)
                result["vel_st_path"].append(config.vel_st_path)

                # Relaxation factors for velocity
                if config.vel_type == VelocityType.RELAXED:
                    result["inflow_relax"].append(
                        config.inflow_relax if config.inflow_relax is not None else 0.5
                    )
                    result["outflow_relax"].append(
                        config.outflow_relax
                        if config.outflow_relax is not None
                        else 0.1
                    )
                else:
                    result["inflow_relax"].append(0.5)  # Default values
                    result["outflow_relax"].append(0.1)

                # Handle Flather boundaries
                if config.vel_type == VelocityType.FLATHER:
                    # Create default values if none provided
                    if config.eta_mean is None:
                        # For testing, create a simple array of zeros with size = num nodes on this boundary
                        # In practice, this should be filled with actual mean elevation values
                        num_nodes = (
                            self.grid.nobn[i]
                            if hasattr(self.grid, "nobn") and i < len(self.grid.nobn)
                            else 1
                        )
                        eta_mean = [0.0] * num_nodes
                    else:
                        eta_mean = config.eta_mean

                    if config.vn_mean is None:
                        # For testing, create a simple array of arrays with zeros
                        num_nodes = (
                            self.grid.nobn[i]
                            if hasattr(self.grid, "nobn") and i < len(self.grid.nobn)
                            else 1
                        )
                        # Assume 5 vertical levels for testing
                        vn_mean = [[0.0] * 5 for _ in range(num_nodes)]
                    else:
                        vn_mean = config.vn_mean

                    result["eta_mean"].append(eta_mean)
                    result["vn_mean"].append(vn_mean)
                else:
                    result["eta_mean"].append(None)
                    result["vn_mean"].append(None)
            else:
                # Default values for missing boundaries
                result["ethconst"].append(0.0)
                result["vthconst"].append(0.0)
                result["tthconst"].append(0.0)
                result["sthconst"].append(0.0)
                result["tobc"].append(1.0)
                result["sobc"].append(1.0)
                result["inflow_relax"].append(0.5)
                result["outflow_relax"].append(0.1)
                result["eta_mean"].append(None)
                result["vn_mean"].append(None)
                result["temp_th_path"].append(None)
                result["temp_3d_path"].append(None)
                result["salt_th_path"].append(None)
                result["salt_3d_path"].append(None)
                result["flow_th_path"].append(None)
                result["elev_st_path"].append(None)
                result["vel_st_path"].append(None)

        return result

    def create_bctides(self) -> Bctides:
        """Create a Bctides instance from this configuration.

        Returns
        -------
        Bctides
            Configured Bctides instance
        """
        flags = self.get_flags_list()
        constants = self.get_constant_values()

        # Clean up lists to avoid None values
        ethconst = constants["ethconst"] if constants["ethconst"] else None
        vthconst = constants["vthconst"] if constants["vthconst"] else None
        tthconst = constants["tthconst"] if constants["tthconst"] else None
        sthconst = constants["sthconst"] if constants["sthconst"] else None
        tobc = constants["tobc"] if constants["tobc"] else None
        sobc = constants["sobc"] if constants["sobc"] else None
        inflow_relax = constants["inflow_relax"] if constants["inflow_relax"] else None
        outflow_relax = (
            constants["outflow_relax"] if constants["outflow_relax"] else None
        )

        # Add flow and flux boundary information
        ncbn = 0
        nfluxf = 0

        # Count the number of flow and flux boundaries
        for i, config in self.boundary_configs.items():
            # Count flow boundaries - both CONSTANT type with non-zero flow value
            # and type 1 (time history) are considered flow boundaries
            if (
                config.vel_type == VelocityType.CONSTANT and config.vthconst is not None
            ) or (config.vel_type == VelocityType.TIMEHIST):
                ncbn += 1

            # Count flux boundaries - type 3 with flux specified
            if config.vel_type == VelocityType.HARMONIC:
                nfluxf += 1

        # Extract file paths
        temp_th_path = (
            constants.get("temp_th_path", [None])[0]
            if constants.get("temp_th_path")
            else None
        )
        temp_3d_path = (
            constants.get("temp_3d_path", [None])[0]
            if constants.get("temp_3d_path")
            else None
        )
        salt_th_path = (
            constants.get("salt_th_path", [None])[0]
            if constants.get("salt_th_path")
            else None
        )
        salt_3d_path = (
            constants.get("salt_3d_path", [None])[0]
            if constants.get("salt_3d_path")
            else None
        )
        flow_th_path = (
            constants.get("flow_th_path", [None])[0]
            if constants.get("flow_th_path")
            else None
        )
        elev_st_path = (
            constants.get("elev_st_path", [None])[0]
            if constants.get("elev_st_path")
            else None
        )
        vel_st_path = (
            constants.get("vel_st_path", [None])[0]
            if constants.get("vel_st_path")
            else None
        )

        # Extract Flather boundary data if available
        eta_mean = (
            constants.get("eta_mean", [None]) if constants.get("eta_mean") else None
        )
        vn_mean = constants.get("vn_mean", [None]) if constants.get("vn_mean") else None

        # Ensure grid boundaries are computed before creating Bctides
        if self.grid is not None:
            if hasattr(self.grid, "compute_bnd") and not hasattr(self.grid, "nob"):
                logger.info("Computing grid boundaries for Bctides")
                self.grid.compute_bnd()
            elif not hasattr(self.grid, "nob") and hasattr(self.grid, "compute_all"):
                logger.info(
                    "Running compute_all to ensure grid boundaries are available"
                )
                self.grid.compute_all()

            # Verify boundaries were computed
            if not hasattr(self.grid, "nob"):
                logger.error(
                    "Failed to compute grid boundaries - grid has no 'nob' attribute"
                )
                raise AttributeError("Grid boundaries could not be computed")

        # Create Bctides object with all the enhanced parameters
        bctides = Bctides(
            hgrid=self.grid,
            flags=flags,
            constituents=self.tidal_data.constituents,
            tidal_database=self.tidal_data.tidal_database,
            tidal_model=self.tidal_data.tidal_model,
            tidal_potential=self.tidal_data.tidal_potential,
            cutoff_depth=self.tidal_data.cutoff_depth,
            nodal_corrections=self.tidal_data.nodal_corrections,
            tide_interpolation_method=self.tidal_data.tide_interpolation_method,
            extrapolate_tides=self.tidal_data.extrapolate_tides,
            extrapolation_distance=self.tidal_data.extrapolation_distance,
            extra_databases=self.tidal_data.extra_databases,
            mdt=getattr(
                self.tidal_data, "_mdt", self.tidal_data.mean_dynamic_topography
            ),
            ethconst=ethconst,
            vthconst=vthconst,
            tthconst=tthconst,
            sthconst=sthconst,
            tobc=tobc,
            sobc=sobc,
            relax=constants.get("inflow_relax", []),  # For backward compatibility
            inflow_relax=inflow_relax,
            outflow_relax=outflow_relax,
            ncbn=ncbn,
            nfluxf=nfluxf,
            elev_th_path=None,  # Time history of elevation is not handled by this path yet
            elev_st_path=elev_st_path,
            flow_th_path=flow_th_path,
            vel_st_path=vel_st_path,
            temp_th_path=temp_th_path,
            temp_3d_path=temp_3d_path,
            salt_th_path=salt_th_path,
            salt_3d_path=salt_3d_path,
        )

        # Set additional properties for Flather boundaries
        if eta_mean and any(x is not None for x in eta_mean):
            bctides.eta_mean = eta_mean
        if vn_mean and any(x is not None for x in vn_mean):
            bctides.vn_mean = vn_mean

        # Set start time and run duration
        if self._start_time and self._rnday is not None:
            bctides._start_time = self._start_time
            bctides._rnday = self._rnday

        return bctides

    def write_boundary_file(self, output_path: Union[str, Path]) -> Path:
        """Write the bctides.in file.

        Parameters
        ----------
        output_path : str or Path
            Path to write the file

        Returns
        -------
        Path
            Path to the written file

        Raises
        ------
        ValueError
            If start_time and rnday are not set
        """
        if not self._start_time or self._rnday is None:
            raise ValueError(
                "start_time and rnday must be set before writing boundary file"
            )

        # Create Bctides object
        bctides = self.create_bctides()

        # Write file
        output_path = Path(output_path)
        bctides.write_bctides(output_path)

        return output_path

Attributes

tidal_data instance-attribute

tidal_data = tidal_data

boundary_configs instance-attribute

boundary_configs = boundary_configs if boundary_configs is not None else {}

temp_th_path instance-attribute

temp_th_path = None

temp_3d_path instance-attribute

temp_3d_path = None

salt_th_path instance-attribute

salt_th_path = None

salt_3d_path instance-attribute

salt_3d_path = None

flow_th_path instance-attribute

flow_th_path = None

elev_st_path instance-attribute

elev_st_path = None

vel_st_path instance-attribute

vel_st_path = None

Functions

set_boundary_config

set_boundary_config(boundary_index: int, config: BoundaryConfig)

Set configuration for a specific boundary.

Parameters

boundary_index : int Index of the boundary config : BoundaryConfig Configuration for the boundary

Source code in rompy_schism/boundary_core.py
def set_boundary_config(self, boundary_index: int, config: BoundaryConfig):
    """Set configuration for a specific boundary.

    Parameters
    ----------
    boundary_index : int
        Index of the boundary
    config : BoundaryConfig
        Configuration for the boundary
    """
    self.boundary_configs[boundary_index] = config

set_boundary_type

set_boundary_type(boundary_index: int, elev_type: ElevationType, vel_type: VelocityType, temp_type: TracerType = NONE, salt_type: TracerType = NONE, **kwargs)

Set boundary types for a specific boundary.

Parameters

boundary_index : int Index of the boundary elev_type : ElevationType Elevation boundary condition type vel_type : VelocityType Velocity boundary condition type temp_type : TracerType, optional Temperature boundary condition type salt_type : TracerType, optional Salinity boundary condition type **kwargs Additional parameters for the boundary configuration

Source code in rompy_schism/boundary_core.py
def set_boundary_type(
    self,
    boundary_index: int,
    elev_type: ElevationType,
    vel_type: VelocityType,
    temp_type: TracerType = TracerType.NONE,
    salt_type: TracerType = TracerType.NONE,
    **kwargs,
):
    """Set boundary types for a specific boundary.

    Parameters
    ----------
    boundary_index : int
        Index of the boundary
    elev_type : ElevationType
        Elevation boundary condition type
    vel_type : VelocityType
        Velocity boundary condition type
    temp_type : TracerType, optional
        Temperature boundary condition type
    salt_type : TracerType, optional
        Salinity boundary condition type
    **kwargs
        Additional parameters for the boundary configuration
    """
    config = BoundaryConfig(
        elev_type=elev_type,
        vel_type=vel_type,
        temp_type=temp_type,
        salt_type=salt_type,
        **kwargs,
    )
    self.set_boundary_config(boundary_index, config)

set_run_parameters

set_run_parameters(start_time, run_days)

Set start time and run duration.

Parameters

start_time : datetime or list Start time for the simulation run_days : float Duration of the simulation in days

Source code in rompy_schism/boundary_core.py
def set_run_parameters(self, start_time, run_days):
    """Set start time and run duration.

    Parameters
    ----------
    start_time : datetime or list
        Start time for the simulation
    run_days : float
        Duration of the simulation in days
    """
    self._start_time = start_time
    self._rnday = run_days

get_flags_list

get_flags_list() -> List[List[int]]

Get list of boundary flags for Bctides.

Returns

list of list of int Boundary flags for each boundary

Source code in rompy_schism/boundary_core.py
def get_flags_list(self) -> List[List[int]]:
    """Get list of boundary flags for Bctides.

    Returns
    -------
    list of list of int
        Boundary flags for each boundary
    """
    if not self.boundary_configs:
        return [[5, 5, 0, 0]]  # Default to tidal

    # Find max boundary without using default parameter
    if self.boundary_configs:
        # Convert keys to list and find max
        boundary_keys = list(self.boundary_configs.keys())
        max_boundary = max(boundary_keys) if boundary_keys else -1
    else:
        max_boundary = -1

    flags = []

    for i in range(int(max_boundary) + 1):
        if i in self.boundary_configs:
            config = self.boundary_configs[i]
            flags.append(
                [
                    int(config.elev_type),
                    int(config.vel_type),
                    int(config.temp_type),
                    int(config.salt_type),
                ]
            )
        else:
            flags.append([0, 0, 0, 0])

    return flags

get_constant_values

get_constant_values() -> Dict[str, List[float]]

Get constant values for boundaries.

Returns

dict Dictionary of constant values for each boundary type

Source code in rompy_schism/boundary_core.py
def get_constant_values(self) -> Dict[str, List[float]]:
    """Get constant values for boundaries.

    Returns
    -------
    dict
        Dictionary of constant values for each boundary type
    """
    result = {
        "ethconst": [],
        "vthconst": [],
        "tthconst": [],
        "sthconst": [],
        "tobc": [],
        "sobc": [],
        "inflow_relax": [],
        "outflow_relax": [],
        "eta_mean": [],
        "vn_mean": [],
        "temp_th_path": [],
        "temp_3d_path": [],
        "salt_th_path": [],
        "salt_3d_path": [],
        "flow_th_path": [],
        "elev_st_path": [],
        "vel_st_path": [],
    }

    if not self.boundary_configs:
        return result

    # Find max boundary without using default parameter
    if self.boundary_configs:
        # Convert keys to list and find max
        boundary_keys = list(self.boundary_configs.keys())
        max_boundary = max(boundary_keys) if boundary_keys else -1
    else:
        max_boundary = -1

    for i in range(int(max_boundary) + 1):
        if i in self.boundary_configs:
            config = self.boundary_configs[i]

            # Handle type 2 (constant) boundaries
            if config.elev_type == ElevationType.CONSTANT:
                result["ethconst"].append(
                    config.ethconst if config.ethconst is not None else 0.0
                )
            else:
                result["ethconst"].append(0.0)

            if config.vel_type == VelocityType.CONSTANT:
                result["vthconst"].append(
                    config.vthconst if config.vthconst is not None else 0.0
                )
            else:
                result["vthconst"].append(0.0)

            if config.temp_type == TracerType.CONSTANT:
                result["tthconst"].append(
                    config.tthconst if config.tthconst is not None else 0.0
                )
            else:
                result["tthconst"].append(0.0)

            if config.salt_type == TracerType.CONSTANT:
                result["sthconst"].append(
                    config.sthconst if config.sthconst is not None else 0.0
                )
            else:
                result["sthconst"].append(0.0)

            # Nudging factors for temperature and salinity
            result["tobc"].append(config.tobc if config.tobc is not None else 1.0)
            result["sobc"].append(config.sobc if config.sobc is not None else 1.0)

            # Temperature and salinity file paths
            result["temp_th_path"].append(config.temp_th_path)
            result["temp_3d_path"].append(config.temp_3d_path)
            result["salt_th_path"].append(config.salt_th_path)
            result["salt_3d_path"].append(config.salt_3d_path)

            # Flow time history path
            result["flow_th_path"].append(config.flow_th_path)

            # Space-time file paths
            result["elev_st_path"].append(config.elev_st_path)
            result["vel_st_path"].append(config.vel_st_path)

            # Relaxation factors for velocity
            if config.vel_type == VelocityType.RELAXED:
                result["inflow_relax"].append(
                    config.inflow_relax if config.inflow_relax is not None else 0.5
                )
                result["outflow_relax"].append(
                    config.outflow_relax
                    if config.outflow_relax is not None
                    else 0.1
                )
            else:
                result["inflow_relax"].append(0.5)  # Default values
                result["outflow_relax"].append(0.1)

            # Handle Flather boundaries
            if config.vel_type == VelocityType.FLATHER:
                # Create default values if none provided
                if config.eta_mean is None:
                    # For testing, create a simple array of zeros with size = num nodes on this boundary
                    # In practice, this should be filled with actual mean elevation values
                    num_nodes = (
                        self.grid.nobn[i]
                        if hasattr(self.grid, "nobn") and i < len(self.grid.nobn)
                        else 1
                    )
                    eta_mean = [0.0] * num_nodes
                else:
                    eta_mean = config.eta_mean

                if config.vn_mean is None:
                    # For testing, create a simple array of arrays with zeros
                    num_nodes = (
                        self.grid.nobn[i]
                        if hasattr(self.grid, "nobn") and i < len(self.grid.nobn)
                        else 1
                    )
                    # Assume 5 vertical levels for testing
                    vn_mean = [[0.0] * 5 for _ in range(num_nodes)]
                else:
                    vn_mean = config.vn_mean

                result["eta_mean"].append(eta_mean)
                result["vn_mean"].append(vn_mean)
            else:
                result["eta_mean"].append(None)
                result["vn_mean"].append(None)
        else:
            # Default values for missing boundaries
            result["ethconst"].append(0.0)
            result["vthconst"].append(0.0)
            result["tthconst"].append(0.0)
            result["sthconst"].append(0.0)
            result["tobc"].append(1.0)
            result["sobc"].append(1.0)
            result["inflow_relax"].append(0.5)
            result["outflow_relax"].append(0.1)
            result["eta_mean"].append(None)
            result["vn_mean"].append(None)
            result["temp_th_path"].append(None)
            result["temp_3d_path"].append(None)
            result["salt_th_path"].append(None)
            result["salt_3d_path"].append(None)
            result["flow_th_path"].append(None)
            result["elev_st_path"].append(None)
            result["vel_st_path"].append(None)

    return result

create_bctides

create_bctides() -> Bctides

Create a Bctides instance from this configuration.

Returns

Bctides Configured Bctides instance

Source code in rompy_schism/boundary_core.py
def create_bctides(self) -> Bctides:
    """Create a Bctides instance from this configuration.

    Returns
    -------
    Bctides
        Configured Bctides instance
    """
    flags = self.get_flags_list()
    constants = self.get_constant_values()

    # Clean up lists to avoid None values
    ethconst = constants["ethconst"] if constants["ethconst"] else None
    vthconst = constants["vthconst"] if constants["vthconst"] else None
    tthconst = constants["tthconst"] if constants["tthconst"] else None
    sthconst = constants["sthconst"] if constants["sthconst"] else None
    tobc = constants["tobc"] if constants["tobc"] else None
    sobc = constants["sobc"] if constants["sobc"] else None
    inflow_relax = constants["inflow_relax"] if constants["inflow_relax"] else None
    outflow_relax = (
        constants["outflow_relax"] if constants["outflow_relax"] else None
    )

    # Add flow and flux boundary information
    ncbn = 0
    nfluxf = 0

    # Count the number of flow and flux boundaries
    for i, config in self.boundary_configs.items():
        # Count flow boundaries - both CONSTANT type with non-zero flow value
        # and type 1 (time history) are considered flow boundaries
        if (
            config.vel_type == VelocityType.CONSTANT and config.vthconst is not None
        ) or (config.vel_type == VelocityType.TIMEHIST):
            ncbn += 1

        # Count flux boundaries - type 3 with flux specified
        if config.vel_type == VelocityType.HARMONIC:
            nfluxf += 1

    # Extract file paths
    temp_th_path = (
        constants.get("temp_th_path", [None])[0]
        if constants.get("temp_th_path")
        else None
    )
    temp_3d_path = (
        constants.get("temp_3d_path", [None])[0]
        if constants.get("temp_3d_path")
        else None
    )
    salt_th_path = (
        constants.get("salt_th_path", [None])[0]
        if constants.get("salt_th_path")
        else None
    )
    salt_3d_path = (
        constants.get("salt_3d_path", [None])[0]
        if constants.get("salt_3d_path")
        else None
    )
    flow_th_path = (
        constants.get("flow_th_path", [None])[0]
        if constants.get("flow_th_path")
        else None
    )
    elev_st_path = (
        constants.get("elev_st_path", [None])[0]
        if constants.get("elev_st_path")
        else None
    )
    vel_st_path = (
        constants.get("vel_st_path", [None])[0]
        if constants.get("vel_st_path")
        else None
    )

    # Extract Flather boundary data if available
    eta_mean = (
        constants.get("eta_mean", [None]) if constants.get("eta_mean") else None
    )
    vn_mean = constants.get("vn_mean", [None]) if constants.get("vn_mean") else None

    # Ensure grid boundaries are computed before creating Bctides
    if self.grid is not None:
        if hasattr(self.grid, "compute_bnd") and not hasattr(self.grid, "nob"):
            logger.info("Computing grid boundaries for Bctides")
            self.grid.compute_bnd()
        elif not hasattr(self.grid, "nob") and hasattr(self.grid, "compute_all"):
            logger.info(
                "Running compute_all to ensure grid boundaries are available"
            )
            self.grid.compute_all()

        # Verify boundaries were computed
        if not hasattr(self.grid, "nob"):
            logger.error(
                "Failed to compute grid boundaries - grid has no 'nob' attribute"
            )
            raise AttributeError("Grid boundaries could not be computed")

    # Create Bctides object with all the enhanced parameters
    bctides = Bctides(
        hgrid=self.grid,
        flags=flags,
        constituents=self.tidal_data.constituents,
        tidal_database=self.tidal_data.tidal_database,
        tidal_model=self.tidal_data.tidal_model,
        tidal_potential=self.tidal_data.tidal_potential,
        cutoff_depth=self.tidal_data.cutoff_depth,
        nodal_corrections=self.tidal_data.nodal_corrections,
        tide_interpolation_method=self.tidal_data.tide_interpolation_method,
        extrapolate_tides=self.tidal_data.extrapolate_tides,
        extrapolation_distance=self.tidal_data.extrapolation_distance,
        extra_databases=self.tidal_data.extra_databases,
        mdt=getattr(
            self.tidal_data, "_mdt", self.tidal_data.mean_dynamic_topography
        ),
        ethconst=ethconst,
        vthconst=vthconst,
        tthconst=tthconst,
        sthconst=sthconst,
        tobc=tobc,
        sobc=sobc,
        relax=constants.get("inflow_relax", []),  # For backward compatibility
        inflow_relax=inflow_relax,
        outflow_relax=outflow_relax,
        ncbn=ncbn,
        nfluxf=nfluxf,
        elev_th_path=None,  # Time history of elevation is not handled by this path yet
        elev_st_path=elev_st_path,
        flow_th_path=flow_th_path,
        vel_st_path=vel_st_path,
        temp_th_path=temp_th_path,
        temp_3d_path=temp_3d_path,
        salt_th_path=salt_th_path,
        salt_3d_path=salt_3d_path,
    )

    # Set additional properties for Flather boundaries
    if eta_mean and any(x is not None for x in eta_mean):
        bctides.eta_mean = eta_mean
    if vn_mean and any(x is not None for x in vn_mean):
        bctides.vn_mean = vn_mean

    # Set start time and run duration
    if self._start_time and self._rnday is not None:
        bctides._start_time = self._start_time
        bctides._rnday = self._rnday

    return bctides

write_boundary_file

write_boundary_file(output_path: Union[str, Path]) -> Path

Write the bctides.in file.

Parameters

output_path : str or Path Path to write the file

Returns

Path Path to the written file

Raises

ValueError If start_time and rnday are not set

Source code in rompy_schism/boundary_core.py
def write_boundary_file(self, output_path: Union[str, Path]) -> Path:
    """Write the bctides.in file.

    Parameters
    ----------
    output_path : str or Path
        Path to write the file

    Returns
    -------
    Path
        Path to the written file

    Raises
    ------
    ValueError
        If start_time and rnday are not set
    """
    if not self._start_time or self._rnday is None:
        raise ValueError(
            "start_time and rnday must be set before writing boundary file"
        )

    # Create Bctides object
    bctides = self.create_bctides()

    # Write file
    output_path = Path(output_path)
    bctides.write_bctides(output_path)

    return output_path

BoundaryConfig

Configuration for individual boundary segments.

BoundaryConfig

Bases: BaseModel

Configuration for a single SCHISM boundary segment.

Source code in rompy_schism/boundary_core.py
class BoundaryConfig(BaseModel):
    """Configuration for a single SCHISM boundary segment."""

    # Required fields with default values
    elev_type: ElevationType = Field(
        default=ElevationType.NONE, description="Elevation boundary condition type"
    )
    vel_type: VelocityType = Field(
        default=VelocityType.NONE, description="Velocity boundary condition type"
    )
    temp_type: TracerType = Field(
        default=TracerType.NONE, description="Temperature boundary condition type"
    )
    salt_type: TracerType = Field(
        default=TracerType.NONE, description="Salinity boundary condition type"
    )

    # Optional fields for specific boundary types
    # Elevation constants (for ElevationType.CONSTANT)
    ethconst: Optional[float] = Field(
        default=None, description="Constant elevation value (for CONSTANT type)"
    )

    # Velocity/flow constants (for VelocityType.CONSTANT)
    vthconst: Optional[float] = Field(
        default=None, description="Constant velocity/flow value (for CONSTANT type)"
    )

    # Temperature constants and parameters
    tthconst: Optional[float] = Field(
        default=None, description="Constant temperature value (for CONSTANT type)"
    )
    tobc: Optional[float] = Field(
        default=1.0,
        description="Temperature nudging factor (0-1, 1 is strongest nudging)",
    )
    temp_th_path: Optional[str] = Field(
        default=None, description="Path to temperature time history file (for type 1)"
    )
    temp_3d_path: Optional[str] = Field(
        default=None, description="Path to 3D temperature file (for type 4)"
    )

    # Salinity constants and parameters
    sthconst: Optional[float] = Field(
        default=None, description="Constant salinity value (for CONSTANT type)"
    )
    sobc: Optional[float] = Field(
        default=1.0, description="Salinity nudging factor (0-1, 1 is strongest nudging)"
    )
    salt_th_path: Optional[str] = Field(
        default=None, description="Path to salinity time history file (for type 1)"
    )
    salt_3d_path: Optional[str] = Field(
        default=None, description="Path to 3D salinity file (for type 4)"
    )

    # Velocity/flow time history parameters (for VelocityType.TIMEHIST)
    flow_th_path: Optional[str] = Field(
        default=None, description="Path to flow time history file (for type 1)"
    )

    # Relaxation parameters for velocity (for VelocityType.RELAXED)
    inflow_relax: Optional[float] = Field(
        default=0.5,
        description="Relaxation factor for inflow (0-1, 1 is strongest nudging)",
    )
    outflow_relax: Optional[float] = Field(
        default=0.1,
        description="Relaxation factor for outflow (0-1, 1 is strongest nudging)",
    )

    # Flather boundary values (for VelocityType.FLATHER)
    eta_mean: Optional[List[float]] = Field(
        default=None, description="Mean elevation profile for Flather boundary"
    )
    vn_mean: Optional[List[List[float]]] = Field(
        default=None, description="Mean velocity profile for Flather boundary"
    )

    # Space-time parameters
    elev_st_path: Optional[str] = Field(
        default=None,
        description="Path to space-time elevation file (for SPACETIME type)",
    )
    vel_st_path: Optional[str] = Field(
        default=None,
        description="Path to space-time velocity file (for SPACETIME type)",
    )

    model_config = ConfigDict(arbitrary_types_allowed=True)

    def __str__(self):
        """String representation of the boundary configuration."""
        return (
            f"BoundaryConfig(elev_type={self.elev_type}, vel_type={self.vel_type}, "
            f"temp_type={self.temp_type}, salt_type={self.salt_type})"
        )

Attributes

elev_type class-attribute instance-attribute

elev_type: ElevationType = Field(default=NONE, description='Elevation boundary condition type')

vel_type class-attribute instance-attribute

vel_type: VelocityType = Field(default=NONE, description='Velocity boundary condition type')

temp_type class-attribute instance-attribute

temp_type: TracerType = Field(default=NONE, description='Temperature boundary condition type')

salt_type class-attribute instance-attribute

salt_type: TracerType = Field(default=NONE, description='Salinity boundary condition type')

ethconst class-attribute instance-attribute

ethconst: Optional[float] = Field(default=None, description='Constant elevation value (for CONSTANT type)')

vthconst class-attribute instance-attribute

vthconst: Optional[float] = Field(default=None, description='Constant velocity/flow value (for CONSTANT type)')

tthconst class-attribute instance-attribute

tthconst: Optional[float] = Field(default=None, description='Constant temperature value (for CONSTANT type)')

tobc class-attribute instance-attribute

tobc: Optional[float] = Field(default=1.0, description='Temperature nudging factor (0-1, 1 is strongest nudging)')

temp_th_path class-attribute instance-attribute

temp_th_path: Optional[str] = Field(default=None, description='Path to temperature time history file (for type 1)')

temp_3d_path class-attribute instance-attribute

temp_3d_path: Optional[str] = Field(default=None, description='Path to 3D temperature file (for type 4)')

sthconst class-attribute instance-attribute

sthconst: Optional[float] = Field(default=None, description='Constant salinity value (for CONSTANT type)')

sobc class-attribute instance-attribute

sobc: Optional[float] = Field(default=1.0, description='Salinity nudging factor (0-1, 1 is strongest nudging)')

salt_th_path class-attribute instance-attribute

salt_th_path: Optional[str] = Field(default=None, description='Path to salinity time history file (for type 1)')

salt_3d_path class-attribute instance-attribute

salt_3d_path: Optional[str] = Field(default=None, description='Path to 3D salinity file (for type 4)')

flow_th_path class-attribute instance-attribute

flow_th_path: Optional[str] = Field(default=None, description='Path to flow time history file (for type 1)')

inflow_relax class-attribute instance-attribute

inflow_relax: Optional[float] = Field(default=0.5, description='Relaxation factor for inflow (0-1, 1 is strongest nudging)')

outflow_relax class-attribute instance-attribute

outflow_relax: Optional[float] = Field(default=0.1, description='Relaxation factor for outflow (0-1, 1 is strongest nudging)')

eta_mean class-attribute instance-attribute

eta_mean: Optional[List[float]] = Field(default=None, description='Mean elevation profile for Flather boundary')

vn_mean class-attribute instance-attribute

vn_mean: Optional[List[List[float]]] = Field(default=None, description='Mean velocity profile for Flather boundary')

elev_st_path class-attribute instance-attribute

elev_st_path: Optional[str] = Field(default=None, description='Path to space-time elevation file (for SPACETIME type)')

vel_st_path class-attribute instance-attribute

vel_st_path: Optional[str] = Field(default=None, description='Path to space-time velocity file (for SPACETIME type)')

model_config class-attribute instance-attribute

model_config = ConfigDict(arbitrary_types_allowed=True)

Boundary Type Enums

ElevationType

ElevationType

Bases: IntEnum

Elevation boundary condition types.

Source code in rompy_schism/boundary_core.py
class ElevationType(IntEnum):
    """Elevation boundary condition types."""

    NONE = 0  # Not specified
    TIMEHIST = 1  # Time history from elev.th
    CONSTANT = 2  # Constant elevation
    HARMONIC = 3  # Harmonic tidal constituents
    EXTERNAL = 4  # External model data from elev2D.th.nc
    HARMONICEXTERNAL = 5  # Combination of harmonic and external data

Attributes

NONE class-attribute instance-attribute

NONE = 0

TIMEHIST class-attribute instance-attribute

TIMEHIST = 1

CONSTANT class-attribute instance-attribute

CONSTANT = 2

HARMONIC class-attribute instance-attribute

HARMONIC = 3

EXTERNAL class-attribute instance-attribute

EXTERNAL = 4

HARMONICEXTERNAL class-attribute instance-attribute

HARMONICEXTERNAL = 5

VelocityType

VelocityType

Bases: IntEnum

Velocity boundary condition types.

Source code in rompy_schism/boundary_core.py
class VelocityType(IntEnum):
    """Velocity boundary condition types."""

    NONE = 0  # Not specified
    TIMEHIST = 1  # Time history from flux.th
    CONSTANT = 2  # Constant discharge
    HARMONIC = 3  # Harmonic tidal constituents
    EXTERNAL = 4  # External model data from uv3D.th.nc
    HARMONICEXTERNAL = 5  # Combination of harmonic and external data
    FLATHER = -1  # Flather type radiation boundary
    RELAXED = -4  # 3D input with relaxation

Attributes

NONE class-attribute instance-attribute

NONE = 0

TIMEHIST class-attribute instance-attribute

TIMEHIST = 1

CONSTANT class-attribute instance-attribute

CONSTANT = 2

HARMONIC class-attribute instance-attribute

HARMONIC = 3

EXTERNAL class-attribute instance-attribute

EXTERNAL = 4

HARMONICEXTERNAL class-attribute instance-attribute

HARMONICEXTERNAL = 5

FLATHER class-attribute instance-attribute

FLATHER = -1

RELAXED class-attribute instance-attribute

RELAXED = -4

TracerType

TracerType

Bases: IntEnum

Temperature/salinity boundary condition types.

Source code in rompy_schism/boundary_core.py
class TracerType(IntEnum):
    """Temperature/salinity boundary condition types."""

    NONE = 0  # Not specified
    TIMEHIST = 1  # Time history from temp/salt.th
    CONSTANT = 2  # Constant temperature/salinity
    INITIAL = 3  # Initial profile for inflow
    EXTERNAL = 4  # External model 3D input

Attributes

NONE class-attribute instance-attribute

NONE = 0

TIMEHIST class-attribute instance-attribute

TIMEHIST = 1

CONSTANT class-attribute instance-attribute

CONSTANT = 2

INITIAL class-attribute instance-attribute

INITIAL = 3

EXTERNAL class-attribute instance-attribute

EXTERNAL = 4

Factory Functions

The boundary conditions module provides convenient factory functions for creating common boundary configurations. These functions return SCHISMDataBoundaryConditions objects that can be directly used in SCHISM simulations.

High-Level Configuration Functions

create_tidal_only_boundary_config

create_tidal_only_boundary_config

create_tidal_only_boundary_config(constituents: Union[str, List[str]] = 'major', tidal_database: Union[str, Path] = None, tidal_model: Optional[str] = 'FES2014', nodal_corrections: bool = True, tidal_potential: bool = True, cutoff_depth: float = 50.0, tide_interpolation_method: str = 'bilinear')

Create a configuration where all open boundaries are treated as tidal boundaries.

Parameters

constituents : str or list, optional Tidal constituents to include, by default "major" tidal_database : str or Path, optional Path to tidal database for pyTMD, by default None tidal_model : str, optional Tidal model to use, by default 'FES2014' nodal_corrections : bool, optional Whether to apply nodal corrections, by default True tidal_potential : bool, optional Whether to include tidal potential, by default True cutoff_depth : float, optional Depth threshold for tidal potential, by default 50.0 tide_interpolation_method : str, optional Method for tide interpolation, by default "bilinear"

Returns

SCHISMDataBoundaryConditions Configured boundary conditions

Source code in rompy_schism/boundary_core.py
def create_tidal_only_boundary_config(
    constituents: Union[str, List[str]] = "major",
    tidal_database: Union[str, Path] = None,
    tidal_model: Optional[str] = "FES2014",
    nodal_corrections: bool = True,
    tidal_potential: bool = True,
    cutoff_depth: float = 50.0,
    tide_interpolation_method: str = "bilinear",
):
    """
    Create a configuration where all open boundaries are treated as tidal boundaries.

    Parameters
    ----------
    constituents : str or list, optional
        Tidal constituents to include, by default "major"
    tidal_database : str or Path, optional
        Path to tidal database for pyTMD, by default None
    tidal_model : str, optional
        Tidal model to use, by default 'FES2014'
    nodal_corrections : bool, optional
        Whether to apply nodal corrections, by default True
    tidal_potential : bool, optional
        Whether to include tidal potential, by default True
    cutoff_depth : float, optional
        Depth threshold for tidal potential, by default 50.0
    tide_interpolation_method : str, optional
        Method for tide interpolation, by default "bilinear"

    Returns
    -------
    SCHISMDataBoundaryConditions
        Configured boundary conditions
    """
    from rompy_schism.data import SCHISMDataBoundaryConditions

    # Create tidal dataset
    tidal_data = TidalDataset(
        constituents=constituents,
        tidal_database=tidal_database,
        tidal_model=tidal_model,
        nodal_corrections=nodal_corrections,
        tidal_potential=tidal_potential,
        cutoff_depth=cutoff_depth,
        tide_interpolation_method=tide_interpolation_method,
    )

    # Create the config with tidal setup
    config = SCHISMDataBoundaryConditions(
        tidal_data=tidal_data,
        setup_type="tidal",
        boundaries={},
        hotstart_config=None,
    )

    return config

Example Usage:

from rompy_schism.boundary_conditions import create_tidal_only_boundary_config

# Basic tidal configuration
bc = create_tidal_only_boundary_config(
    constituents=["M2", "S2", "N2", "K1", "O1"],
    tidal_elevations="/path/to/h_tpxo9.nc",
    tidal_velocities="/path/to/u_tpxo9.nc"
)

# With earth tidal potential
bc = create_tidal_only_boundary_config(
    constituents=["M2", "S2", "K1", "O1"],
    ntip=1  # Enable earth tidal potential
)

create_hybrid_boundary_config

create_hybrid_boundary_config

create_hybrid_boundary_config(constituents: Union[str, List[str]] = 'major', tidal_database: Union[str, Path] = None, tidal_model: Optional[str] = 'FES2014', nodal_corrections: bool = True, tidal_potential: bool = True, cutoff_depth: float = 50.0, tide_interpolation_method: str = 'bilinear', elev_source: Optional[Union[Any, Any]] = None, vel_source: Optional[Union[Any, Any]] = None, temp_source: Optional[Union[Any, Any]] = None, salt_source: Optional[Union[Any, Any]] = None)

Create a configuration for hybrid harmonic + external data boundaries.

Parameters

constituents : str or list, optional Tidal constituents to include, by default "major" tidal_database : str or Path, optional Path to tidal database for pyTMD, by default None tidal_model : str, optional Tidal model to use, by default 'FES2014' nodal_corrections : bool, optional Whether to apply nodal corrections, by default True tidal_potential : bool, optional Whether to include tidal potential, by default True cutoff_depth : float, optional Depth threshold for tidal potential, by default 50.0 tide_interpolation_method : str, optional Method for tide interpolation, by default "bilinear" elev_source : Union[DataBlob, SCHISMDataBoundary], optional Data source for elevation vel_source : Union[DataBlob, SCHISMDataBoundary], optional Data source for velocity temp_source : Union[DataBlob, SCHISMDataBoundary], optional Data source for temperature salt_source : Union[DataBlob, SCHISMDataBoundary], optional Data source for salinity

Returns

SCHISMDataBoundaryConditions Configured boundary conditions

Source code in rompy_schism/boundary_core.py
def create_hybrid_boundary_config(
    constituents: Union[str, List[str]] = "major",
    tidal_database: Union[str, Path] = None,
    tidal_model: Optional[str] = "FES2014",
    nodal_corrections: bool = True,
    tidal_potential: bool = True,
    cutoff_depth: float = 50.0,
    tide_interpolation_method: str = "bilinear",
    elev_source: Optional[Union[Any, Any]] = None,
    vel_source: Optional[Union[Any, Any]] = None,
    temp_source: Optional[Union[Any, Any]] = None,
    salt_source: Optional[Union[Any, Any]] = None,
):
    """
    Create a configuration for hybrid harmonic + external data boundaries.

    Parameters
    ----------
    constituents : str or list, optional
        Tidal constituents to include, by default "major"
    tidal_database : str or Path, optional
        Path to tidal database for pyTMD, by default None
    tidal_model : str, optional
        Tidal model to use, by default 'FES2014'
    nodal_corrections : bool, optional
        Whether to apply nodal corrections, by default True
    tidal_potential : bool, optional
        Whether to include tidal potential, by default True
    cutoff_depth : float, optional
        Depth threshold for tidal potential, by default 50.0
    tide_interpolation_method : str, optional
        Method for tide interpolation, by default "bilinear"
    elev_source : Union[DataBlob, SCHISMDataBoundary], optional
        Data source for elevation
    vel_source : Union[DataBlob, SCHISMDataBoundary], optional
        Data source for velocity
    temp_source : Union[DataBlob, SCHISMDataBoundary], optional
        Data source for temperature
    salt_source : Union[DataBlob, SCHISMDataBoundary], optional
        Data source for salinity

    Returns
    -------
    SCHISMDataBoundaryConditions
        Configured boundary conditions
    """
    from rompy_schism.data import BoundarySetupWithSource, SCHISMDataBoundaryConditions
    from rompy_schism.tides_enhanced import TidalDataset

    # Create tidal dataset
    tidal_data = TidalDataset(
        constituents=constituents,
        tidal_database=tidal_database,
        tidal_model=tidal_model,
        nodal_corrections=nodal_corrections,
        tidal_potential=tidal_potential,
        cutoff_depth=cutoff_depth,
        tide_interpolation_method=tide_interpolation_method,
    )

    # Create the config with hybrid setup
    config = SCHISMDataBoundaryConditions(
        tidal_data=tidal_data,
        setup_type="hybrid",
        boundaries={
            0: BoundarySetupWithSource(
                elev_type=ElevationType.HARMONICEXTERNAL,
                vel_type=VelocityType.HARMONICEXTERNAL
                if vel_source
                else VelocityType.NONE,
                temp_type=TracerType.EXTERNAL if temp_source else TracerType.INITIAL,
                salt_type=TracerType.EXTERNAL if salt_source else TracerType.INITIAL,
                elev_source=elev_source,
                vel_source=vel_source,
                temp_source=temp_source,
                salt_source=salt_source,
            )
        },
        hotstart_config=None,
    )

    return config

Example Usage:

from rompy_schism.boundary_conditions import create_hybrid_boundary_config
from rompy.core.data import DataBlob

# Hybrid configuration with external data
bc = create_hybrid_boundary_config(
    constituents=["M2", "S2"],
    tidal_elevations="/path/to/h_tpxo9.nc",
    tidal_velocities="/path/to/u_tpxo9.nc",
    elev_source=DataBlob(source="/path/to/elev2D.th.nc"),
    vel_source=DataBlob(source="/path/to/uv3D.th.nc"),
    temp_source=DataBlob(source="/path/to/TEM_3D.th.nc"),
    salt_source=DataBlob(source="/path/to/SAL_3D.th.nc")
)

create_river_boundary_config

create_river_boundary_config

create_river_boundary_config(river_boundary_index: int = 0, river_flow: float = -100.0, other_boundaries: Literal['tidal', 'hybrid', 'none'] = 'tidal', constituents: Union[str, List[str]] = 'major', tidal_database: Union[str, Path] = None, tidal_model: Optional[str] = 'FES2014', nodal_corrections: bool = True, tidal_potential: bool = True, cutoff_depth: float = 50.0, tide_interpolation_method: str = 'bilinear')

Create a configuration with a designated river boundary and optional tidal boundaries.

Parameters

river_boundary_index : int Index of the river boundary river_flow : float Flow rate (negative for inflow) other_boundaries : str How to treat other boundaries ("tidal", "hybrid", or "none") constituents : str or list, optional Tidal constituents to include, by default "major" tidal_database : str or Path, optional Path to tidal database for pyTMD, by default None tidal_model : str, optional Tidal model to use, by default 'FES2014' nodal_corrections : bool, optional Whether to apply nodal corrections, by default True tidal_potential : bool, optional Whether to include tidal potential, by default True cutoff_depth : float, optional Depth threshold for tidal potential, by default 50.0 tide_interpolation_method : str, optional Method for tide interpolation, by default "bilinear"

Returns

SCHISMDataBoundaryConditions Configured boundary conditions

Source code in rompy_schism/boundary_core.py
def create_river_boundary_config(
    river_boundary_index: int = 0,
    river_flow: float = -100.0,  # Negative for inflow
    other_boundaries: Literal["tidal", "hybrid", "none"] = "tidal",
    constituents: Union[str, List[str]] = "major",
    tidal_database: Union[str, Path] = None,
    tidal_model: Optional[str] = "FES2014",
    nodal_corrections: bool = True,
    tidal_potential: bool = True,
    cutoff_depth: float = 50.0,
    tide_interpolation_method: str = "bilinear",
):
    """
    Create a configuration with a designated river boundary and optional tidal boundaries.

    Parameters
    ----------
    river_boundary_index : int
        Index of the river boundary
    river_flow : float
        Flow rate (negative for inflow)
    other_boundaries : str
        How to treat other boundaries ("tidal", "hybrid", or "none")
    constituents : str or list, optional
        Tidal constituents to include, by default "major"
    tidal_database : str or Path, optional
        Path to tidal database for pyTMD, by default None
    tidal_model : str, optional
        Tidal model to use, by default 'FES2014'
    nodal_corrections : bool, optional
        Whether to apply nodal corrections, by default True
    tidal_potential : bool, optional
        Whether to include tidal potential, by default True
    cutoff_depth : float, optional
        Depth threshold for tidal potential, by default 50.0
    tide_interpolation_method : str, optional
        Method for tide interpolation, by default "bilinear"

    Returns
    -------
    SCHISMDataBoundaryConditions
        Configured boundary conditions
    """
    from rompy_schism.data import BoundarySetupWithSource, SCHISMDataBoundaryConditions
    from rompy_schism.tides_enhanced import TidalDataset

    # Create tidal dataset if both paths are provided and needed
    tidal_data = None
    if other_boundaries in ["tidal", "hybrid"]:
        tidal_data = TidalDataset(
            constituents=constituents,
            tidal_database=tidal_database,
            tidal_model=tidal_model,
            nodal_corrections=nodal_corrections,
            tidal_potential=tidal_potential,
            cutoff_depth=cutoff_depth,
            tide_interpolation_method=tide_interpolation_method,
        )

    # Create the basic config
    config = SCHISMDataBoundaryConditions(
        tidal_data=tidal_data,
        setup_type="river",
        hotstart_config=None,
    )

    # Add the river boundary
    config.boundaries[river_boundary_index] = BoundarySetupWithSource(
        elev_type=ElevationType.NONE,
        vel_type=VelocityType.CONSTANT,
        temp_type=TracerType.NONE,
        salt_type=TracerType.NONE,
        const_flow=river_flow,
    )

    return config

Example Usage:

from rompy_schism.boundary_conditions import create_river_boundary_config

# River boundary with tidal forcing on other boundaries
bc = create_river_boundary_config(
    river_boundary_index=1,
    river_flow=-500.0,  # 500 m³/s inflow
    river_temp=15.0,    # 15°C
    river_salt=0.1,     # 0.1 PSU (fresh water)
    other_boundaries="tidal",
    constituents=["M2", "S2", "N2"]
)

# River-only configuration
bc = create_river_boundary_config(
    river_boundary_index=0,
    river_flow=-200.0,
    other_boundaries="none"
)

create_nested_boundary_config

create_nested_boundary_config

create_nested_boundary_config(with_tides: bool = True, inflow_relax: float = 0.8, outflow_relax: float = 0.2, elev_source: Optional[Union[Any, Any]] = None, vel_source: Optional[Union[Any, Any]] = None, temp_source: Optional[Union[Any, Any]] = None, salt_source: Optional[Union[Any, Any]] = None, constituents: Union[str, List[str]] = 'major', tidal_database: Union[str, Path] = None, tidal_model: Optional[str] = 'FES2014', nodal_corrections: bool = True, tidal_potential: bool = True, cutoff_depth: float = 50.0, tide_interpolation_method: str = 'bilinear')

Create a configuration for nested model boundaries with external data.

Parameters

with_tides : bool Include tidal components inflow_relax : float Relaxation parameter for inflow (0-1) outflow_relax : float Relaxation parameter for outflow (0-1) elev_source : Union[DataBlob, SCHISMDataBoundary], optional Data source for elevation vel_source : Union[DataBlob, SCHISMDataBoundary], optional Data source for velocity temp_source : Union[DataBlob, SCHISMDataBoundary], optional Data source for temperature salt_source : Union[DataBlob, SCHISMDataBoundary], optional Data source for salinity constituents : str or list, optional Tidal constituents to include, by default "major" tidal_database : str or Path, optional Path to tidal database for pyTMD, by default None tidal_model : str, optional Tidal model to use, by default 'FES2014' nodal_corrections : bool, optional Whether to apply nodal corrections, by default True tidal_potential : bool, optional Whether to include tidal potential, by default True cutoff_depth : float, optional Depth threshold for tidal potential, by default 50.0 tide_interpolation_method : str, optional Method for tide interpolation, by default "bilinear"

Returns

SCHISMDataBoundaryConditions Configured boundary conditions

Source code in rompy_schism/boundary_core.py
def create_nested_boundary_config(
    with_tides: bool = True,
    inflow_relax: float = 0.8,
    outflow_relax: float = 0.2,
    elev_source: Optional[Union[Any, Any]] = None,
    vel_source: Optional[Union[Any, Any]] = None,
    temp_source: Optional[Union[Any, Any]] = None,
    salt_source: Optional[Union[Any, Any]] = None,
    constituents: Union[str, List[str]] = "major",
    tidal_database: Union[str, Path] = None,
    tidal_model: Optional[str] = "FES2014",
    nodal_corrections: bool = True,
    tidal_potential: bool = True,
    cutoff_depth: float = 50.0,
    tide_interpolation_method: str = "bilinear",
):
    """
    Create a configuration for nested model boundaries with external data.

    Parameters
    ----------
    with_tides : bool
        Include tidal components
    inflow_relax : float
        Relaxation parameter for inflow (0-1)
    outflow_relax : float
        Relaxation parameter for outflow (0-1)
    elev_source : Union[DataBlob, SCHISMDataBoundary], optional
        Data source for elevation
    vel_source : Union[DataBlob, SCHISMDataBoundary], optional
        Data source for velocity
    temp_source : Union[DataBlob, SCHISMDataBoundary], optional
        Data source for temperature
    salt_source : Union[DataBlob, SCHISMDataBoundary], optional
        Data source for salinity
    constituents : str or list, optional
        Tidal constituents to include, by default "major"
    tidal_database : str or Path, optional
        Path to tidal database for pyTMD, by default None
    tidal_model : str, optional
        Tidal model to use, by default 'FES2014'
    nodal_corrections : bool, optional
        Whether to apply nodal corrections, by default True
    tidal_potential : bool, optional
        Whether to include tidal potential, by default True
    cutoff_depth : float, optional
        Depth threshold for tidal potential, by default 50.0
    tide_interpolation_method : str, optional
        Method for tide interpolation, by default "bilinear"

    Returns
    -------
    SCHISMDataBoundaryConditions
        Configured boundary conditions
    """
    from rompy_schism.data import BoundarySetupWithSource, SCHISMDataBoundaryConditions
    from rompy_schism.tides_enhanced import TidalDataset

    # Create tidal dataset if both paths are provided and needed
    tidal_data = None
    if with_tides:
        tidal_data = TidalDataset(
            constituents=constituents,
            tidal_database=tidal_database,
            tidal_model=tidal_model,
            nodal_corrections=nodal_corrections,
            tidal_potential=tidal_potential,
            cutoff_depth=cutoff_depth,
            tide_interpolation_method=tide_interpolation_method,
        )

    # Create the basic config
    config = SCHISMDataBoundaryConditions(
        tidal_data=tidal_data,
        setup_type="nested",
        hotstart_config=None,
    )

    # Determine elevation type based on tides setting
    elev_type = ElevationType.HARMONICEXTERNAL if with_tides else ElevationType.EXTERNAL

    # Add the nested boundary configuration
    config.boundaries[0] = BoundarySetupWithSource(
        elev_type=elev_type,
        vel_type=VelocityType.RELAXED,
        temp_type=TracerType.EXTERNAL if temp_source else TracerType.NONE,
        salt_type=TracerType.EXTERNAL if salt_source else TracerType.NONE,
        inflow_relax=inflow_relax,
        outflow_relax=outflow_relax,
        elev_source=elev_source,
        vel_source=vel_source,
        temp_source=temp_source,
        salt_source=salt_source,
    )

    return config

Example Usage:

from rompy_schism.boundary_conditions import create_nested_boundary_config
from rompy_schism.data import SCHISMDataBoundary
from rompy.core.source import SourceFile

# Nested boundary with tides and parent model data
bc = create_nested_boundary_config(
    with_tides=True,
    inflow_relax=0.9,
    outflow_relax=0.1,
    constituents=["M2", "S2"],
    elev_source=SCHISMDataBoundary(
        source=SourceFile(uri="/path/to/parent_model.nc"),
        variables=["ssh"]
    ),
    vel_source=SCHISMDataBoundary(
        source=SourceFile(uri="/path/to/parent_model.nc"),
        variables=["u", "v"]
    )
)

# Nested boundary without tides
bc = create_nested_boundary_config(
    with_tides=False,
    inflow_relax=0.8,
    outflow_relax=0.2,
    elev_source=elev_data,
    vel_source=vel_data
)

Low-Level Boundary Creation Functions

These functions create BoundaryHandler objects for direct grid-based boundary manipulation:

create_tidal_boundary

create_tidal_boundary(grid_path: Union[str, Path], constituents: Union[str, List[str]] = 'major', tidal_database: Union[str, Path] = None, tidal_model: Optional[str] = 'FES2014', nodal_corrections: bool = True, tidal_potential: bool = True, cutoff_depth: float = 50.0, tide_interpolation_method: str = 'bilinear') -> BoundaryHandler

Create a tidal-only boundary.

Parameters

grid_path : str or Path Path to SCHISM grid constituents : str or list, optional Tidal constituents, by default "major" tidal_database : str or Path, optional Tidal database path for pyTMD to use, by default None tidal_model : str, optional Tidal model to use, by default 'FES2014' nodal_corrections : bool, optional Whether to apply nodal corrections, by default True tidal_potential : bool, optional Whether to include tidal potential, by default True cutoff_depth : float, optional Depth threshold for tidal potential, by default 50.0

Returns

BoundaryHandler Configured tidal boundary

Source code in rompy_schism/boundary_core.py
def create_tidal_boundary(
    grid_path: Union[str, Path],
    constituents: Union[str, List[str]] = "major",
    tidal_database: Union[str, Path] = None,
    tidal_model: Optional[str] = "FES2014",
    nodal_corrections: bool = True,
    tidal_potential: bool = True,
    cutoff_depth: float = 50.0,
    tide_interpolation_method: str = "bilinear",
) -> BoundaryHandler:
    """Create a tidal-only boundary.

    Parameters
    ----------
    grid_path : str or Path
        Path to SCHISM grid
    constituents : str or list, optional
        Tidal constituents, by default "major"
    tidal_database : str or Path, optional
        Tidal database path for pyTMD to use, by default None
    tidal_model : str, optional
        Tidal model to use, by default 'FES2014'
    nodal_corrections : bool, optional
        Whether to apply nodal corrections, by default True
    tidal_potential : bool, optional
        Whether to include tidal potential, by default True
    cutoff_depth : float, optional
        Depth threshold for tidal potential, by default 50.0

    Returns
    -------
    BoundaryHandler
        Configured tidal boundary
    """

    tidal_data = TidalDataset(
        constituents=constituents,
        tidal_database=tidal_database,
        tidal_model=tidal_model,
        nodal_corrections=nodal_corrections,
        tidal_potential=tidal_potential,
        cutoff_depth=cutoff_depth,
        tide_interpolation_method=tide_interpolation_method,
    )

    boundary = BoundaryHandler(
        grid_path=grid_path,
        tidal_data=tidal_data,
    )

    # Set default configuration for all boundaries: pure tidal
    boundary.set_boundary_type(
        0,  # Will be applied to all boundaries
        elev_type=ElevationType.HARMONIC,
        vel_type=VelocityType.HARMONIC,
    )

    return boundary

create_hybrid_boundary

create_hybrid_boundary(grid_path: Union[str, Path], constituents: Union[str, List[str]] = 'major', tidal_database: Union[str, Path] = None, tidal_model: Optional[str] = 'FES2014', nodal_corrections: bool = True, tidal_potential: bool = True, cutoff_depth: float = 50.0, tide_interpolation_method: str = 'bilinear') -> BoundaryHandler

Create a hybrid boundary with tides + external data.

Parameters

grid_path : str or Path Path to SCHISM grid constituents : str or list, optional Tidal constituents to include, by default "major" tidal_database : str or Path, optional Path to tidal database for pyTMD, by default None tidal_model : str, optional Tidal model to use, by default 'FES2014' nodal_corrections : bool, optional Whether to apply nodal corrections, by default True tidal_potential : bool, optional Whether to include tidal potential, by default True cutoff_depth : float, optional Depth threshold for tidal potential, by default 50.0 tide_interpolation_method : str, optional Method for tide interpolation, by default "bilinear"

Returns

BoundaryHandler Configured hybrid boundary

Source code in rompy_schism/boundary_core.py
def create_hybrid_boundary(
    grid_path: Union[str, Path],
    constituents: Union[str, List[str]] = "major",
    tidal_database: Union[str, Path] = None,
    tidal_model: Optional[str] = "FES2014",
    nodal_corrections: bool = True,
    tidal_potential: bool = True,
    cutoff_depth: float = 50.0,
    tide_interpolation_method: str = "bilinear",
) -> BoundaryHandler:
    """Create a hybrid boundary with tides + external data.

    Parameters
    ----------
    grid_path : str or Path
        Path to SCHISM grid
    constituents : str or list, optional
        Tidal constituents to include, by default "major"
    tidal_database : str or Path, optional
        Path to tidal database for pyTMD, by default None
    tidal_model : str, optional
        Tidal model to use, by default 'FES2014'
    nodal_corrections : bool, optional
        Whether to apply nodal corrections, by default True
    tidal_potential : bool, optional
        Whether to include tidal potential, by default True
    cutoff_depth : float, optional
        Depth threshold for tidal potential, by default 50.0
    tide_interpolation_method : str, optional
        Method for tide interpolation, by default "bilinear"

    Returns
    -------
    BoundaryHandler
        Configured hybrid boundary
    """

    tidal_data = TidalDataset(
        constituents=constituents,
        tidal_database=tidal_database,
        tidal_model=tidal_model,
        nodal_corrections=nodal_corrections,
        tidal_potential=tidal_potential,
        cutoff_depth=cutoff_depth,
        tide_interpolation_method=tide_interpolation_method,
    )

    boundary = BoundaryHandler(grid_path=grid_path, tidal_data=tidal_data)

    # Set default configuration for all boundaries: tidal + spacetime
    boundary.set_boundary_type(
        0,  # Will be applied to all boundaries
        elev_type=ElevationType.HARMONICEXTERNAL,
        vel_type=VelocityType.HARMONICEXTERNAL,
    )

    return boundary

create_river_boundary

create_river_boundary(grid_path: Union[str, Path], river_flow: float = -100.0, river_boundary_index: int = 0) -> BoundaryHandler

Create a river boundary with constant flow.

Parameters

grid_path : str or Path Path to SCHISM grid river_flow : float, optional River flow value (negative for inflow), by default -100.0 river_boundary_index : int, optional Index of the river boundary, by default 0

Returns

BoundaryHandler Configured river boundary

Source code in rompy_schism/boundary_core.py
def create_river_boundary(
    grid_path: Union[str, Path],
    river_flow: float = -100.0,  # Negative for inflow
    river_boundary_index: int = 0,
) -> BoundaryHandler:
    """Create a river boundary with constant flow.

    Parameters
    ----------
    grid_path : str or Path
        Path to SCHISM grid
    river_flow : float, optional
        River flow value (negative for inflow), by default -100.0
    river_boundary_index : int, optional
        Index of the river boundary, by default 0

    Returns
    -------
    BoundaryHandler
        Configured river boundary
    """
    boundary = BoundaryHandler(grid_path=grid_path)

    # Set river boundary
    boundary.set_boundary_type(
        river_boundary_index,
        elev_type=ElevationType.NONE,  # No elevation specified
        vel_type=VelocityType.CONSTANT,  # Constant flow
        vthconst=river_flow,  # Flow value
    )

    return boundary

create_nested_boundary

create_nested_boundary(grid_path: Union[str, Path], with_tides: bool = False, inflow_relax: float = 0.8, outflow_relax: float = 0.8, constituents: Union[str, List[str]] = 'major', tidal_database: Union[str, Path] = None, tidal_model: Optional[str] = 'FES2014', nodal_corrections: bool = True, tidal_potential: bool = True, cutoff_depth: float = 50.0, tide_interpolation_method: str = 'bilinear') -> BoundaryHandler

Create a nested boundary with optional tides.

Parameters

grid_path : str or Path Path to SCHISM grid with_tides : bool, optional Whether to include tides, by default False inflow_relax : float, optional Relaxation factor for inflow, by default 0.8 outflow_relax : float, optional Relaxation factor for outflow, by default 0.8 constituents : str or list, optional Tidal constituents to include, by default "major" tidal_database : str or Path, optional Path to tidal database for pyTMD, by default None tidal_model : str, optional Tidal model to use, by default 'FES2014' nodal_corrections : bool, optional Whether to apply nodal corrections, by default True tidal_potential : bool, optional Whether to include tidal potential, by default True cutoff_depth : float, optional Depth threshold for tidal potential, by default 50.0 tide_interpolation_method : str, optional Method for tide interpolation, by default "bilinear"

Returns

BoundaryHandler Configured nested boundary

Source code in rompy_schism/boundary_core.py
def create_nested_boundary(
    grid_path: Union[str, Path],
    with_tides: bool = False,
    inflow_relax: float = 0.8,
    outflow_relax: float = 0.8,
    constituents: Union[str, List[str]] = "major",
    tidal_database: Union[str, Path] = None,
    tidal_model: Optional[str] = "FES2014",
    nodal_corrections: bool = True,
    tidal_potential: bool = True,
    cutoff_depth: float = 50.0,
    tide_interpolation_method: str = "bilinear",
) -> BoundaryHandler:
    """Create a nested boundary with optional tides.

    Parameters
    ----------
    grid_path : str or Path
        Path to SCHISM grid
    with_tides : bool, optional
        Whether to include tides, by default False
    inflow_relax : float, optional
        Relaxation factor for inflow, by default 0.8
    outflow_relax : float, optional
        Relaxation factor for outflow, by default 0.8
    constituents : str or list, optional
        Tidal constituents to include, by default "major"
    tidal_database : str or Path, optional
        Path to tidal database for pyTMD, by default None
    tidal_model : str, optional
        Tidal model to use, by default 'FES2014'
    nodal_corrections : bool, optional
        Whether to apply nodal corrections, by default True
    tidal_potential : bool, optional
        Whether to include tidal potential, by default True
    cutoff_depth : float, optional
        Depth threshold for tidal potential, by default 50.0
    tide_interpolation_method : str, optional
        Method for tide interpolation, by default "bilinear"

    Returns
    -------
    BoundaryHandler
        Configured nested boundary
    """

    tidal_data = None
    if with_tides:
        tidal_data = TidalDataset(
            constituents=constituents,
            tidal_database=tidal_database,
            tidal_model=tidal_model,
            nodal_corrections=nodal_corrections,
            tidal_potential=tidal_potential,
            cutoff_depth=cutoff_depth,
            tide_interpolation_method=tide_interpolation_method,
        )

    boundary = BoundaryHandler(
        grid_path=grid_path,
        constituents=constituents if with_tides else None,
        tidal_data=tidal_data,
    )

    if with_tides:
        # Tides + external data with relaxation
        boundary.set_boundary_type(
            0,  # Will be applied to all boundaries
            elev_type=ElevationType.HARMONICEXTERNAL,
            vel_type=VelocityType.RELAXED,
            temp_type=TracerType.EXTERNAL,
            salt_type=TracerType.EXTERNAL,
            inflow_relax=inflow_relax,
            outflow_relax=outflow_relax,
        )
    else:
        # Just external data with relaxation
        boundary.set_boundary_type(
            0,  # Will be applied to all boundaries
            elev_type=ElevationType.EXTERNAL,
            vel_type=VelocityType.RELAXED,
            temp_type=TracerType.EXTERNAL,
            salt_type=TracerType.EXTERNAL,
            inflow_relax=inflow_relax,
            outflow_relax=outflow_relax,
        )

    return boundary

Usage Examples

Tidal-Only Configuration

For simulations with purely tidal forcing:

from rompy_schism.boundary_conditions import create_tidal_only_boundary_config
from rompy_schism.data import SCHISMData

# Create tidal-only boundary configuration
boundary_conditions = create_tidal_only_boundary_config(
    constituents=["M2", "S2", "N2", "K1", "O1"],
    tidal_database="tpxo",
    tidal_elevations="path/to/tidal_elevations.nc",
    tidal_velocities="path/to/tidal_velocities.nc",
)

# Use in SCHISM configuration
schism_data = SCHISMData(
    boundary_conditions=boundary_conditions,
)

Hybrid Tidal + Ocean Data

For simulations combining tidal forcing with external ocean data:

from rompy_schism.boundary_conditions import create_hybrid_boundary_config
from rompy.core.data import DataBlob

# Create hybrid boundary configuration
boundary_conditions = create_hybrid_boundary_config(
    constituents=["M2", "S2"],
    tidal_elevations="path/to/tidal_elevations.nc",
    tidal_velocities="path/to/tidal_velocities.nc",
    # Add ocean data sources
    elev_source=DataBlob(source="path/to/elev2D.th.nc"),
    vel_source=DataBlob(source="path/to/uv3D.th.nc"),
    temp_source=DataBlob(source="path/to/TEM_3D.th.nc"),
    salt_source=DataBlob(source="path/to/SAL_3D.th.nc"),
)

River Boundary Configuration

For simulations with river inputs:

from rompy_schism.boundary_conditions import create_river_boundary_config

# Create river boundary configuration
boundary_conditions = create_river_boundary_config(
    river_boundary_index=1,     # Index of the river boundary
    river_flow=-100.0,          # Negative for inflow (m³/s)
    other_boundaries="tidal",   # Other boundaries are tidal
    constituents=["M2", "S2"],
    tidal_elevations="path/to/tidal_elevations.nc",
    tidal_velocities="path/to/tidal_velocities.nc",
)

Nested Model Configuration

For simulations nested within a larger model:

from rompy_schism.boundary_conditions import create_nested_boundary_config
from rompy_schism.data import SCHISMDataBoundary
from rompy.core.source import SourceFile

# Create nested boundary configuration
boundary_conditions = create_nested_boundary_config(
    with_tides=True,
    inflow_relax=0.8,
    outflow_relax=0.2,
    constituents=["M2", "S2"],
    tidal_elevations="path/to/tidal_elevations.nc",
    tidal_velocities="path/to/tidal_velocities.nc",
    # Add parent model data sources
    elev_source=SCHISMDataBoundary(
        source=SourceFile(uri="path/to/parent_model.nc"),
        variables=["ssh"],
    ),
    vel_source=SCHISMDataBoundary(
        source=SourceFile(uri="path/to/parent_model.nc"),
        variables=["u", "v"],
    ),
)

Direct Boundary Handler Usage

For maximum control, use the BoundaryHandler class directly:

from rompy_schism.boundary_core import (
    BoundaryHandler, 
    ElevationType, 
    VelocityType, 
    TracerType
)

# Create boundary handler
boundary = BoundaryHandler(
    grid_path="path/to/hgrid.gr3",
    constituents=["M2", "S2", "K1", "O1"],
    tidal_database="tpxo",
    tidal_elevations="path/to/h_tpxo9.nc",
    tidal_velocities="path/to/uv_tpxo9.nc"
)

# Configure different boundary types
boundary.set_boundary_type(
    0,  # Ocean boundary with tides
    elev_type=ElevationType.HARMONIC,
    vel_type=VelocityType.HARMONIC
)

boundary.set_boundary_type(
    1,  # River boundary
    elev_type=ElevationType.NONE,
    vel_type=VelocityType.CONSTANT,
    vthconst=-500.0  # River inflow
)

# Set simulation parameters and write output
boundary.set_run_parameters(start_time, run_days)
boundary.write_boundary_file("path/to/bctides.in")

Custom Boundary Configuration

For complex scenarios with mixed boundary types:

from rompy_schism.data import SCHISMDataBoundaryConditions, BoundarySetupWithSource
from rompy_schism.boundary_core import ElevationType, VelocityType, TracerType
from rompy.core.data import DataBlob

# Create custom boundary configuration
boundary_conditions = SCHISMDataBoundaryConditions(
    constituents=["M2", "S2"],
    tidal_database="tpxo",
    boundaries={
        # Ocean boundary (harmonic + external data)
        0: BoundarySetupWithSource(
            elev_type=ElevationType.HARMONICEXTERNAL,
            vel_type=VelocityType.HARMONICEXTERNAL,
            temp_type=TracerType.EXTERNAL,
            salt_type=TracerType.EXTERNAL,
            elev_source=DataBlob(source="path/to/elev2D.th.nc"),
            vel_source=DataBlob(source="path/to/uv3D.th.nc"),
            temp_source=DataBlob(source="path/to/TEM_3D.th.nc"),
            salt_source=DataBlob(source="path/to/SAL_3D.th.nc"),
        ),
        # River boundary (constant flow)
        1: BoundarySetupWithSource(
            elev_type=ElevationType.NONE,
            vel_type=VelocityType.CONSTANT,
            temp_type=TracerType.CONSTANT,
            salt_type=TracerType.CONSTANT,
            const_flow=-100.0,  # m³/s, negative for inflow
            const_temp=15.0,    # °C
            const_salt=0.5,     # PSU
        ),
    }
)

Boundary Types

The system supports various boundary condition types for different variables:

Elevation Types

  • NONE - No elevation boundary condition
  • TIMEHIST - Time history from elev.th
  • CONSTANT - Constant elevation
  • HARMONIC - Pure harmonic tidal elevation using tidal constituents
  • EXTERNAL - Time-varying elevation from external data (elev2D.th.nc)
  • HARMONICEXTERNAL - Combined harmonic and external elevation data

Velocity Types

  • NONE - No velocity boundary condition
  • TIMEHIST - Time history from flux.th
  • CONSTANT - Constant velocity/flow rate
  • HARMONIC - Pure harmonic tidal velocity using tidal constituents
  • EXTERNAL - Time-varying velocity from external data (uv3D.th.nc)
  • HARMONICEXTERNAL - Combined harmonic and external velocity data
  • FLATHER - Flather type radiation boundary
  • RELAXED - Relaxation boundary condition (for nesting)

Tracer Types

  • NONE - No tracer boundary condition
  • TIMEHIST - Time history from temp/salt.th
  • CONSTANT - Constant tracer value
  • INITIAL - Initial profile for inflow
  • EXTERNAL - Time-varying tracer from external data

Data Sources

The system supports multiple data source types:

DataBlob

Simple file-based data source for pre-processed SCHISM input files:

from rompy.core.data import DataBlob

elev_source = DataBlob(source="path/to/elev2D.th.nc")

SCHISMDataBoundary

Advanced data source with variable mapping and coordinate transformation:

from rompy_schism.data import SCHISMDataBoundary
from rompy.core.source import SourceFile

vel_source = SCHISMDataBoundary(
    source=SourceFile(uri="path/to/ocean_model.nc"),
    variables=["u", "v"],
    crop_coords={"lon": [-180, 180], "lat": [-90, 90]},
)

Configuration Files

The boundary conditions can also be configured via YAML files:

Tidal-Only Configuration:

boundary_conditions:
  data_type: boundary_conditions
  constituents: ["M2", "S2", "N2", "K1", "O1"]
  tidal_database: tpxo
  tidal_data:
    elevations: path/to/h_tpxo9.nc
    velocities: path/to/u_tpxo9.nc
  setup_type: tidal

Hybrid Configuration:

boundary_conditions:
  data_type: boundary_conditions
  constituents: ["M2", "S2", "N2", "K1", "O1"]
  tidal_database: tpxo
  tidal_data:
    elevations: path/to/h_tpxo9.nc
    velocities: path/to/u_tpxo9.nc
  setup_type: hybrid
  boundaries:
    0:
      elev_type: HARMONICEXTERNAL
      vel_type: HARMONICEXTERNAL
      temp_type: EXTERNAL
      salt_type: EXTERNAL
      elev_source:
        data_type: blob
        source: path/to/elev2D.th.nc
      vel_source:
        data_type: blob
        source: path/to/uv3D.th.nc
      temp_source:
        data_type: blob
        source: path/to/TEM_3D.th.nc
      salt_source:
        data_type: blob
        source: path/to/SAL_3D.th.nc

River Configuration:

boundary_conditions:
  data_type: boundary_conditions
  constituents: ["M2", "S2"]
  tidal_database: tpxo
  setup_type: river
  boundaries:
    0:  # Tidal boundary
      elev_type: HARMONIC
      vel_type: HARMONIC
      temp_type: NONE
      salt_type: NONE
    1:  # River boundary
      elev_type: NONE
      vel_type: CONSTANT
      temp_type: CONSTANT
      salt_type: CONSTANT
      const_flow: -500.0
      const_temp: 15.0
      const_salt: 0.1

Nested Configuration:

boundary_conditions:
  data_type: boundary_conditions
  constituents: ["M2", "S2"]
  tidal_database: tpxo
  tidal_data:
    elevations: path/to/h_tpxo9.nc
    velocities: path/to/u_tpxo9.nc
  setup_type: nested
  boundaries:
    0:
      elev_type: HARMONICEXTERNAL
      vel_type: RELAXED
      temp_type: EXTERNAL
      salt_type: EXTERNAL
      inflow_relax: 0.8
      outflow_relax: 0.2
      elev_source:
        data_type: schism_boundary
        source:
          data_type: source_file
          uri: path/to/parent_model.nc
        variables: ["ssh"]
      vel_source:
        data_type: schism_boundary
        source:
          data_type: source_file
          uri: path/to/parent_model.nc
        variables: ["u", "v"]

Benefits of the New System

  1. Unified Interface - Single configuration object for all boundary types
  2. Flexible Configuration - Mix different boundary types per segment
  3. Factory Functions - Simplified setup for common scenarios
  4. Better Validation - Comprehensive validation of boundary configurations
  5. Data Source Integration - Seamless integration with data processing pipeline
  6. Backward Compatibility - Maintains compatibility with existing workflows where possible
  7. Clear Naming - Module and class names reflect actual functionality
  8. Consolidated Code - Eliminates duplication between modules

Advanced Features

Factory Function Parameters

All factory functions support additional parameters for fine-tuning:

Common Parameters:

  • constituents: List of tidal constituents (e.g., ["M2", "S2", "N2", "K1", "O1"])
  • tidal_database: Database identifier ("tpxo", "fes2014", "got")
  • tidal_elevations: Path to tidal elevation NetCDF file
  • tidal_velocities: Path to tidal velocity NetCDF file

Tidal Potential:

bc = create_tidal_only_boundary_config(
    constituents=["M2", "S2", "K1", "O1"],
    ntip=1,          # Enable tidal potential
    tip_dp=1.0,      # Depth threshold
    cutoff_depth=50.0,  # Cutoff depth
)

Relaxation Parameters:

bc = create_nested_boundary_config(
    with_tides=True,
    inflow_relax=0.8,   # Strong relaxation for inflow
    outflow_relax=0.2,  # Weak relaxation for outflow
)

Multiple Tidal Databases:

bc = create_tidal_only_boundary_config(
    tidal_database="fes2014",  # Alternative: "tpxo", "got"
    constituents=["M2", "S2", "N2", "K2", "K1", "O1", "P1", "Q1"],
)

Custom Boundary Types:

from rompy_schism.data import BoundarySetupWithSource
from rompy_schism.boundary_core import ElevationType, VelocityType, TracerType

# Custom boundary with specific types
custom_boundary = BoundarySetupWithSource(
    elev_type=ElevationType.HARMONICEXTERNAL,
    vel_type=VelocityType.HARMONICEXTERNAL,
    temp_type=TracerType.EXTERNAL,
    salt_type=TracerType.EXTERNAL,
    inflow_relax=0.9,
    outflow_relax=0.1
)

Flather Radiation Boundaries

Configure Flather radiation boundaries using the low-level BoundaryHandler:

from rompy_schism.boundary_core import BoundaryHandler, ElevationType, VelocityType

# Create boundary handler
boundary = BoundaryHandler(grid_path="path/to/hgrid.gr3")

# Configure Flather boundary
boundary.set_boundary_type(
    boundary_index=1,
    elev_type=ElevationType.NONE,
    vel_type=VelocityType.FLATHER,
    eta_mean=[0.0, 0.0, 0.0],        # Mean elevation at each node
    vn_mean=[[0.1], [0.1], [0.1]]    # Mean normal velocity at each node
)

Common Tidal Constituents

Major Constituents (recommended for most applications):

  • M2, S2, N2, K1, O1

Semi-diurnal:

  • M2 (Principal lunar), S2 (Principal solar), N2 (Lunar elliptic), K2 (Lunisolar)

Diurnal:

  • K1 (Lunar diurnal), O1 (Lunar principal), P1 (Solar principal), Q1 (Larger lunar elliptic)

Long Period:

  • Mf (Lunisolar fortnightly), Mm (Lunar monthly), Ssa (Solar semiannual)

Full Set Example:

bc = create_tidal_only_boundary_config(
    constituents=[
        "M2", "S2", "N2", "K2",     # Semi-diurnal
        "K1", "O1", "P1", "Q1",     # Diurnal  
        "Mf", "Mm", "Ssa"           # Long period
    ]
)

Best Practices

  1. Start Simple: Begin with tidal-only configurations using major constituents
  2. Validate Data: Ensure tidal and external data files cover your model domain and time period
  3. Check Units: River flows are in m³/s (negative for inflow)
  4. Relaxation Values: Use 0.8-1.0 for strong nudging, 0.1-0.3 for weak nudging
  5. File Formats: Use NetCDF files for better performance and metadata
  6. Coordinate Systems: Ensure all data sources use consistent coordinate systems
  7. Time Coverage: External data must cover the entire simulation period plus spin-up

See Also

  • Core Data - Core data handling classes
  • Core Boundary - Base boundary condition classes
  • rompy_schism.data.SCHISMData - Main SCHISM configuration class
  • rompy_schism.grid.SCHISMGrid - SCHISM grid handling
  • Hotstart - Hotstart configuration documentation