From 04dcd5e917bc9ad048be5cf0eec9f9786a0ca782 Mon Sep 17 00:00:00 2001 From: Sebastian Strempfer Date: Sun, 8 Feb 2026 21:04:59 -0600 Subject: [PATCH] Initial commit --- README.md | 197 +++++++++++++++++ diagnose.sh | 191 ++++++++++++++++ environment.yml | 98 +++++++++ setup.sh | 575 ++++++++++++++++++++++++++++++++++++++++++++++++ verify.py | 276 +++++++++++++++++++++++ 5 files changed, 1337 insertions(+) create mode 100755 README.md create mode 100755 diagnose.sh create mode 100755 environment.yml create mode 100755 setup.sh create mode 100755 verify.py diff --git a/README.md b/README.md new file mode 100755 index 0000000..b6dcc1d --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +# Firedrake + icepack via conda + +Install [Firedrake](https://www.firedrakeproject.org/) and [icepack](https://icepack.github.io/) into a self-contained conda environment with **no system packages required** beyond conda itself. + +## How it works + +This is a hybrid conda + pip approach: + +- **conda** provides all the difficult-to-compile native dependencies: PETSc (with MUMPS, hypre, SuperLU, etc.), MPI, HDF5, BLAS/LAPACK, compilers, and the C toolchain needed for Firedrake's JIT compilation. +- **pip** installs Firedrake and its Python ecosystem (UFL, FIAT, TSFC, PyOP2, loopy) from git, since these are not on conda-forge. +- **Activation scripts** automatically set `PETSC_DIR`, `CC=mpicc`, `HDF5_MPI=ON`, and other environment variables when you `conda activate` so that Firedrake's runtime code generation compiles against the conda-provided libraries. + +The key insight is that conda-forge's PETSc is built with all the external packages Firedrake needs, and PETSc installed in "prefix" mode (`PETSC_DIR=$CONDA_PREFIX`, no `PETSC_ARCH`) is compatible with how Firedrake discovers PETSc at runtime. + +## Prerequisites + +- **conda** or **mamba** — [Install miniforge](https://github.com/conda-forge/miniforge) (recommended) or [miniconda](https://docs.conda.io/en/latest/miniconda.html) +- **Git** — needed to clone Firedrake repositories +- ~5 GB disk space for the full environment +- ~15 minutes for setup (mostly downloading conda packages) + +No `sudo`, no system package manager, no Docker required. + +## Installation + +```bash +git clone firedrake-conda +cd firedrake-conda +bash setup.sh +``` + +By default this creates a conda environment named `firedrake` with Firedrake +release **2025.10.2**. Both can be customised: + +```bash +# Custom environment name +bash setup.sh my-firedrake + +# Environment at a specific path (uses conda -p) +bash setup.sh ./envs/firedrake +bash setup.sh ~/projects/icesheet/env + +# Change Firedrake version +bash setup.sh firedrake main +bash setup.sh firedrake 2025.10.1 + +# Or parameterize using environment variables (takes precedence over commandline arguments) +FIREDRAKE_ENV=~/fd FIREDRAKE_REF=2025.10.2 bash setup.sh +``` + +The script will: +1. Create a conda environment (named or at a path) with all native dependencies +2. Set up activation scripts for the environment variables +3. Clone Firedrake and icepack with visible git progress +4. pip-install them, patching out conda-provided deps (petsc4py, mpi4py, h5py) + to avoid build failures + +## Usage + +```bash +conda activate firedrake +python +``` + +```python +import firedrake +import icepack + +# Everything works as documented in icepack tutorials +mesh = firedrake.Mesh("glacier.msh") +Q = firedrake.FunctionSpace(mesh, "CG", 2) +V = firedrake.VectorFunctionSpace(mesh, "CG", 2) + +model = icepack.models.IceStream() +solver = icepack.solvers.FlowSolver(model) +u = solver.diagnostic_solve(velocity=u0, thickness=h, ...) +``` + +The environment variables (`PETSC_DIR`, `CC`, `HDF5_MPI`, etc.) are automatically set when you `conda activate firedrake` and restored when you deactivate. + +## Verification + +```bash +conda activate firedrake +python verify.py +``` + +This tests environment variables, library linking, Python imports, JIT compilation, a Poisson solve, and icepack model instantiation. + +If something fails, run the diagnostic script for detailed information: + +```bash +bash diagnose.sh +``` + +## Troubleshooting + +### `pip install firedrake` fails to build extensions + +This usually means the C compiler can't find PETSc headers. Check that: +```bash +echo $PETSC_DIR # should be $CONDA_PREFIX +echo $CC # should be mpicc +mpicc --version # should work and show conda's compiler +``` + +If these aren't set, deactivate and reactivate: +```bash +conda deactivate +conda activate firedrake +``` + +### Import errors about missing symbols (`libpetsc.so`) + +This means a Python extension was linked against a different PETSc than the one in the conda env. Fix by rebuilding the offending package: +```bash +pip cache purge +pip install --force-reinstall --no-binary :all: --no-build-isolation petsc4py +``` + +### `h5py` MPI errors + +h5py must be built with MPI support. The conda environment provides this, but if pip reinstalls h5py it may pull a non-MPI wheel. Force the conda version: +```bash +conda install -c conda-forge "h5py=*=mpi_mpich_*" +``` + +### JIT-compiled kernels segfault + +This usually means the JIT compiler is using the system compiler instead of conda's `mpicc`. Verify: +```bash +which mpicc # should be in $CONDA_PREFIX/bin/ +mpicc -show # should reference $CONDA_PREFIX libraries +``` + +### Updating + +To update Firedrake and icepack while keeping conda dependencies: +```bash +conda activate firedrake +# Pull latest sources (or checkout a different tag) +cd ~/.firedrake-conda/clones/firedrake +git fetch --tags origin +git checkout 2025.10.2 # or: git checkout main && git pull +cd ~/.firedrake-conda/clones/icepack && git pull --progress +# Reinstall using the constraints file +pip install --upgrade --no-build-isolation \ + -c $CONDA_PREFIX/constraints.txt \ + ~/.firedrake-conda/clones/firedrake ~/.firedrake-conda/clones/icepack +``` + +To update all conda dependencies (then regenerate constraints): +```bash +conda update --all -n firedrake +# Re-run setup.sh to regenerate constraints.txt +``` + +## What conda provides vs what pip provides + +| Package | Source | Why | +|---------|--------|-----| +| PETSc + petsc4py | conda-forge | Complex build with MUMPS, hypre, etc. | +| SLEPc + slepc4py | conda-forge | Must match PETSc version | +| mpich + mpi4py | conda-forge | MPI ABI consistency | +| HDF5 + h5py | conda-forge | Must be MPI-enabled | +| compilers (gcc/clang) | conda-forge | Runtime JIT needs consistent ABI | +| BLAS/LAPACK | conda-forge | Linked by PETSc | +| numpy, scipy, sympy | conda-forge | Standard scientific Python | +| islpy | conda-forge | Has compiled C extension | +| **firedrake** | **pip (git)** | Not on conda-forge | +| fenics-ufl | pip (git, via firedrake) | Firedrake tracks development branch | +| firedrake-fiat | pip (git, via firedrake) | Firedrake's own fork | +| tsfc, pyop2, finat | pip (via firedrake) | Part of Firedrake monorepo | +| loopy | pip (via firedrake) | Code generation stack | +| libsupermesh | pip (git) | Small C lib, builds against conda MPI | +| **icepack** | **pip (git)** | Pure Python, depends on firedrake | + +## Platform support + +| Platform | Status | Notes | +|----------|--------|-------| +| Linux x86_64 | Primary | Best tested | +| macOS ARM64 (Apple Silicon) | Supported | Uses clang from conda | +| macOS x86_64 (Intel) | Untested | Should work but Firedrake dropped official support | +| Linux aarch64 | Should work | conda-forge has ARM builds | +| Windows (WSL2) | Should work | Via WSL2's Linux environment | + +## Files + +``` +firedrake-conda/ +├── environment.yml # Conda env spec (all native deps) +├── setup.sh # One-command setup script +├── verify.py # Post-install verification +├── diagnose.sh # Detailed diagnostics for debugging +└── README.md +``` diff --git a/diagnose.sh b/diagnose.sh new file mode 100755 index 0000000..3a2b0dc --- /dev/null +++ b/diagnose.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# ============================================================================ +# diagnose.sh — Diagnose linking and configuration issues +# +# Run this if verify.py fails. It prints detailed information about +# the environment that helps debug library conflicts. +# +# Usage: +# conda activate firedrake +# bash diagnose.sh +# ============================================================================ + +set -uo pipefail + +echo "═══════════════════════════════════════════════════════════════" +echo "Firedrake conda environment diagnostics" +echo "═══════════════════════════════════════════════════════════════" + +echo "" +echo "── Platform ─────────────────────────────────────────────────" +echo "OS: $(uname -s)" +echo "Arch: $(uname -m)" +echo "Kernel: $(uname -r)" +if [[ "$(uname -s)" == "Linux" ]]; then + echo "Distro: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')" + echo "glibc: $(ldd --version 2>&1 | head -1)" +fi + +echo "" +echo "── Conda ────────────────────────────────────────────────────" +echo "conda: $(conda --version 2>/dev/null || echo 'not found')" +echo "mamba: $(mamba --version 2>/dev/null | head -1 || echo 'not found')" +echo "CONDA_PREFIX: ${CONDA_PREFIX:-not set}" +echo "Active env: ${CONDA_DEFAULT_ENV:-none}" + +echo "" +echo "── Key environment variables ────────────────────────────────" +for var in PETSC_DIR PETSC_ARCH CC CXX FC HDF5_MPI HDF5_DIR \ + PYOP2_CFLAGS PYOP2_LDFLAGS OMP_NUM_THREADS \ + LD_LIBRARY_PATH DYLD_LIBRARY_PATH PATH; do + val="${!var:-}" + # Truncate long PATH values + if [[ ${#val} -gt 120 ]]; then + val="${val:0:120}..." + fi + printf " %-22s %s\n" "$var" "$val" +done + +echo "" +echo "── Compilers ────────────────────────────────────────────────" +for cmd in cc gcc mpicc mpicxx mpifort; do + loc=$(which "$cmd" 2>/dev/null || echo "not found") + printf " %-10s %s\n" "$cmd" "$loc" +done + +echo "" +echo "── MPI ────────────────────────────────────────────────────────" +if command -v mpicc &> /dev/null; then + echo "mpicc -show:" + mpicc -show 2>/dev/null || mpicc --showme 2>/dev/null || echo " (could not determine)" +fi +if command -v mpiexec &> /dev/null; then + mpiexec --version 2>&1 | head -3 +fi + +echo "" +echo "── Shared libraries in CONDA_PREFIX/lib ─────────────────────" +if [[ -n "${CONDA_PREFIX:-}" ]]; then + for lib in libpetsc libslepc libmpi libhdf5 libopenblas libsuperlu_dist \ + libdmumps libscotch libmetis libparmetis; do + found=$(ls "${CONDA_PREFIX}/lib/${lib}"* 2>/dev/null | head -1) + if [[ -n "$found" ]]; then + printf " %-25s %s\n" "$lib" "$(basename "$found")" + else + printf " %-25s %s\n" "$lib" "NOT FOUND" + fi + done +fi + +echo "" +echo "── Python packages ──────────────────────────────────────────" +python3 -c " +import sys +print(f' Python: {sys.version}') +print(f' Prefix: {sys.prefix}') +print() + +pkgs = [ + 'numpy', 'mpi4py', 'petsc4py', 'slepc4py', 'h5py', + 'ufl', 'FIAT', 'finat', 'tsfc', 'loopy', 'pyop2', + 'firedrake', 'icepack', +] +for name in pkgs: + try: + mod = __import__(name) + ver = getattr(mod, '__version__', '?') + loc = getattr(mod, '__file__', '?') + # Shorten path + loc = loc.replace(sys.prefix, '\$PREFIX') + print(f' {name:15s} {ver:20s} {loc}') + except ImportError as e: + print(f' {name:15s} IMPORT FAILED: {e}') +" + +echo "" +echo "── petsc4py linking ─────────────────────────────────────────" +if [[ "$(uname -s)" == "Linux" ]]; then + PETSC4PY_SO=$(python3 -c "import petsc4py; print(petsc4py.__file__)" 2>/dev/null) + if [[ -n "$PETSC4PY_SO" ]]; then + PETSC4PY_DIR=$(dirname "$PETSC4PY_SO") + SO_FILE=$(find "$PETSC4PY_DIR" -name "PETSc*.so" 2>/dev/null | head -1) + if [[ -n "$SO_FILE" ]]; then + echo " petsc4py .so: $SO_FILE" + echo " Links against:" + LD_LIBRARY_PATH="${CONDA_PREFIX}/lib:${LD_LIBRARY_PATH:-}" ldd "$SO_FILE" 2>/dev/null | grep -E "petsc|mpi|hdf5|blas|lapack" | sed 's/^/ /' + fi + fi +elif [[ "$(uname -s)" == "Darwin" ]]; then + PETSC4PY_SO=$(python3 -c "import petsc4py; print(petsc4py.__file__)" 2>/dev/null) + if [[ -n "$PETSC4PY_SO" ]]; then + PETSC4PY_DIR=$(dirname "$PETSC4PY_SO") + SO_FILE=$(find "$PETSC4PY_DIR" -name "PETSc*.so" -o -name "PETSc*.dylib" 2>/dev/null | head -1) + if [[ -n "$SO_FILE" ]]; then + echo " petsc4py .so: $SO_FILE" + echo " Links against:" + otool -L "$SO_FILE" 2>/dev/null | grep -E "petsc|mpi|hdf5|blas|lapack" | sed 's/^/ /' + fi + fi +fi + +echo "" +echo "── JIT compilation test ─────────────────────────────────────" +python3 -c " +import os, tempfile, subprocess, sys + +cc = os.environ.get('CC', 'mpicc') +prefix = os.environ.get('CONDA_PREFIX', '') + +# Write a minimal C file that links against PETSc +src = ''' +#include +int main(int argc, char **argv) { + PetscInitialize(&argc, &argv, NULL, NULL); + PetscPrintf(PETSC_COMM_WORLD, \"PETSc JIT test OK\\\\n\"); + PetscFinalize(); + return 0; +} +''' + +with tempfile.NamedTemporaryFile(suffix='.c', mode='w', delete=False) as f: + f.write(src) + src_path = f.name + +out_path = src_path.replace('.c', '') + +# Build command similar to what PyOP2 would use +cmd = [ + cc, + src_path, + '-o', out_path, + f'-I{prefix}/include', + f'-L{prefix}/lib', + '-lpetsc', + f'-Wl,-rpath,{prefix}/lib', +] + +print(f' Compile command: {\" \".join(cmd)}') +result = subprocess.run(cmd, capture_output=True, text=True) +if result.returncode == 0: + print(' ✓ Compilation succeeded') + run = subprocess.run([out_path], capture_output=True, text=True) + if run.returncode == 0: + print(f' ✓ Execution succeeded: {run.stdout.strip()}') + else: + print(f' ✗ Execution failed: {run.stderr.strip()}') +else: + print(f' ✗ Compilation failed:') + print(f' stdout: {result.stdout.strip()}') + print(f' stderr: {result.stderr.strip()}') + +os.unlink(src_path) +try: + os.unlink(out_path) +except: + pass +" 2>&1 + +echo "" +echo "═══════════════════════════════════════════════════════════════" +echo "Done. If issues persist, share this output when seeking help." +echo "═══════════════════════════════════════════════════════════════" diff --git a/environment.yml b/environment.yml new file mode 100755 index 0000000..7dcb3a7 --- /dev/null +++ b/environment.yml @@ -0,0 +1,98 @@ +# Firedrake + icepack conda environment +# +# This provides all the compiled/native dependencies (PETSc, MPI, compilers, +# etc.) from conda-forge so that no system packages are needed beyond conda +# itself. Firedrake's own Python packages are then pip-installed on top. +# +# Usage: +# conda env create -f environment.yml +# conda activate firedrake +# bash setup.sh +# +# Tested on: Linux x86_64, macOS ARM64 (Apple Silicon) + +name: firedrake +channels: + - conda-forge + - nodefaults + +dependencies: + # ── Python ── + - python >=3.11,<3.13 + + # ── Compilers (needed at runtime for Firedrake JIT) ── + # The `compilers` metapackage provides gcc/gfortran on Linux and + # clang/gfortran on macOS, all targeting the conda env's sysroot. + - compilers + - cmake + - make + + # ── MPI ── + # mpich is more portable across platforms in conda-forge than openmpi. + # All MPI-linked packages below will be built against this same mpich. + - mpich + + # ── PETSc ecosystem ── + # conda-forge PETSc includes: MUMPS, hypre, SuperLU, SuperLU_dist, + # METIS, ParMETIS, ScaLAPACK, SuiteSparse, pt-scotch. + - petsc =*=real* + - petsc4py + - slepc + - slepc4py + + # ── HDF5 with MPI support ── + - hdf5 =*=mpi_mpich_* + - h5py =*=mpi_mpich_* + + # ── Linear algebra ── + - libopenblas + - libblas =*=*openblas* + - liblapack =*=*openblas* + - scalapack + + # ── Other compiled libs Firedrake needs ── + - patchelf # [linux] + - libspatialindex + - gmsh + + # ── Core Python dependencies ── + - numpy + - scipy + - cython + - mpi4py + - sympy + - cachetools + - packaging + - decorator + - requests + - setuptools + - pip + - wheel + - meson + - meson-python + - pkgconfig + - scikit-build-core + - ninja + - pybind11 + - hatchling + - flit-core + + # ── islpy / loopy code generation stack ── + - islpy + + # ── Visualization (optional but useful) ── + - matplotlib + + # ── Pip-installed packages ── + # These are the Firedrake-specific packages that are NOT on conda-forge. + # They're installed in the setup script, listed here for documentation. + # - pip: + # - fenics-ufl (from git, Firedrake-compatible version) + # - firedrake-fiat (Firedrake's FIAT fork) + # - FInAT + # - loopy (latest, for code generation) + # - tsfc + # - pyop2 + # - libsupermesh + # - firedrake + # - icepack diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..0cc0626 --- /dev/null +++ b/setup.sh @@ -0,0 +1,575 @@ +#!/usr/bin/env bash +# ============================================================================ +# setup.sh — Install Firedrake + icepack into a conda environment +# +# This script: +# 1. Creates a conda environment with all native dependencies from conda-forge +# 2. Sets environment variables so Firedrake finds conda's PETSc, MPI, etc. +# 3. pip-installs Firedrake and icepack (skipping binary builds for +# MPI-linked packages so they compile against conda's libraries) +# +# Usage: +# bash setup.sh # env named "firedrake" +# bash setup.sh my-env # env named "my-env" +# bash setup.sh ./envs/firedrake # env at path ./envs/firedrake +# FIREDRAKE_ENV=~/fd FIREDRAKE_REF=main bash setup.sh +# +# Prerequisites: +# - conda or mamba installed +# - internet access (to download packages) +# +# Tested on: Linux x86_64, macOS ARM64 +# ============================================================================ + +set -euo pipefail + +# ── Configuration ────────────────────────────────────────────────────────── + +# Environment name or path — accepts: +# - A simple name like "firedrake" → conda env -n firedrake +# - An absolute/relative path like "./envs/fd" → conda env -p ./envs/fd +# Set via FIREDRAKE_ENV env var, first positional arg, or default "firedrake". +ENV_TARGET="${FIREDRAKE_ENV:-${1:-firedrake}}" + +# Detect if it's a path (contains / or starts with .) +if [[ "$ENV_TARGET" == */* ]] || [[ "$ENV_TARGET" == .* ]]; then + # Resolve to absolute path, creating parent dir if needed + _parent="$(dirname "$ENV_TARGET")" + mkdir -p "$_parent" 2>/dev/null || true + ENV_TARGET="$(cd "$_parent" && pwd)/$(basename "$ENV_TARGET")" + ENV_IS_PATH=true + CONDA_ENV_FLAG="-p" +else + ENV_IS_PATH=false + CONDA_ENV_FLAG="-n" +fi +ENV_DISPLAY="$ENV_TARGET" +ENV_ACTIVATE="$ENV_TARGET" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Firedrake version: set to a release tag (e.g. "2025.10.2") or "main" +FIREDRAKE_REF="${FIREDRAKE_REF:-${2:-2025.10.2}}" + +# Prefer mamba if available (much faster solver) +if command -v mamba &> /dev/null; then + CONDA=mamba +elif command -v conda &> /dev/null; then + CONDA=conda +else + echo "ERROR: Neither conda nor mamba found. Please install one first." + echo " See: https://docs.conda.io/en/latest/miniconda.html" + exit 1 +fi + +# ── Colors ───────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +DIM='\033[2m' +NC='\033[0m' # No color + +info() { echo -e "${BLUE}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +fail() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; } + +# ── Live rolling tail ───────────────────────────────────────────────────── +# Runs a command while showing a live, continuously-updating view of the +# last N lines of its combined stdout/stderr. When the command finishes, +# the rolling display is cleared and replaced with a summary. +# +# Usage: run_live +# Returns the exit code of the command. + +TAIL_LINES=20 + +run_live() { + local nlines="$1"; shift + local desc="$1"; shift + + local logfile + logfile=$(mktemp /tmp/firedrake-install-XXXXXX.log) + + echo -e "${DIM}─── ${desc} ───${NC}" + + # Run the actual command in the background, directing all output to logfile + "$@" > "$logfile" 2>&1 & + local cmd_pid=$! + + # Tail the logfile in real-time, showing last N lines with in-place rewriting + python3 -u -c " +import sys, os, time + +nlines = int(sys.argv[1]) +path = sys.argv[2] +pid = int(sys.argv[3]) +cols = int(os.environ.get('COLUMNS', 120)) + +buf = [] +shown = 0 + +def render(): + global shown + # Move up to overwrite previous output + if shown > 0: + sys.stdout.write(f'\033[{shown}A') + shown = 0 + for b in buf: + sys.stdout.write(f'\033[2K {b}\n') + shown += 1 + sys.stdout.flush() + +def pid_alive(p): + try: + os.kill(p, 0) + return True + except ProcessLookupError: + return False + +with open(path, 'r') as f: + while True: + line = f.readline() + if line: + line = line.rstrip('\n')[:cols] + buf.append(line) + if len(buf) > nlines: + buf = buf[-nlines:] + render() + else: + # No new data — check if process is still running + if not pid_alive(pid): + # Drain any remaining lines + for line in f: + line = line.rstrip('\n')[:cols] + buf.append(line) + if len(buf) > nlines: + buf = buf[-nlines:] + render() + break + time.sleep(0.05) + +# Clear the rolling display +if shown > 0: + sys.stdout.write(f'\033[{shown}A') + for _ in range(shown): + sys.stdout.write('\033[2K\n') + sys.stdout.write(f'\033[{shown}A') + sys.stdout.flush() +" "$nlines" "$logfile" "$cmd_pid" + + # Collect the exit code + local exit_code=0 + wait "$cmd_pid" || exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + warn "${desc} failed (exit code ${exit_code}). Full log: ${logfile}" + echo -e "${DIM}─── Last 30 lines of ${logfile} ───${NC}" + tail -30 "$logfile" + echo -e "${DIM}────────────────────────────────────${NC}" + else + rm -f "$logfile" + fi + + return $exit_code +} + +# ── Step 1: Create conda environment ────────────────────────────────────── + +info "Creating conda environment '${ENV_DISPLAY}' from environment.yml ..." + +# Check if environment already exists +ENV_EXISTS=false +if [[ "$ENV_IS_PATH" == true ]]; then + [[ -d "${ENV_TARGET}/conda-meta" ]] && ENV_EXISTS=true +else + $CONDA env list 2>/dev/null | grep -q "^${ENV_TARGET} " && ENV_EXISTS=true +fi + +if [[ "$ENV_EXISTS" == true ]]; then + warn "Environment '${ENV_DISPLAY}' already exists." + read -p " Recreate it? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + $CONDA env remove ${CONDA_ENV_FLAG} "${ENV_TARGET}" -y + else + info "Reusing existing environment." + fi +fi + +$CONDA env create ${CONDA_ENV_FLAG} "${ENV_TARGET}" -f "${SCRIPT_DIR}/environment.yml" || \ + $CONDA env update ${CONDA_ENV_FLAG} "${ENV_TARGET}" -f "${SCRIPT_DIR}/environment.yml" + +ok "Conda environment created." + +# ── Step 2: Activate and detect paths ───────────────────────────────────── + +# We need to source conda's shell hook to use `conda activate` in a script +eval "$(conda shell.bash hook)" +conda activate "${ENV_ACTIVATE}" + +CONDA_PREFIX="${CONDA_PREFIX}" +info "CONDA_PREFIX = ${CONDA_PREFIX}" + +# Detect platform +OS="$(uname -s)" +ARCH="$(uname -m)" +info "Platform: ${OS} ${ARCH}" + +if [[ "$OS" == "Darwin" ]]; then + SOEXT="dylib" +else + SOEXT="so" +fi + +# ── Step 3: Verify conda packages ──────────────────────────────────────── + +info "Verifying critical conda packages ..." + +# Check PETSc +if [[ ! -f "${CONDA_PREFIX}/lib/libpetsc.${SOEXT}" ]]; then + fail "PETSc shared library not found at ${CONDA_PREFIX}/lib/libpetsc.${SOEXT}" +fi +ok "PETSc found." + +# Check MPI compiler wrappers +if ! command -v mpicc &> /dev/null; then + fail "mpicc not found. MPI compiler wrappers are missing." +fi +ok "MPI compilers found: $(which mpicc)" + +# Check HDF5 +if [[ ! -f "${CONDA_PREFIX}/lib/libhdf5.${SOEXT}" ]]; then + fail "HDF5 not found." +fi +ok "HDF5 found." + +# ── Step 4: Set environment variables ───────────────────────────────────── + +info "Setting environment variables for Firedrake ..." + +# PETSc: conda installs in "prefix" mode (no PETSC_ARCH) +export PETSC_DIR="${CONDA_PREFIX}" +export PETSC_ARCH="" + +# Compilers: use MPI wrappers from conda +export CC=mpicc +export CXX=mpicxx +export FC=mpifort + +# HDF5: tell h5py and Firedrake to use MPI-enabled HDF5 +export HDF5_MPI="ON" +export HDF5_DIR="${CONDA_PREFIX}" + +# Firedrake recommends single-threaded BLAS +export OMP_NUM_THREADS=1 + +# PyOP2 JIT: conda's mpicc wraps x86_64-conda-linux-gnu-cc which PyOP2 +# doesn't recognise → falls back to generic compiler without -shared/-fPIC +export PYOP2_CFLAGS="-fPIC -O2" +export PYOP2_LDFLAGS="-shared -Wl,-rpath,${CONDA_PREFIX}/lib" + +# Tell pip not to use cached wheels for MPI-linked packages +# (they may be linked against the wrong MPI/PETSc) +export PIP_NO_CACHE_DIR=1 + +info " PETSC_DIR = ${PETSC_DIR}" +info " PETSC_ARCH = (empty — prefix install)" +info " CC = ${CC}" +info " HDF5_MPI = ${HDF5_MPI}" + +# ── Step 5: Pip install Firedrake ecosystem ─────────────────────────────── + +info "Installing Firedrake and dependencies via pip ..." +info "This may take several minutes (compiling C extensions) ..." + +# Purge pip cache to avoid stale binaries linked against wrong libs +pip cache purge 2>/dev/null || true + +# Install packages that have C extensions and must link against conda's libs. +# We use --no-binary to force compilation against the conda environment. +# The order matters: dependencies first, then Firedrake, then icepack. + +info " [1/5] Installing build dependencies ..." +# These are needed as build backends / build-time deps by libsupermesh +# and firedrake. We install them now so --no-build-isolation works +# (which ensures C extensions link against conda's MPI/PETSc, not +# some isolated copy). +# Install ALL build backends that Firedrake's dependency tree uses. +# --no-build-isolation means pip won't auto-install these, so we need +# them all present before building anything. +run_live $TAIL_LINES "Installing build backends" \ + pip install \ + setuptools wheel \ + scikit-build-core \ + meson-python meson ninja \ + hatchling hatch-vcs hatch-fancy-pypi-readme \ + flit-core \ + cython \ + pybind11 \ + petsctools +ok "Build dependencies installed." + +info " [2/5] Installing mpi4py, petsc4py, h5py (from source against conda libs) ..." +# These should already be installed by conda, but if pip needs to reinstall +# them (e.g. for Firedrake version constraints), force source builds. +# Usually conda's versions are fine and pip will skip them. + +info " [3/5] Installing libsupermesh ..." +run_live $TAIL_LINES "libsupermesh" \ + pip install --no-build-isolation libsupermesh || { + warn "libsupermesh pip install failed, trying from git ..." + run_live $TAIL_LINES "libsupermesh (git)" \ + pip install --no-build-isolation \ + "libsupermesh @ git+https://github.com/firedrakeproject/libsupermesh.git" +} +ok "libsupermesh installed." + +info " [4/5] Installing Firedrake (${FIREDRAKE_REF}) ..." +# Firedrake's pyproject.toml lists dependencies like petsc4py, mpi4py, h5py +# as git URLs. Conda already provides working versions of these, so we +# generate a pip constraints file that pins them to the conda-installed +# versions. This prevents pip from cloning & rebuilding petsc4py (which +# fails on release tags due to a setuptools dry_run incompatibility). +# +# Clone first so the user can see git progress (pip hides it entirely). +# Clones live in ~/.firedrake-conda/clones/ so they persist across +# environment recreations (conda env remove + re-create). +CLONE_DIR="${HOME}/.firedrake-conda/clones" +mkdir -p "${CLONE_DIR}" + +info " Cloning Firedrake (${FIREDRAKE_REF}) ..." +if [[ -d "${CLONE_DIR}/firedrake" ]]; then + info " (updating existing clone)" + # Unshallow if needed (shallow clones can't switch tags) + if [[ -f "${CLONE_DIR}/firedrake/.git/shallow" ]]; then + git -C "${CLONE_DIR}/firedrake" fetch --progress --unshallow --tags origin 2>&1 || \ + git -C "${CLONE_DIR}/firedrake" fetch --progress --tags origin 2>&1 + else + git -C "${CLONE_DIR}/firedrake" fetch --progress --tags origin 2>&1 + fi + git -C "${CLONE_DIR}/firedrake" checkout "${FIREDRAKE_REF}" 2>&1 + if [[ "${FIREDRAKE_REF}" == "main" ]]; then + git -C "${CLONE_DIR}/firedrake" reset --hard origin/main + fi +else + # Full clone for tags; shallow clone for main + if [[ "${FIREDRAKE_REF}" == "main" ]]; then + git clone --progress --depth 1 --branch main \ + https://github.com/firedrakeproject/firedrake.git \ + "${CLONE_DIR}/firedrake" 2>&1 + else + git clone --progress --branch "${FIREDRAKE_REF}" \ + https://github.com/firedrakeproject/firedrake.git \ + "${CLONE_DIR}/firedrake" 2>&1 + fi +fi + +# ── Generate pip constraints file ────────────────────────────────────── +# Pin every conda-provided package so pip doesn't try to rebuild them +# from git URLs in Firedrake's pyproject.toml. +info " Generating pip constraints for conda packages ..." +CONSTRAINTS="${CONDA_PREFIX}/constraints.txt" +python3 - <<'PYEOF' > "${CONSTRAINTS}" +import importlib.metadata, sys + +# Packages that conda provides and pip must not rebuild +CONDA_OWNS = [ + "petsc4py", "slepc4py", "mpi4py", "h5py", "numpy", "scipy", + "sympy", "cython", "pybind11", "pkgconfig", "rtree", + "scikit-build-core", "hatchling", "flit-core", "setuptools", +] + +for pkg in CONDA_OWNS: + try: + version = importlib.metadata.version(pkg) + print(f"{pkg}=={version}") + except importlib.metadata.PackageNotFoundError: + pass +PYEOF + +info " Constraints file:" +cat "${CONSTRAINTS}" | sed 's/^/ /' + +# ── Patch pyproject.toml to remove conda-provided deps ───────────────── +# Firedrake's release tags may list petsc4py, slepc4py, mpi4py, h5py as +# git URL deps (@ git+https://...) or version pins. Pip can't satisfy +# direct URL refs from conda, and tries to rebuild them — which fails +# (e.g. petsc4py's setuptools dry_run bug). We strip these deps since +# conda already provides them. +info " Patching pyproject.toml to skip conda-provided deps ..." +PYPROJECT="${CLONE_DIR}/firedrake/pyproject.toml" +python3 - "${PYPROJECT}" <<'PYEOF' +import re, sys + +path = sys.argv[1] +text = open(path).read() + +# Packages conda provides — remove any line in dependencies[] that +# starts with these names (handles version pins AND @ git+... URLs) +CONDA_PKGS = ["petsc4py", "slepc4py", "mpi4py", "h5py"] + +lines = text.split("\n") +patched = [] +removed = [] +for line in lines: + stripped = line.strip().strip('"').strip("'").strip(",").strip() + skip = False + for pkg in CONDA_PKGS: + # Match "petsc4py...", " petsc4py...", '"petsc4py...', etc. + if re.match(rf"^{re.escape(pkg)}(\s|>|<|=|@|;|$)", stripped): + skip = True + removed.append(stripped) + break + if not skip: + patched.append(line) + +if removed: + open(path, "w").write("\n".join(patched)) + for r in removed: + print(f" Removed: {r}") +else: + print(" (no conda-provided deps found to remove)") +PYEOF + +# ── pip install with constraints ─────────────────────────────────────── +info " pip install Firedrake (from local clone) ..." +run_live $TAIL_LINES "Firedrake" \ + pip install --no-build-isolation -c "${CONSTRAINTS}" "${CLONE_DIR}/firedrake" +ok "Firedrake installed." + +info " [5/5] Installing icepack ..." +info " Cloning icepack ..." +if [[ -d "${CLONE_DIR}/icepack" ]]; then + info " (updating existing clone)" + git -C "${CLONE_DIR}/icepack" pull --progress 2>&1 +else + git clone --progress --depth 1 \ + https://github.com/icepack/icepack.git \ + "${CLONE_DIR}/icepack" 2>&1 +fi + +info " pip install icepack (from local clone) ..." +run_live $TAIL_LINES "icepack" \ + pip install --no-build-isolation -c "${CONSTRAINTS}" "${CLONE_DIR}/icepack" +ok "icepack installed." + +# ── Step 6: Install activation script ──────────────────────────────────── + +info "Installing conda activation/deactivation scripts ..." + +ACTIVATE_DIR="${CONDA_PREFIX}/etc/conda/activate.d" +DEACTIVATE_DIR="${CONDA_PREFIX}/etc/conda/deactivate.d" +mkdir -p "${ACTIVATE_DIR}" "${DEACTIVATE_DIR}" + +cat > "${ACTIVATE_DIR}/firedrake-env-vars.sh" << 'ACTIVATE_EOF' +#!/usr/bin/env bash +# Set environment variables needed by Firedrake's JIT compiler. +# These are automatically set on `conda activate`. + +# Save previous values so we can restore on deactivate +export _FIREDRAKE_OLD_CC="${CC:-}" +export _FIREDRAKE_OLD_CXX="${CXX:-}" +export _FIREDRAKE_OLD_FC="${FC:-}" +export _FIREDRAKE_OLD_PETSC_DIR="${PETSC_DIR:-}" +export _FIREDRAKE_OLD_PETSC_ARCH="${PETSC_ARCH:-}" +export _FIREDRAKE_OLD_HDF5_MPI="${HDF5_MPI:-}" +export _FIREDRAKE_OLD_HDF5_DIR="${HDF5_DIR:-}" +export _FIREDRAKE_OLD_OMP_NUM_THREADS="${OMP_NUM_THREADS:-}" +export _FIREDRAKE_OLD_PYOP2_CFLAGS="${PYOP2_CFLAGS:-}" +export _FIREDRAKE_OLD_PYOP2_LDFLAGS="${PYOP2_LDFLAGS:-}" + +# PETSc +export PETSC_DIR="${CONDA_PREFIX}" +export PETSC_ARCH="" + +# Compilers — use MPI wrappers so JIT-compiled kernels link against +# the correct MPI and can be loaded alongside PETSc. +export CC=mpicc +export CXX=mpicxx +export FC=mpifort + +# HDF5 +export HDF5_MPI="ON" +export HDF5_DIR="${CONDA_PREFIX}" + +# Firedrake recommends single-threaded BLAS to avoid oversubscription +export OMP_NUM_THREADS=1 + +# PyOP2 JIT flags — conda's mpicc wraps x86_64-conda-linux-gnu-cc +# which PyOP2 doesn't recognise, so it falls back to a generic compiler +# class that omits -shared and -fPIC. These env vars inject the flags. +export PYOP2_CFLAGS="-fPIC -O2" +export PYOP2_LDFLAGS="-shared -Wl,-rpath,${CONDA_PREFIX}/lib" +ACTIVATE_EOF + +cat > "${DEACTIVATE_DIR}/firedrake-env-vars.sh" << 'DEACTIVATE_EOF' +#!/usr/bin/env bash +# Restore environment variables on `conda deactivate`. + +export CC="${_FIREDRAKE_OLD_CC}" +export CXX="${_FIREDRAKE_OLD_CXX}" +export FC="${_FIREDRAKE_OLD_FC}" +export PETSC_DIR="${_FIREDRAKE_OLD_PETSC_DIR}" +export PETSC_ARCH="${_FIREDRAKE_OLD_PETSC_ARCH}" +export HDF5_MPI="${_FIREDRAKE_OLD_HDF5_MPI}" +export HDF5_DIR="${_FIREDRAKE_OLD_HDF5_DIR}" +export OMP_NUM_THREADS="${_FIREDRAKE_OLD_OMP_NUM_THREADS}" +export PYOP2_CFLAGS="${_FIREDRAKE_OLD_PYOP2_CFLAGS}" +export PYOP2_LDFLAGS="${_FIREDRAKE_OLD_PYOP2_LDFLAGS}" + +unset _FIREDRAKE_OLD_CC +unset _FIREDRAKE_OLD_CXX +unset _FIREDRAKE_OLD_FC +unset _FIREDRAKE_OLD_PETSC_DIR +unset _FIREDRAKE_OLD_PETSC_ARCH +unset _FIREDRAKE_OLD_HDF5_MPI +unset _FIREDRAKE_OLD_HDF5_DIR +unset _FIREDRAKE_OLD_OMP_NUM_THREADS +unset _FIREDRAKE_OLD_PYOP2_CFLAGS +unset _FIREDRAKE_OLD_PYOP2_LDFLAGS + +# Clean up empty vars +[[ -z "$CC" ]] && unset CC +[[ -z "$CXX" ]] && unset CXX +[[ -z "$FC" ]] && unset FC +[[ -z "$PETSC_DIR" ]] && unset PETSC_DIR +[[ -z "$PETSC_ARCH" ]] && unset PETSC_ARCH +[[ -z "$HDF5_MPI" ]] && unset HDF5_MPI +[[ -z "$HDF5_DIR" ]] && unset HDF5_DIR +[[ -z "$OMP_NUM_THREADS" ]] && unset OMP_NUM_THREADS +[[ -z "$PYOP2_CFLAGS" ]] && unset PYOP2_CFLAGS +[[ -z "$PYOP2_LDFLAGS" ]] && unset PYOP2_LDFLAGS +DEACTIVATE_EOF + +chmod +x "${ACTIVATE_DIR}/firedrake-env-vars.sh" +chmod +x "${DEACTIVATE_DIR}/firedrake-env-vars.sh" + +ok "Activation scripts installed." + +# ── Step 7: Verify ──────────────────────────────────────────────────────── + +info "Running verification ..." + +python "${SCRIPT_DIR}/verify.py" && ok "All checks passed!" || { + warn "Some checks failed. See output above." + warn "You may need to debug linking issues." + warn "Try: conda activate ${ENV_ACTIVATE} && python verify.py" +} + +# ── Done ────────────────────────────────────────────────────────────────── + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +ok "Installation complete!" +echo "" +echo " To use Firedrake + icepack:" +echo "" +echo " conda activate ${ENV_ACTIVATE}" +echo " python -c 'import firedrake; import icepack; print(\"Ready!\")'" +echo "" +echo " Environment variables (PETSC_DIR, CC, etc.) are set automatically" +echo " when you activate the environment." +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/verify.py b/verify.py new file mode 100755 index 0000000..8024e40 --- /dev/null +++ b/verify.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +"""Verify that the Firedrake conda environment is correctly configured. + +Checks: + 1. Environment variables (PETSC_DIR, CC, etc.) + 2. Conda-provided libraries (PETSc, MPI, HDF5) + 3. Python imports (petsc4py, mpi4py, firedrake, icepack) + 4. JIT compilation (the most likely failure point) + 5. A minimal PDE solve + +Run with: + conda activate firedrake + python verify.py +""" + +import os +import sys +import importlib +import ctypes +from pathlib import Path + +PASS = "\033[92m✓\033[0m" +FAIL = "\033[91m✗\033[0m" +WARN = "\033[93m!\033[0m" +passed = 0 +failed = 0 +warned = 0 + + +def check(description, condition, detail=""): + global passed, failed + if condition: + print(f" {PASS} {description}") + passed += 1 + return True + else: + msg = f" {FAIL} {description}" + if detail: + msg += f" ({detail})" + print(msg) + failed += 1 + return False + + +def warn_check(description, detail=""): + global warned + msg = f" {WARN} {description}" + if detail: + msg += f" ({detail})" + print(msg) + warned += 1 + + +# ═══════════════════════════════════════════════════════════════════════════ +print("\n1. Environment variables") +print("─" * 40) + +conda_prefix = os.environ.get("CONDA_PREFIX", "") +check("CONDA_PREFIX is set", bool(conda_prefix), conda_prefix or "not set") + +petsc_dir = os.environ.get("PETSC_DIR", "") +check( + "PETSC_DIR points to conda prefix", + petsc_dir == conda_prefix, + f"PETSC_DIR={petsc_dir!r}, expected {conda_prefix!r}" +) + +petsc_arch = os.environ.get("PETSC_ARCH", "") +check( + "PETSC_ARCH is empty (prefix install)", + petsc_arch == "", + f"PETSC_ARCH={petsc_arch!r}" +) + +cc = os.environ.get("CC", "") +check("CC is set to mpicc", cc == "mpicc", f"CC={cc!r}") + +hdf5_mpi = os.environ.get("HDF5_MPI", "") +check("HDF5_MPI=ON", hdf5_mpi == "ON", f"HDF5_MPI={hdf5_mpi!r}") + +pyop2_cflags = os.environ.get("PYOP2_CFLAGS", "") +check("PYOP2_CFLAGS has -fPIC", "-fPIC" in pyop2_cflags, f"PYOP2_CFLAGS={pyop2_cflags!r}") + +pyop2_ldflags = os.environ.get("PYOP2_LDFLAGS", "") +check("PYOP2_LDFLAGS has -shared", "-shared" in pyop2_ldflags, f"PYOP2_LDFLAGS={pyop2_ldflags!r}") + +# ═══════════════════════════════════════════════════════════════════════════ +print("\n2. Shared libraries") +print("─" * 40) + +if conda_prefix: + lib_dir = Path(conda_prefix) / "lib" + ext = "dylib" if sys.platform == "darwin" else "so" + + for lib_name in ["libpetsc", "libmpi", "libhdf5"]: + lib_path = lib_dir / f"{lib_name}.{ext}" + if not lib_path.exists(): + # Try finding any matching file + matches = list(lib_dir.glob(f"{lib_name}*")) + found = len(matches) > 0 + detail = str(matches[0]) if found else f"no {lib_name}* in {lib_dir}" + else: + found = True + detail = str(lib_path) + check(f"{lib_name} found", found, detail) +else: + warn_check("Skipping library checks (CONDA_PREFIX not set)") + +# ═══════════════════════════════════════════════════════════════════════════ +print("\n3. Python imports") +print("─" * 40) + +modules = [ + ("numpy", "numpy"), + ("mpi4py", "mpi4py"), + ("petsc4py", "petsc4py"), + ("h5py", "h5py"), + ("ufl", "ufl"), + ("FIAT", "FIAT"), + ("finat", "finat"), + ("loopy", "loopy"), + ("tsfc", "tsfc"), + ("pyop2", "pyop2"), + ("firedrake", "firedrake"), + ("icepack", "icepack"), +] + +imported = {} +for display_name, module_name in modules: + try: + mod = importlib.import_module(module_name) + version = getattr(mod, "__version__", "?") + check(f"import {display_name}", True, f"v{version}") + imported[module_name] = mod + except Exception as e: + check(f"import {display_name}", False, str(e)) + +# ═══════════════════════════════════════════════════════════════════════════ +print("\n4. PETSc configuration") +print("─" * 40) + +if "petsc4py" in imported: + from petsc4py import PETSc as petsc + + # Check that PETSc has key external packages + has_mumps = petsc.Sys.hasExternalPackage("mumps") + check("PETSc has MUMPS", has_mumps) + + has_hypre = petsc.Sys.hasExternalPackage("hypre") + check("PETSc has hypre", has_hypre) + + has_superlu = petsc.Sys.hasExternalPackage("superlu_dist") + check("PETSc has SuperLU_dist", has_superlu) + + # Scalar type + import numpy as np + scalar = petsc.ScalarType + is_real = scalar in (float, np.float64, np.float32) + check("PETSc scalar type is real", is_real, f"scalar={scalar}") +else: + warn_check("Skipping PETSc config checks (import failed)") + +# ═══════════════════════════════════════════════════════════════════════════ +print("\n5. JIT compilation test") +print("─" * 40) + +if "firedrake" in imported: + try: + import firedrake + + # This triggers the full JIT pipeline: + # UFL form → TSFC → loopy → C code → compile → dlopen + mesh = firedrake.UnitSquareMesh(4, 4) + V = firedrake.FunctionSpace(mesh, "CG", 1) + u = firedrake.TrialFunction(V) + v = firedrake.TestFunction(V) + a = firedrake.inner(firedrake.grad(u), firedrake.grad(v)) * firedrake.dx + A = firedrake.assemble(a) + + check("JIT compilation works", True, "assembled a Laplacian matrix") + except Exception as e: + check("JIT compilation works", False, str(e)) + # Try to find and display the actual compiler error + import glob + err_files = sorted( + glob.glob("/tmp/pyop2-tempcache-*/**/*.err", recursive=True), + key=lambda f: Path(f).stat().st_mtime, + reverse=True, + ) + log_files = sorted( + glob.glob("/tmp/pyop2-tempcache-*/**/*.log", recursive=True), + key=lambda f: Path(f).stat().st_mtime, + reverse=True, + ) + for label, files in [("COMPILE ERRORS", err_files), ("COMPILE LOG", log_files)]: + if files: + content = Path(files[0]).read_text().strip() + if content: + print(f"\n ── {label}: {files[0]} ──") + for line in content.splitlines()[-20:]: + print(f" {line}") +else: + warn_check("Skipping JIT test (firedrake import failed)") + +# ═══════════════════════════════════════════════════════════════════════════ +print("\n6. Minimal PDE solve") +print("─" * 40) + +if "firedrake" in imported: + try: + import firedrake + from firedrake import * + + mesh = UnitSquareMesh(8, 8) + V = FunctionSpace(mesh, "CG", 1) + + u = TrialFunction(V) + v = TestFunction(V) + + x, y = SpatialCoordinate(mesh) + f = sin(pi * x) * sin(pi * y) + + a = inner(grad(u), grad(v)) * dx + L = f * v * dx + bc = DirichletBC(V, 0, "on_boundary") + + u_sol = Function(V) + solve(a == L, u_sol, bcs=bc) + + # Check the solution is reasonable + import numpy as np + u_data = u_sol.dat.data_ro + check( + "Poisson solve produces valid output", + np.all(np.isfinite(u_data)) and np.max(np.abs(u_data)) > 0, + f"max|u| = {np.max(np.abs(u_data)):.6f}", + ) + except Exception as e: + check("Poisson solve", False, str(e)) +else: + warn_check("Skipping PDE solve (firedrake import failed)") + +# ═══════════════════════════════════════════════════════════════════════════ +print("\n7. icepack smoke test") +print("─" * 40) + +if "icepack" in imported and "firedrake" in imported: + try: + import icepack + import firedrake + + # Check that core model classes exist and instantiate + model = icepack.models.IceShelf() + check("icepack.models.IceShelf()", True) + + model = icepack.models.IceStream() + check("icepack.models.IceStream()", True) + + # Check rate factor function + A = icepack.rate_factor(260.0) + check( + "icepack.rate_factor(260 K)", + A > 0, + f"A = {A:.4e}", + ) + except Exception as e: + check("icepack smoke test", False, str(e)) +else: + warn_check("Skipping icepack test (import failed)") + +# ═══════════════════════════════════════════════════════════════════════════ +print("\n" + "═" * 50) +print(f"Results: {PASS} {passed} passed {FAIL} {failed} failed {WARN} {warned} warnings") +print("═" * 50) + +sys.exit(0 if failed == 0 else 1)