Skip to content

Validation

validation

Validation module for PyFDS.

This module provides comprehensive validation for FDS simulations, including input sanitization, simulation-level validation, cross-reference checking, and execution configuration validation.

Examples:

>>> from pyfds.validation import validate_simulation
>>> result = validate_simulation(sim)
>>> if not result.is_valid:
...     for error in result.errors:
...         print(error)

Classes

BuiltinSpecies

Bases: StrEnum

Built-in FDS species that don't require explicit definition.

These species IDs are predefined by FDS and can be referenced without creating a corresponding SPEC namelist.

Functions
values classmethod
values()

Return all builtin species values as a frozenset.

Source code in src/pyfds/core/enums.py
@classmethod
def values(cls) -> frozenset[str]:
    """Return all builtin species values as a frozenset."""
    return frozenset(member.value for member in cls)

BuiltinSurface

Bases: StrEnum

Built-in FDS surfaces that don't require explicit definition.

These surface IDs are predefined by FDS and can be referenced without creating a corresponding SURF namelist.

Functions
values classmethod
values()

Return all builtin surface values as a frozenset.

Source code in src/pyfds/core/enums.py
@classmethod
def values(cls) -> frozenset[str]:
    """Return all builtin surface values as a frozenset."""
    return frozenset(member.value for member in cls)

Severity

Bases: StrEnum

Validation issue severity levels.

Issue dataclass

Issue(severity, message, namelist=None, field=None)

A validation issue.

ATTRIBUTE DESCRIPTION
severity

Issue severity level (ERROR, WARNING, INFO)

TYPE: Severity

message

Human-readable description of the issue

TYPE: str

namelist

FDS namelist name where issue was found

TYPE: (str, optional)

field

Field name where issue was found

TYPE: (str, optional)

ValidationResult dataclass

ValidationResult(issues)

Result of a validation operation.

ATTRIBUTE DESCRIPTION
issues

All validation issues found

TYPE: list[Issue]

Attributes
errors property
errors

Get only ERROR-level issues.

warnings property
warnings

Get only WARNING-level issues.

infos property
infos

Get only INFO-level issues.

is_valid property
is_valid

True if no ERROR-level issues exist.

Functions
__bool__
__bool__()

True if no issues at all.

Source code in src/pyfds/validation/base.py
def __bool__(self) -> bool:
    """True if no issues at all."""
    return len(self.issues) == 0
__len__
__len__()

Return number of issues.

Source code in src/pyfds/validation/base.py
def __len__(self) -> int:
    """Return number of issues."""
    return len(self.issues)
__iter__
__iter__()

Iterate over issues.

Source code in src/pyfds/validation/base.py
def __iter__(self) -> Iterator[Issue]:
    """Iterate over issues."""
    return iter(self.issues)

CrossReferenceValidator

CrossReferenceValidator(simulation)

Validates cross-references between FDS namelists.

Provides more detailed reference checking than the main SimulationValidator, including: - RAMP references from SURF and MATL - BURN_AWAY surface configurations - HT3D configurations

RETURNS DESCRIPTION
ValidationResult

Result containing all validation issues found.

Source code in src/pyfds/validation/cross_references.py
def __init__(self, simulation: "Simulation | SimulationRegistry") -> None:
    if hasattr(simulation, "_registry"):
        self._registry = simulation._registry
    else:
        self._registry = simulation
    self._ramp_ids: set[str] = set()
    self._surface_ids: set[str] = set()
    self._material_ids: set[str] = set()
    self._species_ids: set[str] = set()
Functions
validate
validate()

Run all cross-reference validations.

RETURNS DESCRIPTION
ValidationResult

Validation result containing all issues found.

Source code in src/pyfds/validation/cross_references.py
def validate(self) -> ValidationResult:
    """Run all cross-reference validations.

    Returns
    -------
    ValidationResult
        Validation result containing all issues found.
    """
    # Cache IDs for efficiency
    self._cache_ids()

    issues: list[Issue] = []
    issues.extend(self._check_obstruction_surface_refs())
    issues.extend(self._check_surface_material_refs())
    issues.extend(self._check_ramp_refs())
    issues.extend(self._check_material_species_refs())
    issues.extend(self._check_burn_away_config())
    issues.extend(self._check_ht3d_config())

    return ValidationResult(issues=issues)

ExecutionValidator

ExecutionValidator()

Validates parallel execution configuration.

Checks MPI process count, OpenMP thread count, and provides recommendations for optimal performance.

Based on FDS User Guide Chapter 3 "Running FDS" guidelines for optimal parallel performance.

Source code in src/pyfds/validation/execution.py
def __init__(self) -> None:
    self._cpu_count = os.cpu_count() or 1
Functions
validate
validate(simulation, n_mpi=1, n_threads=1)

Validate parallel configuration.

PARAMETER DESCRIPTION
simulation

Simulation to validate

TYPE: Simulation

n_mpi

Number of MPI processes

TYPE: int DEFAULT: 1

n_threads

Number of OpenMP threads per process

TYPE: int DEFAULT: 1

RETURNS DESCRIPTION
ValidationResult

Validation result containing any issues found.

Source code in src/pyfds/validation/execution.py
def validate(
    self,
    simulation: "Simulation",
    n_mpi: int = 1,
    n_threads: int = 1,
) -> ValidationResult:
    """Validate parallel configuration.

    Parameters
    ----------
    simulation : Simulation
        Simulation to validate
    n_mpi : int
        Number of MPI processes
    n_threads : int
        Number of OpenMP threads per process

    Returns
    -------
    ValidationResult
        Validation result containing any issues found.
    """
    issues: list[Issue] = []
    issues.extend(self._check_mpi_vs_meshes(simulation, n_mpi))
    issues.extend(self._check_thread_count(n_threads))
    mesh_count = len(simulation.meshes)
    if (n_mpi > 1 and n_mpi != mesh_count) or n_threads > 1:
        issues.extend(self._check_total_parallelism(n_mpi, n_threads))
    return ValidationResult(issues=issues)
suggest_config
suggest_config(simulation)

Suggest optimal parallel configuration.

PARAMETER DESCRIPTION
simulation

Simulation to analyze

TYPE: Simulation

RETURNS DESCRIPTION
dict

Suggested n_mpi and n_threads values with rationale

Source code in src/pyfds/validation/execution.py
def suggest_config(self, simulation: "Simulation") -> dict[str, int | str]:
    """Suggest optimal parallel configuration.

    Parameters
    ----------
    simulation : Simulation
        Simulation to analyze

    Returns
    -------
    dict
        Suggested n_mpi and n_threads values with rationale
    """
    mesh_count = len(simulation.meshes)

    if mesh_count == 0:
        return {
            "n_mpi": 1,
            "n_threads": 1,
            "rationale": "No meshes defined - cannot recommend configuration",
        }

    if mesh_count == 1:
        # Single mesh: use OpenMP only
        recommended_threads = max(1, self._cpu_count // 2)
        return {
            "n_mpi": 1,
            "n_threads": recommended_threads,
            "rationale": (
                f"Single mesh simulation - using OpenMP with ~50% of {self._cpu_count} cores."
            ),
        }

    # Multi-mesh: prefer MPI
    n_mpi = min(mesh_count, self._cpu_count)

    return {
        "n_mpi": n_mpi,
        "n_threads": 1,
        "rationale": (
            f"Multi-mesh ({mesh_count} meshes) - using MPI with "
            f"{n_mpi} processes for best performance."
        ),
    }

SimulationValidator

SimulationValidator(simulation)

Validates complete simulation configurations.

Validation is performed in order: 1. Required components (HEAD, TIME, MESH) 2. Cross-reference validation (SURF_ID, MATL_ID, etc.) 3. Geometry quality checks 4. Physical reasonableness checks

Source code in src/pyfds/validation/simulation.py
def __init__(self, simulation: "Simulation | SimulationRegistry") -> None:
    if hasattr(simulation, "_registry"):
        self._registry = simulation._registry
    else:
        self._registry = simulation
Functions
validate
validate()

Run all validations.

RETURNS DESCRIPTION
ValidationResult

Result containing all validation issues

Source code in src/pyfds/validation/simulation.py
def validate(self) -> ValidationResult:
    """Run all validations.

    Returns
    -------
    ValidationResult
        Result containing all validation issues
    """
    issues: list[Issue] = []

    issues.extend(self._check_required())
    issues.extend(self._check_cross_references())
    issues.extend(self._check_geometry())
    issues.extend(self._check_species())
    issues.extend(self._check_physics())
    issues.extend(self._check_physical_bounds())

    return ValidationResult(issues=issues)

Functions

safe_read_text

safe_read_text(
    file_path,
    max_size=MAX_OUTPUT_FILE_SIZE,
    encoding="utf-8",
)

Safely read a text file with size validation.

PARAMETER DESCRIPTION
file_path

Path to file to read

TYPE: Path

max_size

Maximum allowed file size in bytes, by default MAX_OUTPUT_FILE_SIZE

TYPE: int DEFAULT: MAX_OUTPUT_FILE_SIZE

encoding

Text encoding, by default "utf-8"

TYPE: str DEFAULT: 'utf-8'

RETURNS DESCRIPTION
str

File contents

RAISES DESCRIPTION
ValidationError

If file is too large or cannot be read

Source code in src/pyfds/validation/input.py
def safe_read_text(
    file_path: Path, max_size: int = MAX_OUTPUT_FILE_SIZE, encoding: str = "utf-8"
) -> str:
    """Safely read a text file with size validation.

    Parameters
    ----------
    file_path : Path
        Path to file to read
    max_size : int, optional
        Maximum allowed file size in bytes, by default MAX_OUTPUT_FILE_SIZE
    encoding : str, optional
        Text encoding, by default "utf-8"

    Returns
    -------
    str
        File contents

    Raises
    ------
    ValidationError
        If file is too large or cannot be read
    """
    # Validate file exists and size
    validate_file_size(file_path, max_size)

    try:
        return file_path.read_text(encoding=encoding)
    except UnicodeDecodeError as e:
        raise ValidationError(f"File is not valid {encoding} text: {file_path}") from e
    except OSError as e:
        raise ValidationError(f"Cannot read file {file_path}: {e}") from e

validate_chid

validate_chid(chid)

Validate and sanitize a CHID (case identifier).

CHID must be: - Non-empty - 60 characters or less - Contain only letters, numbers, underscores, and hyphens - Not contain path separators or path traversal sequences

PARAMETER DESCRIPTION
chid

Case identifier to validate

TYPE: str

RETURNS DESCRIPTION
str

Validated CHID (unchanged if valid)

RAISES DESCRIPTION
ValidationError

If CHID is invalid

Examples:

>>> validate_chid("my_simulation")
'my_simulation'
>>> validate_chid("test-case-123")
'test-case-123'
Source code in src/pyfds/validation/input.py
def validate_chid(chid: str) -> str:
    """Validate and sanitize a CHID (case identifier).

    CHID must be:
    - Non-empty
    - 60 characters or less
    - Contain only letters, numbers, underscores, and hyphens
    - Not contain path separators or path traversal sequences

    Parameters
    ----------
    chid : str
        Case identifier to validate

    Returns
    -------
    str
        Validated CHID (unchanged if valid)

    Raises
    ------
    ValidationError
        If CHID is invalid

    Examples
    --------
    >>> validate_chid("my_simulation")
    'my_simulation'
    >>> validate_chid("test-case-123")
    'test-case-123'
    """
    if not chid:
        raise ValidationError("CHID cannot be empty")

    if not isinstance(chid, str):
        raise ValidationError(f"CHID must be a string, got {type(chid).__name__}")

    # Check length
    if len(chid) > CHID_MAX_LENGTH:
        raise ValidationError(
            f"CHID must be {CHID_MAX_LENGTH} characters or less, got {len(chid)} characters"
        )

    # Check for path separators and path traversal
    if ".." in chid or "/" in chid or "\\" in chid:
        raise ValidationError(f"CHID cannot contain path separators or '..' sequences: '{chid}'")

    # Check for invalid characters
    if not CHID_PATTERN.match(chid):
        raise ValidationError(
            f"CHID can only contain letters, numbers, underscores, and hyphens. "
            f"Invalid CHID: '{chid}'"
        )

    return chid

validate_file_size

validate_file_size(
    file_path, max_size=MAX_OUTPUT_FILE_SIZE
)

Validate that a file is not too large to read safely.

PARAMETER DESCRIPTION
file_path

Path to file to check

TYPE: Path

max_size

Maximum allowed file size in bytes, by default MAX_OUTPUT_FILE_SIZE (100 MB)

TYPE: int DEFAULT: MAX_OUTPUT_FILE_SIZE

RETURNS DESCRIPTION
Path

The file path (unchanged if valid)

RAISES DESCRIPTION
ValidationError

If file is too large

Source code in src/pyfds/validation/input.py
def validate_file_size(file_path: Path, max_size: int = MAX_OUTPUT_FILE_SIZE) -> Path:
    """Validate that a file is not too large to read safely.

    Parameters
    ----------
    file_path : Path
        Path to file to check
    max_size : int, optional
        Maximum allowed file size in bytes, by default MAX_OUTPUT_FILE_SIZE (100 MB)

    Returns
    -------
    Path
        The file path (unchanged if valid)

    Raises
    ------
    ValidationError
        If file is too large
    """
    if not file_path.exists():
        raise ValidationError(f"File does not exist: {file_path}")

    if not file_path.is_file():
        raise ValidationError(f"Path is not a file: {file_path}")

    file_size = file_path.stat().st_size

    if file_size > max_size:
        size_mb = file_size / (1024 * 1024)
        max_mb = max_size / (1024 * 1024)
        raise ValidationError(
            f"File is too large to read: {file_path}\n"
            f"File size: {size_mb:.1f} MB\n"
            f"Maximum allowed: {max_mb:.1f} MB"
        )

    return file_path

validate_non_negative_number

validate_non_negative_number(value, name='value')

Validate that a number is non-negative (>= 0).

PARAMETER DESCRIPTION
value

Number to validate

TYPE: int or float

name

Name of the parameter (for error messages), by default "value"

TYPE: str DEFAULT: 'value'

RETURNS DESCRIPTION
int or float

The value (unchanged if valid)

RAISES DESCRIPTION
ValidationError

If value is negative

Source code in src/pyfds/validation/input.py
def validate_non_negative_number(value: int | float, name: str = "value") -> int | float:
    """Validate that a number is non-negative (>= 0).

    Parameters
    ----------
    value : int or float
        Number to validate
    name : str, optional
        Name of the parameter (for error messages), by default "value"

    Returns
    -------
    int or float
        The value (unchanged if valid)

    Raises
    ------
    ValidationError
        If value is negative
    """
    if not isinstance(value, (int, float)):
        raise ValidationError(f"{name} must be a number, got {type(value).__name__}")

    if value < 0:
        raise ValidationError(f"{name} must be non-negative, got {value}")

    return value

validate_path

validate_path(
    path,
    must_exist=False,
    must_be_file=False,
    must_be_dir=False,
    allow_create=True,
)

Validate and resolve a file system path.

PARAMETER DESCRIPTION
path

Path to validate

TYPE: str or Path

must_exist

If True, path must already exist, by default False

TYPE: bool DEFAULT: False

must_be_file

If True, path must be a file (not a directory), by default False

TYPE: bool DEFAULT: False

must_be_dir

If True, path must be a directory (not a file), by default False

TYPE: bool DEFAULT: False

allow_create

If True, allow paths that don't exist yet (for output files), by default True

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
Path

Validated, resolved absolute path

RAISES DESCRIPTION
ValidationError

If path validation fails

Source code in src/pyfds/validation/input.py
def validate_path(
    path: str | Path,
    must_exist: bool = False,
    must_be_file: bool = False,
    must_be_dir: bool = False,
    allow_create: bool = True,
) -> Path:
    """Validate and resolve a file system path.

    Parameters
    ----------
    path : str or Path
        Path to validate
    must_exist : bool, optional
        If True, path must already exist, by default False
    must_be_file : bool, optional
        If True, path must be a file (not a directory), by default False
    must_be_dir : bool, optional
        If True, path must be a directory (not a file), by default False
    allow_create : bool, optional
        If True, allow paths that don't exist yet (for output files), by default True

    Returns
    -------
    Path
        Validated, resolved absolute path

    Raises
    ------
    ValidationError
        If path validation fails
    """
    if not isinstance(path, (str, Path)):
        raise ValidationError(f"Path must be string or Path, got {type(path).__name__}")

    try:
        path = Path(path)
    except (TypeError, ValueError) as e:
        raise ValidationError(f"Invalid path: {e}") from e

    # Resolve to absolute path
    try:
        resolved = path.resolve()
    except (OSError, RuntimeError) as e:
        raise ValidationError(f"Cannot resolve path '{path}': {e}") from e

    # Check existence
    if must_exist and not resolved.exists():
        raise ValidationError(f"Path does not exist: {resolved}")

    if not allow_create and not resolved.exists():
        raise ValidationError(f"Path does not exist: {resolved}")

    # Check type constraints
    if resolved.exists():
        if must_be_file and not resolved.is_file():
            raise ValidationError(f"Path is not a file: {resolved}")
        if must_be_dir and not resolved.is_dir():
            raise ValidationError(f"Path is not a directory: {resolved}")

    # Check parent directory exists (for new files)
    if not resolved.exists() and allow_create and not resolved.parent.exists():
        raise ValidationError(f"Parent directory does not exist: {resolved.parent}")

    return resolved

validate_positive_number

validate_positive_number(value, name='value')

Validate that a number is positive (> 0).

PARAMETER DESCRIPTION
value

Number to validate

TYPE: int or float

name

Name of the parameter (for error messages), by default "value"

TYPE: str DEFAULT: 'value'

RETURNS DESCRIPTION
int or float

The value (unchanged if valid)

RAISES DESCRIPTION
ValidationError

If value is not positive

Source code in src/pyfds/validation/input.py
def validate_positive_number(value: int | float, name: str = "value") -> int | float:
    """Validate that a number is positive (> 0).

    Parameters
    ----------
    value : int or float
        Number to validate
    name : str, optional
        Name of the parameter (for error messages), by default "value"

    Returns
    -------
    int or float
        The value (unchanged if valid)

    Raises
    ------
    ValidationError
        If value is not positive
    """
    if not isinstance(value, (int, float)):
        raise ValidationError(f"{name} must be a number, got {type(value).__name__}")

    if value <= 0:
        raise ValidationError(f"{name} must be positive, got {value}")

    return value

validate_fds_file

validate_fds_file(filepath)

Validate an existing FDS input file.

PARAMETER DESCRIPTION
filepath

Path to FDS input file

TYPE: Path

RETURNS DESCRIPTION
bool

True if file is valid

RAISES DESCRIPTION
ValidationError

If validation fails

Source code in src/pyfds/validation/simulation.py
def validate_fds_file(filepath: Path) -> bool:
    """Validate an existing FDS input file.

    Parameters
    ----------
    filepath : Path
        Path to FDS input file

    Returns
    -------
    bool
        True if file is valid

    Raises
    ------
    ValidationError
        If validation fails
    """
    if not filepath.exists():
        raise ValidationError(f"File not found: {filepath}")

    if filepath.suffix != ".fds":
        raise ValidationError(f"File must have .fds extension: {filepath}")

    content = filepath.read_text()

    # Basic syntax checks
    if not re.search(r"&HEAD", content, re.IGNORECASE):
        raise ValidationError("Missing &HEAD namelist")

    if not re.search(r"&TAIL", content, re.IGNORECASE):
        raise ValidationError("Missing &TAIL namelist")

    # Check for balanced namelists
    namelist_starts = len(re.findall(r"&\w+", content))
    namelist_ends = len(re.findall(r"/", content))

    if namelist_starts != namelist_ends:
        raise ValidationError(
            f"Unbalanced namelists: {namelist_starts} starts, {namelist_ends} ends"
        )

    return True

validate_simulation

validate_simulation(simulation)

Validate a complete simulation configuration.

This is the main entry point for simulation validation. Runs all validation checks including required components, cross-references, geometry quality, and physical bounds.

PARAMETER DESCRIPTION
simulation

Simulation to validate

TYPE: Simulation

RETURNS DESCRIPTION
ValidationResult

Validation result with all issues

Examples:

>>> sim = Simulation(chid="test")
>>> sim.add(Mesh(...), Time(...))
>>> result = validate_simulation(sim)
>>> if result.is_valid:
...     print("Simulation is valid!")
Source code in src/pyfds/validation/__init__.py
def validate_simulation(simulation: "Simulation") -> ValidationResult:
    """Validate a complete simulation configuration.

    This is the main entry point for simulation validation.
    Runs all validation checks including required components,
    cross-references, geometry quality, and physical bounds.

    Parameters
    ----------
    simulation : Simulation
        Simulation to validate

    Returns
    -------
    ValidationResult
        Validation result with all issues

    Examples
    --------
    >>> sim = Simulation(chid="test")
    >>> sim.add(Mesh(...), Time(...))
    >>> result = validate_simulation(sim)
    >>> if result.is_valid:
    ...     print("Simulation is valid!")
    """
    validator = SimulationValidator(simulation)
    return validator.validate()

Overview

The validation module provides comprehensive validation for FDS simulations, including input sanitization, simulation-level validation, cross-reference checking, and execution configuration validation.

Core Classes

Validator

Main validation class for checking simulation configurations.

from pyfds import Simulation, Mesh, Time
from pyfds.core.geometry import Bounds3D, Grid3D
from pyfds.validation import Validator

sim = Simulation(chid="test")
sim.add(Time(t_end=600.0))
sim.add(Mesh(ijk=Grid3D.of(50, 50, 25), xb=Bounds3D.of(0, 5, 0, 5, 0, 2.5)))

# Validate simulation
validator = Validator()
result = validator.validate(sim)

if result.is_valid:
    print("Simulation is valid!")
else:
    for error in result.errors:
        print(f"Error: {error}")

ValidationResult

Container for validation results with errors, warnings, and info messages.

from pyfds.validation import validate_simulation

sim = Simulation(chid="test")
# ... build simulation ...

result = validate_simulation(sim)

print(f"Valid: {result.is_valid}")
print(f"Errors: {len(result.errors)}")
print(f"Warnings: {len(result.warnings)}")
print(f"Info: {len(result.info)}")

# Get all issues
for issue in result.all_issues:
    print(f"[{issue.severity}] {issue.message}")

Issue

Represents a single validation issue with severity, message, and context.

from pyfds.validation import Issue, Severity

# Issues are created by validators
issue = Issue(
    severity=Severity.ERROR,
    message="Missing required parameter",
    field="t_end",
    value=None
)

print(f"{issue.severity}: {issue.message}")

Severity

Enumeration of validation issue severity levels.

from pyfds.validation import Severity

# Severity levels
Severity.ERROR    # Critical issues that prevent FDS from running
Severity.WARNING  # Issues that may cause problems
Severity.INFO     # Informational messages

Validation Functions

validate_simulation()

Main entry point for validating a complete simulation.

from pyfds import Simulation, Mesh, Time
from pyfds.core.geometry import Bounds3D, Grid3D
from pyfds.validation import validate_simulation

sim = Simulation(chid="test")
sim.add(Time(t_end=600.0))
sim.add(Mesh(ijk=Grid3D.of(50, 50, 25), xb=Bounds3D.of(0, 5, 0, 5, 0, 2.5)))

result = validate_simulation(sim)

if not result.is_valid:
    for error in result.errors:
        print(f"Error: {error.message}")
else:
    sim.write("test.fds")

validate_fds_file()

Validate an existing FDS input file.

from pyfds.validation import validate_fds_file

result = validate_fds_file("simulation.fds")

if result.is_valid:
    print("FDS file is valid")
else:
    print("Validation errors found:")
    for error in result.errors:
        print(f"  - {error.message}")

Input Validators

validate_chid()

Validate simulation CHID (case ID).

from pyfds.validation import validate_chid, CHID_MAX_LENGTH

# Valid CHID
try:
    validate_chid("my_simulation")
except ValueError as e:
    print(f"Invalid CHID: {e}")

# Too long
try:
    validate_chid("a" * (CHID_MAX_LENGTH + 1))
except ValueError as e:
    print(f"CHID too long: {e}")

# Invalid characters
try:
    validate_chid("my simulation")  # Spaces not allowed
except ValueError as e:
    print(f"Invalid characters: {e}")

validate_path()

Validate file paths for safety and existence.

from pyfds.validation import validate_path
from pathlib import Path

# Check if path exists
try:
    validate_path("/path/to/file.fds", must_exist=True)
except ValueError as e:
    print(f"Path validation failed: {e}")

# Check if parent directory exists
try:
    validate_path("/path/to/output.fds", check_parent=True)
except ValueError as e:
    print(f"Parent directory doesn't exist: {e}")

validate_positive_number()

Ensure a number is positive.

from pyfds.validation import validate_positive_number

# Valid
validate_positive_number(10.0, "t_end")  # OK

# Invalid
try:
    validate_positive_number(0, "t_end")
except ValueError as e:
    print(f"Error: {e}")  # "t_end must be positive"

try:
    validate_positive_number(-5.0, "dt")
except ValueError as e:
    print(f"Error: {e}")  # "dt must be positive"

validate_non_negative_number()

Ensure a number is non-negative (zero allowed).

from pyfds.validation import validate_non_negative_number

# Valid
validate_non_negative_number(0, "t_begin")    # OK
validate_non_negative_number(10.0, "t_begin") # OK

# Invalid
try:
    validate_non_negative_number(-1.0, "t_begin")
except ValueError as e:
    print(f"Error: {e}")  # "t_begin must be non-negative"

validate_file_size()

Check if a file size is within acceptable limits.

from pyfds.validation import validate_file_size, MAX_INPUT_FILE_SIZE
from pathlib import Path

fds_file = Path("large_simulation.fds")

try:
    validate_file_size(fds_file, MAX_INPUT_FILE_SIZE)
except ValueError as e:
    print(f"File too large: {e}")

safe_read_text()

Safely read text files with size and encoding validation.

from pyfds.validation import safe_read_text

try:
    content = safe_read_text("simulation.fds")
    print(f"Read {len(content)} characters")
except ValueError as e:
    print(f"Failed to read file: {e}")

Specialized Validators

SimulationValidator

Comprehensive validation for complete simulations.

from pyfds.validation import SimulationValidator

sim = Simulation(chid="test")
# ... build simulation ...

validator = SimulationValidator(sim)
result = validator.validate()

# Check specific aspects
result.check_required_components()
result.check_cross_references()
result.check_geometry_quality()
result.check_physical_bounds()

CrossReferenceValidator

Validates ID references between namelists.

from pyfds.validation import CrossReferenceValidator

validator = CrossReferenceValidator(sim)
issues = validator.validate()

for issue in issues:
    if "surf_id" in issue.message.lower():
        print(f"Surface reference issue: {issue.message}")

ExecutionValidator

Validates execution configuration.

from pyfds.validation import ExecutionValidator
from pyfds.config import RunConfig

config = RunConfig(n_threads=4, timeout=3600)
validator = ExecutionValidator()

issues = validator.validate_config(config)
if issues:
    for issue in issues:
        print(f"Config issue: {issue.message}")

Validation Constants

from pyfds.validation import (
    CHID_MAX_LENGTH,      # Maximum CHID length
    CHID_PATTERN,         # Regex pattern for valid CHIDs
    MAX_INPUT_FILE_SIZE,  # Maximum input file size
    MAX_OUTPUT_FILE_SIZE, # Maximum output file size
)

print(f"Max CHID length: {CHID_MAX_LENGTH}")
print(f"Max input file: {MAX_INPUT_FILE_SIZE / 1024 / 1024} MB")

Built-in Enums

BuiltinSpecies

Enumeration of FDS built-in species.

from pyfds.validation import BuiltinSpecies

# Check if species is built-in
if "OXYGEN" in [s.value for s in BuiltinSpecies]:
    print("OXYGEN is a built-in species")

# List all built-in species
for species in BuiltinSpecies:
    print(species.value)

BuiltinSurface

Enumeration of FDS built-in surfaces.

from pyfds.validation import BuiltinSurface

# Check if surface is built-in
if "INERT" in [s.value for s in BuiltinSurface]:
    print("INERT is a built-in surface")

# List all built-in surfaces
for surface in BuiltinSurface:
    print(surface.value)

Usage Patterns

Pre-Write Validation

from pyfds import Simulation
from pyfds.validation import validate_simulation

sim = Simulation(chid="test")
# ... build simulation ...

# Validate before writing
result = validate_simulation(sim)
if result.is_valid:
    sim.write("test.fds")
else:
    print("Fix these errors before writing:")
    for error in result.errors:
        print(f"  - {error.message}")

Progressive Validation

from pyfds import Simulation
from pyfds.validation import SimulationValidator

sim = Simulation(chid="test")
validator = SimulationValidator(sim)

# Add components and validate incrementally
sim.add(Time(t_end=600.0))
if not validator.check_required_components():
    print("Still missing required components")

sim.add(Mesh(...))
if validator.validate().is_valid:
    print("Simulation is now valid!")

Custom Validation

from pyfds.validation import Validator, Issue, Severity

class CustomValidator(Validator):
    """Custom validator with project-specific rules."""

    def validate_custom(self, sim):
        """Check custom requirements."""
        issues = []

        # Example: require at least one device
        if not sim.get_all(Device):
            issues.append(Issue(
                severity=Severity.WARNING,
                message="No devices defined - results may be limited",
            ))

        return issues

See Also