#!/bin/sh
#------------------------------------------------------------------------------
# =========                 |
# \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
#  \\    /   O peration     | Website:  https://openfoam.org
#   \\  /    A nd           | Copyright (C) 2015-2025 OpenFOAM Foundation
#    \\/     M anipulation  |
#------------------------------------------------------------------------------
# License
#     This file is part of OpenFOAM.
#
#     OpenFOAM is free software: you can redistribute it and/or modify it
#     under the terms of the GNU General Public License as published by
#     the Free Software Foundation, either version 3 of the License, or
#     (at your option) any later version.
#
#     OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
#     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
#     for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
#
# Script
#     foamMonitor
#
# Description
#     Displays graphs to monitor data from files written by OpenFOAM, e.g.
#     by functionObjects. Requires gnuplot to be installed.
#
#------------------------------------------------------------------------------

usage() {
    cat<<USAGE

Usage: ${0##*/} [OPTION] <file>
options:
  -ascii    | -a         show the graph by printing ascii in the terminal
  -columns  | -c <cols>  display specified columns, comma-separated, e.g. "2,4"
  -flip     | -f         plot data on x-axis, independent variable on y-axis
  -help     | -h         print the usage
  -logscale | -l         plots data (y-axis) on log scale, e.g. for residuals
  -once     | -o         print a graph one time without refreshing
  -refresh  | -r <time>  refreshes display every <time> sec (default 10s)
  -size     | -s <size>  set the size of the output plot, format "640,480"
  -title    | -t <title> set the graph title (default "Data Monitoring")
  -yrange   | -y <range> sets data (y-axis) range, format "[0:1]"

Displays graphs to monitor data from files written by OpenFOAM, e.g. by
functionObjects. Requires gnuplot to be installed.

Graphs are plotted from column data files written by OpenFOAM, where column 1 is
the independent variable, e.g. time, plotted on the horizontal axis by default.
The other columns are the dependent data, plotted on the vertical axis by
default. The data can be plotted within a specified range, using the '-r' option
and/or on a log scale using the '-l' option. The default axes can be flipped by
the '-f' option (with data plotted on the horizontal axis).

In standard monitoring mode, graphs are refreshed (default: every 10s).
Graphs can also be printed one time without refreshing using the '-o' option.

Examples:
+ Monitor residuals on a log scale, running in background ('&')
    foamMonitor -l postProcessing/residuals/0/residuals.dat &
+ Monitor probed pressure values from column 2, also in background
    foamMonitor -c 2 postProcessing/probes/0/p &
+ Plot force coefficients from column 3 once, e.g. after the simulation stops
    foamMonitor -o -c 3 postProcessing/forces/0/forceCoeffs.dat

USAGE
}

error() {
    exec 1>&2
    while [ "$#" -ge 1 ]; do echo "$1"; shift; done
    usage
    exit 1
}

howMany() ( echo "$1" | awk '{ print NF }' )

isInt() {
    [ "$1" -eq "$1" ] 2> /dev/null
}

flip() {
    [ "$AXIS" = y ] && echo "$1" && return 0
    echo "$1" | awk -F: '{print $2 ":" $1}'
}

# Check gnuplot is installed
command -v gnuplot >/dev/null 2>&1 || error "Gnuplot not installed"

# Variables used globally
AXIS="y"

# Variables used locally
term="$(echo show term | gnuplot 2>&1 | sed -n \
's/[ ]\+terminal type is \([^ ]\+\) .*/\1/p') \
 1 linewidth 1.5 noraise"
size="default"
logscale="no"
range="none"
title="Data Monitoring"

refresh=10
once=""
columns=""

# Options
while [ "$#" -gt 0 ]
do
    case "$1" in
    -a | -ascii)
        term="dumb"
        shift 1
        ;;
    -c | -columns)
        [ "$#" -ge 2 ] || error "'$1' option requires an argument"
        columns=$(echo "$2" | tr , " ")
        for c in $columns
        do isInt "$c" || error "Argument of '$1' is not an integer: '$c'"
        done
        shift 2
        ;;
    -f | -flip)
        AXIS="x"
        shift 1
        ;;
    -h | -help)
        usage && exit 0
        ;;
    -l | -logscale)
        logscale="yes"
        shift 1
        ;;
    -o | -once)
        once="yes"
        shift 1
        ;;
    -r | -refresh)
        [ "$#" -ge 2 ] || error "'$1' option requires an argument"
        isInt "$2" || error "Argument of '$1' is not an integer: '$2'"
        refresh="$2"
        shift 2
        ;;
    -s | -size)
        [ "$#" -ge 2 ] || error "'$1' option requires an argument"
        size="$2"
        shift 2
        ;;
    -t | -title)
        [ "$#" -ge 2 ] || usage "'$1' option requires an argument"
        title=$2
        shift 2
        ;;
    -y | -yrange)
        [ "$#" -ge 2 ] || error "'$1' option requires an argument"
        range="$2"
        shift 2
        ;;
    -*)
        error "unknown option: '$*'"
        ;;
    *)
        break
        ;;
    esac
done

# Check input file
[ $# -eq 1 ] || error "Incorrect arguments specified"
[ -f "$1" ]  || error "File $1 does not exist"
file=$1

# Get keys from file header, removing space between the column names and units
keys=$(grep -E '^#' "$file" | sed 's/ *\[/\[/g' | tail -1 | tr -s " ")

# If no header, use 'Step' to describe column 1
[ "$keys" = "" ] && keys="# Step"

n_keys=$(howMany "$keys")
n_cols=$(tail -1 "$file" | awk '{ print NF }')

# With full column labels, n_keys = n_cols + 1, since it includes "#"
# If n_keys > n_cols + 1, REMOVE EXCESS keys
n_cols_pone=$(( n_cols + 1 ))
[ "$n_keys" -gt "$n_cols_pone" ] && \
    keys=$(echo "$keys" | cut -d" " -f1-$n_cols_pone)
n_keys=$(howMany "$keys")

# If n_keys < n_cols + 1, create keys 'data1', 'data2', etc
i=0
while [ "$n_keys" -le "$n_cols" ]
do
    i=$(( i + 1 ))
    keys="$keys data$i"
    n_keys=$(howMany "$keys")
done

# Remove starting "#"; set 'n_keys' finally
keys=$(echo "$keys" | cut -d " " -f2-)
n_keys=$(howMany "$keys")

# Set 'label' to column 1 key
label=$(echo "$keys" | awk '{print $1}')

# Set columns to display
[ "$columns" ] || columns="$(seq 2 "$n_keys")"

# Check the highest column is not out of range
highest_column="$(echo "$columns" | xargs -n 1 | \
                  sort -n | xargs | awk '{print $NF}')"
[ "$highest_column" -gt "$n_keys" ] && \
    error "Data does not extend to specified column '$highest_column'"

# Last column
# shellcheck disable=SC2086
last_col="$(echo $columns | awk '{print $NF}')"

# Generate the gnuplot commands to set up the plot
gnuplotSetup() {
    _ticFormat="%g"
    [ "$logscale" = yes ] && _ticFormat="%1.e"
    _otherAxis="$([ "$AXIS" = x ] && echo y || echo x)"

    printf "set term %s" "$term"

    # if '-size' specified, scale the font size to a sensible compromise
    # assumes approximate defaults of window size 600x400 and font size 12
    ! [ "$size" = default ] && \
        _fontsize="$(awk \
            -v x="$(echo "$size" | cut -d, -f1)" \
            -v y="$(echo "$size" | cut -d, -f2)" \
            'BEGIN {x/=600; y/=400; printf "%i", 12*(x < y ? x : y)**0.8}')" && \
        printf " size %s" "$size" && \
        printf " font \",%i\"" "$_fontsize"

    printf "\n"

    [ "$logscale" = yes ] && printf "set logscale %s\n" "$AXIS"
    [ "$range" = none ] || printf "set %srange %s\n" "$AXIS" "$range"
    printf "set %stics format \"%s\"\n" "$AXIS" "$_ticFormat"
    printf "set title \"%s\"\n" "$title"
    printf "set %slabel \"%s\"\n" "$_otherAxis" "$label"
}

# Generate the gnuplot plot command
gnuplotPlot() {
    echo "plot \\"
    for col in $columns
    do
        # Reinstate the space between the column name and units
        field="$(echo "$keys" | awk -v c="$col" '{print $c}' | sed 's/\[/ \[/')"
        plot_line="    \"$file\" using $(flip "1:${col}") with lines title \"$field\""
        [ "$col" -ne "$last_col" ] && plot_line="$plot_line, \\"
        echo "$plot_line"
    done
}

# Generate the full gnuplot command for a single plot
gnuplotOnce() {
    cat << EOF
$(gnuplotSetup)
$(gnuplotPlot)
EOF
}

# Generate the full gnuplot command for repeated plots
gnuplotRepeated() {
    cat << EOF
$(gnuplotSetup)
stop = 0
bind Close "stop = 1"
while !stop {
$(gnuplotPlot)
pause $refresh
}
EOF
}

# Call gnuplot. Run in the background unless this is a dumb/ascii terminal. If
# plotting once then keep the plot open until the user closes the window. If we
# ever add support for more terminals here, this might need extending.
if [ "$once" ]
then
    if [ "$term" = "dumb" ]
    then
        gnuplot << EOF
$(gnuplotOnce)
EOF
    else
        gnuplot << EOF &
$(gnuplotOnce)
pause mouse close
EOF
    fi
else
    if [ "$term" = "dumb" ]
    then
        gnuplot << EOF
$(gnuplotRepeated)
EOF
    else
        gnuplot << EOF &
$(gnuplotRepeated)
EOF
    fi
fi

#------------------------------------------------------------------------------
