Welcome to the new DOCS.vastdata.com. We've added new menus and a version selector, plus new persona and resource pages to help customers get started. Looking for a previous article? Use the docs search.

atanh

Prev Next

Applies to VAST Cluster 5.5.

Functions › Numeric and math functions › Hyperbolic functions. Returns the inverse hyperbolic tangent of x: the value whose hyperbolic tangent is x. x must be from -1 to 1.

Syntax

atanh(x)
Argument Type Description
x DOUBLE (any number) A value from -1 to 1. Integers, DECIMAL and FLOAT values are converted to DOUBLE.

Returns: DOUBLE.

Set up the examples

The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_numeric_atanh. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.

This block connects, defines the run() and plan() helpers, and creates a table of seven correlation coefficients, each from -1 to 1. Row 3 has no value:

import os
import re

import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb

BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_numeric_atanh"'

conn = dbapi.connect(
    driver=adbc_driver_vastdb.get_driver_path(),
    db_kwargs={
        "vast.db.endpoint": os.environ["QE_ENDPOINT"],
        "vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
        "vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
    },
    autocommit=True,
)

def server_message(e):
    """The server's message in a driver error, without the request ID or the echoed statement."""
    text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
    found = (re.search(r'message: "(.*?)", request_id', text, re.S)
             or re.search(r"description: (.*)\)", text, re.S))
    message = found.group(1) if found else text.strip().splitlines()[-1]
    message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
    message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
    return message.replace(BUCKET, "<bucket>")

def run(sql):
    """Run one statement. Print the rows it returns, or the error."""
    try:
        with conn.cursor() as cur:
            cur.execute(sql)
            rows = cur.fetchall() if cur.description else []
    except (adbc_driver_manager.Error, OSError) as e:
        print(f"{type(e).__name__}: {server_message(e)}")
    else:
        print(rows)

def plan(sql):
    """Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
    with conn.cursor() as cur:
        cur.execute("EXPLAIN " + sql)
        steps = cur.fetchall()[0][1]
    print("\n".join(line for line in steps.splitlines() if "Rows" not in line))

TABLE = f"{SCHEMA}.correlations"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, r DOUBLE)")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, 0.5), (2, -0.5), (3, NULL), (4, 0.0), (5, 1.0), (6, -1.0), (7, 0.9)")
[]
[]
[(7,)]

What it does

atanh(x) is the inverse of tanh: for x between -1 and 1 it returns the value y with tanh(y) = x. It's an odd function, so atanh(-0.5) is -atanh(0.5), -0.5493061443340548. Results are rounded to the nearest DOUBLE, so a round trip can be off in the last digit: atanh(tanh(1)) is 0.9999999999999999.

A common use is the Fisher z-transform of a correlation coefficient r: atanh(r) turns correlations into values you can average, and tanh() turns the average back into a correlation.

NULLs and edge cases

Input Result
NULL NULL
0 0.0
0.5, -0.5 0.5493061443340548, -0.5493061443340548
-0.0::DOUBLE -0.0
5e-324::DOUBLE (the smallest DOUBLE above 0) 5e-324
1 inf
-1 inf (positive infinity, not -inf)
'nan'::DOUBLE nan
2, -2, 1.0000000001, 'inf'::DOUBLE or '-inf'::DOUBLE Error: ATANH is undefined outside [-1,1]
'12' (a string) Converted to DOUBLE, then the same error
'abc' Error: Could not convert string 'abc' to DOUBLE
A BLOB, date, boolean, list or interval Error: No function matches the given name and argument types, for example 'atanh(DATE)'
An INTEGER, FLOAT or DECIMAL value Converted; the result is DOUBLE

The same cases, as the cluster returns them. They use the table and helpers from Set up the examples:

run("SELECT atanh(NULL), atanh(0), atanh(0.5), atanh(-0.5), atanh(-0.0::DOUBLE), atanh(5e-324::DOUBLE)")
run("SELECT atanh(1), atanh(-1), atanh('nan'::DOUBLE)")
run("SELECT typeof(atanh(1::INTEGER)), typeof(atanh(0.5::FLOAT)), typeof(atanh(0.5))")
run("SELECT atanh(2)")
run("SELECT atanh(1.0000000001)")
run("SELECT atanh('inf'::DOUBLE)")
run("SELECT atanh('12')")
run("SELECT atanh('abc')")
run("SELECT atanh(DATE '2026-10-16')")
[(None, 0.0, 0.5493061443340548, -0.5493061443340548, -0.0, 5e-324)]
[(inf, inf, nan)]
[('DOUBLE', 'DOUBLE', 'DOUBLE')]
OSError: ATANH is undefined outside [-1,1]
OSError: ATANH is undefined outside [-1,1]
OSError: ATANH is undefined outside [-1,1]
OSError: ATANH is undefined outside [-1,1]
OSError: Could not convert string 'abc' to DOUBLE
InternalError: Binder Error: No function matches the given name and argument types 'atanh(DATE)'. You might need to add explicit type casts.

Examples

The transform of each correlation. Row 5 (1.0) and row 6 (-1.0) both give inf:

run(f"SELECT id, r, atanh(r) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 0.5, 0.5493061443340548), (2, -0.5, -0.5493061443340548), (3, None, None), (4, 0.0, 0.0), (5, 1.0, inf), (6, -1.0, inf), (7, 0.9, 1.4722194895832204)]

The average of the correlations strictly between -1 and 1, by the Fisher z-transform: transform, average, transform back:

run(f"SELECT tanh(avg(atanh(r))) FROM {TABLE} WHERE r > -1 AND r < 1")
[(0.3522891589092123,)]

How it runs in VAST

The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). VAST DataBase can evaluate a comparison of a column with a constant, but not a function of a column, so atanh() of a column is always computed by the Query Engine:

  • In the SELECT list, the scan reads the column and the Query Engine computes atanh() for each row it returns.
  • In WHERE, the scan sends the column for every row, and a FILTER step in the Query Engine computes atanh() and keeps the rows that match.

The plan of a WHERE condition on atanh(r) shows the FILTER step above the scan:

plan(f"SELECT id FROM {TABLE} WHERE atanh(r) > 0.5")
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #1            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│      (atanh(r) > 0.5)     │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│             r             │
│             id            │
│                           │
└───────────────────────────┘

Make it faster

  • Compare the raw column instead. atanh() only grows, so atanh(r) > 0.5 keeps the same rows as r > tanh(0.5). A function of constants is computed once, before the scan, so that condition can be pushed down to VAST DataBase:
plan(f"SELECT id FROM {TABLE} WHERE r > tanh(0.5)")
run(f"SELECT id FROM {TABLE} WHERE r > tanh(0.5) ORDER BY id LIMIT 10")
┌───────────────────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│      Projections: id      │
│                           │
└───────────────────────────┘
[(1,), (5,), (7,)]

Limitations and errors

  • One value outside -1 to 1 cancels the whole query. The error is ATANH is undefined outside [-1,1], and it happens on any row, here row 7, where r * 2 is 1.8. Return NULL for those rows with CASE:
run(f"SELECT id, atanh(r * 2) FROM {TABLE} ORDER BY id LIMIT 10")
run(f"SELECT id, CASE WHEN abs(r * 2) <= 1 THEN atanh(r * 2) END FROM {TABLE} ORDER BY id LIMIT 10")
OSError: ATANH is undefined outside [-1,1]
[(1, inf), (2, inf), (3, None), (4, 0.0), (5, None), (6, None), (7, None)]
  • atanh(-1) is inf, not -inf. If the sign matters, test for -1 yourself:
run(f"SELECT id, CASE WHEN r = -1 THEN '-inf'::DOUBLE ELSE atanh(r) END FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 0.5493061443340548), (2, -0.5493061443340548), (3, None), (4, 0.0), (5, inf), (6, -inf), (7, 1.4722194895832204)]
  • Infinity isn't accepted. atanh('inf'::DOUBLE) and atanh('-inf'::DOUBLE) fail with the same error; NaN gives NaN.
  • One argument, numbers only. A second argument, a date, a boolean, a BLOB, a list or an interval fails with No function matches the given name and argument types. A string is converted to DOUBLE first, and fails with Could not convert string ... to DOUBLE if it isn't a number.

Similar functions

Function Computes Relation to atanh
tanh hyperbolic tangent its inverse: tanh(atanh(x)) gives back about x
asinh inverse hyperbolic sine takes any number
acosh inverse hyperbolic cosine takes values from 1 up
ln natural logarithm atanh(x) is, up to rounding, 0.5 * ln((1 + x) / (1 - x))
run("SELECT atanh(0.5), tanh(atanh(0.5)), 0.5 * ln((1 + 0.5) / (1 - 0.5)), asinh(0.5), acosh(1.5)")
[(0.5493061443340548, 0.49999999999999994, 0.5493061443340549, 0.48121182505960347, 0.9624236501192069)]

Clean up:

run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]

See also