\documentclass[a4paper,11pt]{article}
\usepackage[T1]{fontenc}
\usepackage[english]{babel}
\usepackage{lmodern}
\usepackage{microtype}
\usepackage{geometry}
\usepackage{xcolor}
\usepackage{booktabs}
\usepackage{array}
\usepackage{longtable}
\usepackage{tabularx}
\usepackage{graphicx}
\usepackage{hyperref}
\usepackage[noautomatic,quiet]{imakeidx}
\usepackage{listings}
\usepackage[most]{tcolorbox}
\usepackage{luacoolprop}

\geometry{margin=24mm}
\hypersetup{
  colorlinks=true,
  linkcolor=blue!55!black,
  urlcolor=blue!55!black,
  pdftitle={LuaCoolProp Reference Manual},
  pdfauthor={Christophe Jorssen},
  pdfsubject={Thermodynamic diagrams and CoolProp calculations for generic LuaTeX},
  pdfkeywords={LuaTeX, CoolProp, thermodynamics, PGFPlots, PH, PV, TS, HS, PT}
}
\pgfplotsset{compat=1.18}
% The reference contains deliberately dense labelled diagrams.  Keep every
% label candidate available at the plot boundary and hide low-priority labels
% if a showcase has no collision-free complete assignment.
\pgfplotsset{
  auto node allow outside=true,
  auto node failure mode=hide-low-priority
}

\definecolor{LCPBlue}{RGB}{28,70,140}
\definecolor{LCPGreen}{RGB}{0,110,95}
\definecolor{LCPOrange}{RGB}{190,95,20}
\definecolor{LCPGray}{RGB}{246,247,249}
\definecolor{LCPLightBlue}{RGB}{235,243,255}

\lstdefinestyle{lcptex}{basicstyle=\ttfamily\small,columns=fullflexible,keepspaces=true,breaklines=true}
\newtcblisting{LCPExample}[2][]{enhanced,breakable,colback=LCPGray,colframe=LCPBlue!70!black,listing only,listing engine=listings,listing options={style=lcptex},title={#2},fonttitle=\bfseries,left=1mm,right=1mm,top=1mm,bottom=1mm,#1}
\newtcblisting{LCPShowcase}[2][]{enhanced,colback=LCPGray,colframe=LCPBlue!70!black,
  listing side text,sidebyside align=top seam,sidebyside gap=4mm,
  lefthand ratio=.56,listing engine=listings,listing options={style=lcptex},
  title={#2},fonttitle=\bfseries,left=1mm,right=1mm,top=1mm,bottom=1mm,#1}
\newtcblisting{LCPShowcaseWide}[2][]{enhanced,breakable,colback=LCPGray,
  colframe=LCPBlue!70!black,listing and text,listing engine=listings,
  listing options={style=lcptex},title={#2},fonttitle=\bfseries,
  left=1mm,right=1mm,top=1mm,bottom=1mm,#1}
\newtcolorbox{LCPNote}[1][]{enhanced,breakable,colback=LCPLightBlue,colframe=LCPBlue!70!black,title={#1},fonttitle=\bfseries}
\newtcolorbox{LCPWarning}[1][]{enhanced,breakable,colback=orange!8,colframe=LCPOrange!85!black,title={#1},fonttitle=\bfseries}

\newcommand{\pkg}[1]{\texttt{#1}}
\newcommand{\key}[1]{\texttt{#1}}
\newcommand{\macro}[1]{\texttt{\string#1}}
\newcommand{\meta}[1]{\ensuremath{\langle\textit{#1}\rangle}}
\newcommand{\marg}[1]{\texttt{\char`\{}\meta{#1}\texttt{\char`\}}}
\newcommand{\oarg}[1]{\texttt{[}\meta{#1}\texttt{]}}

\makeindex[name=commands,title={Command Index},columns=2]
\makeindex[name=keys,title={Key and Value Index},columns=2]

\newenvironment{LCPCommandRef}[2]{%
  \index[commands]{#1@\protect\texttt{\protect\textbackslash#1}}%
  \begin{tcolorbox}[enhanced,breakable,colback=LCPLightBlue,
    colframe=LCPBlue!75!black,fonttitle=\bfseries,
    title={\texttt{\textbackslash#1}#2}]%
}{\end{tcolorbox}}

\newenvironment{LCPKeyRef}[4]{%
  \index[keys]{#2@\protect\texttt{#2}}%
  \begin{tcolorbox}[enhanced,breakable,colback=white,
    colframe=LCPGreen!70!black,fonttitle=\bfseries,
    title={\texttt{#2}=\meta{#3}\hfill\normalfont\small Default: \texttt{#4}}]%
}{\end{tcolorbox}}

\newcommand{\LCPKeyAlias}[3]{%
  \index[keys]{#2@\protect\texttt{#2} (alias)}%
  \par\smallskip\noindent\textit{Accepted alias: }
  \texttt{#2} forwards to \texttt{#3}.\par}

\newcommand{\LCPChoice}[4]{%
  \index[keys]{#2=#3@\protect\texttt{#2=#3}}%
  \item[\texttt{#3}] #4}

\newcommand{\LCPIndexValue}[2]{%
  \index[keys]{#1=#2@\protect\texttt{#1=#2}}}

\newcommand{\LCPKeyUse}[3]{%
  \par\smallskip\noindent\textit{Example: }
  \texttt{#2=\detokenize{#3}}.\par}

\newcommand{\LCPCodeUse}[1]{%
  \par\smallskip\noindent\textit{Example: }
  \texttt{\detokenize{#1}}.\par}

\newcommand{\LCPCommandAlias}[2]{%
  \index[commands]{#1@\protect\texttt{\protect\textbackslash#1} (alias)}%
  \par\smallskip\noindent\textit{Accepted alias: }
  \texttt{\textbackslash#1}.\par
  \noindent Target: \texttt{\textbackslash#2}.\par
  \noindent Example: \texttt{\textbackslash#1}\allowbreak
  \texttt{[fluid=R134a]}.\par}

\newcommand{\LCPKeyDoc}[6]{%
  \begin{LCPKeyRef}{#1}{#2}{#3}{#4}%
  #5\LCPKeyUse{#1}{#2}{#6}%
  \end{LCPKeyRef}}

\title{\pkg{luacoolprop}\\[1mm]\large LuaTeX access to CoolProp for PGFPlots thermodynamic diagrams}
\author{Christophe Jorssen\\\texttt{christophe.jorssen@gmail.com}}
\date{Version 1.1.0 --- 2026-09-18}

\begin{document}
\maketitle
\tableofcontents
\clearpage

\part{Tutorials}

\input{docs/r717-teaching-tutorial.tex}
\clearpage
\input{docs/r134a-air-conditioner-tutorial.tex}
\clearpage
\input{docs/butane-pv-tutorial.tex}
\clearpage
\input{docs/r1234yf-heat-pump-tutorial.tex}
\clearpage
\input{docs/methane-linde-tutorial.tex}

\part{User manual}

\section{Purpose and scope}

\pkg{luacoolprop} connects LuaTeX to the CoolProp shared library through LuaTeX
FFI.  Thermodynamic states and isolines are computed in Lua; graphical output is
emitted as PGFPlots code.  The package provides PH
(pressure--enthalpy), PV (pressure--specific-volume), TS
(temperature--entropy), HS (enthalpy--entropy), and PT
(pressure--temperature) diagrams for pure fluids.
PH uses a linear enthalpy
axis and logarithmic pressure axis; PV uses logarithmic axes for both specific
volume and pressure; TS uses linear mass-specific entropy and absolute
temperature axes; HS uses linear mass-specific entropy and enthalpy axes; PT
uses temperature and logarithmic pressure axes.

The package supports quality curves, isotherms, isentropes, isochores, TS
isenthalps, HS isobars,
thermodynamic process paths, named curves, per-curve overrides, and automatic
label placement delegated to \pkg{pgfplots-autonode}.

\begin{LCPWarning}[AI-assisted ``vibe coding'']
This package has been \emph{vibe-coded} with ChatGPT, an artificial-intelligence
system from OpenAI.  Substantial parts of its design, implementation,
documentation, and tests have been generated or revised with AI assistance.
AI-generated work can be convincing while still containing incorrect code,
incorrect thermodynamic assumptions, incomplete tests, or misleading
documentation.  Publication on CTAN is not a safety certification.  Inspect
the source and independently validate every result before relying on it,
especially in industrial, regulated, safety-critical, or high-consequence
work.  The maintainers remain responsible for reviewing releases and welcome
reproducible issue reports.
\end{LCPWarning}

Copyright \textcopyright\ 2026 Christophe Jorssen.  LuaCoolProp and its
included \pkg{pgfplots-autonode} library are distributed under the LaTeX
Project Public License 1.3c or later.  The maintenance status is
\emph{maintained}, and the Current Maintainer is Christophe Jorssen
(\href{mailto:christophe.jorssen@gmail.com}{\nolinkurl{christophe.jorssen@gmail.com}}).
CoolProp is an external,
MIT-licensed dependency and is not included in this work.

\subsection{How to cite CoolProp}

CoolProp asks users to cite its published reference and the project website
when reporting work based on its calculations.  The citation is:

\begin{quote}
Ian H. Bell, Jorrit Wronski, Sylvain Quoilin, and Vincent Lemort,
``Pure and Pseudo-pure Fluid Thermophysical Property Evaluation and the
Open-Source Thermophysical Property Library CoolProp,''
\emph{Industrial \& Engineering Chemistry Research} \textbf{53}(6),
2498--2508 (2014).  DOI:
\href{https://doi.org/10.1021/ie4033999}{10.1021/ie4033999}.
Project website: \url{https://coolprop.org/}.
\end{quote}

The project's \href{https://coolprop.org/citation.html}{citation page}
provides a BibTeX entry.  Cite the CoolProp version used for reproducibility
as well as this article; LuaCoolProp is an independent TeX interface, not an
official CoolProp component.

\begin{LCPNote}[Design principle]
LuaCoolProp deliberately separates thermodynamics from label placement.
Thermodynamic states, isolines, and process paths are computed in Lua; automatic
label placement is delegated to \pkg{pgfplots-autonode}.  This keeps the package
compatible with PGFPlots' survey/render lifecycle and avoids duplicating axis
geometry in Lua.
\end{LCPNote}

\section{Installation and runtime requirements}

A working document needs LuaTeX or LuaHBTeX with FFI enabled, PGF/TikZ, PGFPlots
1.18 or later,
and a CoolProp shared library.  The \pkg{pgfplots-autonode} library is part of
this project and is shipped with \pkg{luacoolprop}; it is not an external
runtime dependency.  CoolProp itself remains a separate shared-library
dependency.

LuaCoolProp does not set PGFPlots' \texttt{compat} key. Select the compatibility
level required by the complete document in its preamble. Automatic label hooks
are local to the axes created by \macro{\LCPDiagram}; a manually constructed
axis using low-level family commands must add
\texttt{auto node placement} when automatic labels are requested.

\subsection{Install the TeX files}

After publication, a TeX distribution can install the package through its
normal package manager: use \texttt{tlmgr install luacoolprop} with TeX Live,
or select \pkg{luacoolprop} in the MiKTeX Console.  CoolProp remains a separate
native dependency in both cases.

For a manual installation, unpack \texttt{luacoolprop.tds.zip} at the root of
a personal TEXMF tree.  TeX Live normally reports that directory with
\texttt{kpsewhich -var-value=TEXMFHOME}.  Refresh its filename database with
\texttt{mktexlsr} if the local installation uses one.  The browsing archive
\texttt{luacoolprop.zip} is intended for inspection and must not be substituted
for the TDS archive.

The shared library is typically named \texttt{libCoolProp.so} on Linux,
\texttt{libCoolProp.dylib} on macOS, and \texttt{CoolProp.dll} on Windows.
LuaCoolProp does not vendor CoolProp, and installing the CTAN package does not
install that library.  Obtain it separately from the official CoolProp project:
\begin{itemize}
  \item source repository: \url{https://github.com/CoolProp/CoolProp};
  \item shared-library instructions and release downloads:
        \url{https://coolprop.org/coolprop/wrappers/SharedLibrary/}.
\end{itemize}

\subsection{Security boundary and \texttt{--shell-escape}}

\begin{LCPWarning}[Compile trusted TeX sources only]
The required \texttt{--shell-escape} option is dangerous.  It permits a TeX
document, and every class, package, module, or file that it loads, to run
external programs with the permissions of the current user.  A malicious TeX
source can therefore read, modify, or disclose user-accessible data.  Compile
only sources and dependencies that you trust, preferably in an isolated build
directory or sandbox, without elevated privileges or unnecessary secrets.
Review downloaded examples before compiling them.  Never run the TeX engine as
an administrator or as \texttt{root}.
\end{LCPWarning}

LuaCoolProp requires full shell escape because the LuaTeX distribution exposes
its Lua FFI module only in a shell-escape-enabled typesetting process.  That FFI
module loads \texttt{libCoolProp} as native executable code and calls its C API.
LuaCoolProp itself does not invoke a compiler, download CoolProp, or issue shell
commands while typesetting; nevertheless, enabling the option enlarges the
capabilities of the \emph{entire} document.  Restricted shell escape is not
sufficient for this FFI mechanism.

The shared library is another trust boundary: load only a CoolProp binary that
you built from a reviewed revision or obtained from an official release.  Use
an absolute \texttt{LUACOOLPROP\_LIB} path so an unrelated library with the same
name cannot be selected earlier in a platform search path.

\subsection{Choose a compatible CoolProp binary}

The TeX engine process and the CoolProp library must use the same architecture:
normally \texttt{arm64} or \texttt{x86\_64} on macOS, \texttt{x86\_64} on
64-bit Linux, and \texttt{x64} on 64-bit Windows.  An architecture mismatch
usually appears as a dynamic-loader error before the first diagram is computed.

There are two supported approaches:
\begin{enumerate}
  \item download a precompiled library for the required operating system and
        architecture from CoolProp's official shared-library page; or
  \item build a pinned CoolProp tag or commit from source, as described below.
\end{enumerate}
For reproducible documents, record the exact CoolProp release or commit beside
the document sources.  Do not copy the CoolProp checkout, build tree, or binary
into the LuaCoolProp package tree or a CTAN archive.

Building requires Git, CMake, Ninja, Python, a C++ compiler, and the tools used
by CoolProp's submodules.  Clone outside the LuaCoolProp tree and initialize all
submodules.  The following example pins CoolProp \texttt{v8.0.0}; replace that
tag deliberately when adopting another reviewed release.

\begin{LCPExample}{Common source checkout on macOS or Linux}
git clone https://github.com/CoolProp/CoolProp.git ../coolprop-src
git -C ../coolprop-src fetch --tags
git -C ../coolprop-src checkout --detach v8.0.0
git -C ../coolprop-src submodule update --init --recursive
\end{LCPExample}

If \texttt{../coolprop-src} already exists, verify that it is the intended Git
checkout, fetch the requested revision, check it out, and repeat the recursive
submodule update.  A detached checkout is intentional here: it makes the exact
dependency visible and prevents an accidental branch update.

\subsection{macOS: download or compile the \texttt{.dylib}}

Install Apple's command-line developer tools and make Git, CMake, and Ninja
available.  For a reproducible build, first perform the pinned common checkout
above.  From a LuaCoolProp source or CTAN tree, the supplied script then reuses
that checkout, initializes its submodules, and performs the CMake and Ninja
build in sibling directories.  If the checkout does not yet exist, the script
can clone CoolProp's current default branch for exploratory use:

\begin{LCPExample}{Native macOS build}
./scripts/build-coolprop-macos.sh
\end{LCPExample}

On Apple Silicon the default is \texttt{arm64}, including when the script is
started from a translated shell.  Override it when the TeX engine has a
different architecture, or request a universal library explicitly:

\begin{LCPExample}{macOS architecture overrides}
COOLPROP_MACOS_ARCH=x86_64 ./scripts/build-coolprop-macos.sh
COOLPROP_MACOS_ARCH='arm64;x86_64' ./scripts/build-coolprop-macos.sh
\end{LCPExample}

The script prints the absolute path to \texttt{libCoolProp.dylib} as its final
line.  It deliberately leaves both CoolProp source and build products outside
the LuaCoolProp directory.  Package maintainers who intentionally adopt a new
CoolProp API may use \texttt{./scripts/update-coolprop.sh v8.0.0}; unlike the
build-only script, that maintainer tool also regenerates the marked FFI
declaration block in \pkg{luacoolprop.lua}, so end users should not run it as a
routine installation step.  A release tag may be written with or without its
leading \texttt{v}.

The equivalent explicit configuration, useful when the helper script is not
available, is:

\begin{LCPExample}{Manual arm64 macOS build}
cmake -S ../coolprop-src -B ../coolprop-build-macos-arm64 -G Ninja \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_OSX_ARCHITECTURES=arm64 \
  -DCOOLPROP_SHARED_LIBRARY=ON \
  -DCOOLPROP_STATIC_LIBRARY=OFF \
  -DCOOLPROP_OBJECT_LIBRARY=OFF
cmake --build ../coolprop-build-macos-arm64 --target CoolProp
export LUACOOLPROP_LIB="$PWD/../coolprop-build-macos-arm64/libCoolProp.dylib"
\end{LCPExample}

No system-wide installation is necessary.  Keeping an absolute environment
variable beside a pinned build makes it clear which thermodynamic library a
document used.

\subsection{Linux: download or compile the \texttt{.so}}

Install Git, CMake, Ninja, Python, a C++ build toolchain, and 7-Zip using the
distribution's package manager.  For example, Debian and Ubuntu package the
usual prerequisites as \texttt{git}, \texttt{cmake},
\texttt{ninja-build}, \texttt{build-essential}, \texttt{python3}, and
\texttt{p7zip-full}.  After the common source checkout above, configure and
build the shared-library target:

\begin{LCPExample}{Linux shared-library build}
cmake -S ../coolprop-src -B ../coolprop-build-linux -G Ninja \
  -DCMAKE_BUILD_TYPE=Release \
  -DCOOLPROP_SHARED_LIBRARY=ON \
  -DCOOLPROP_STATIC_LIBRARY=OFF \
  -DCOOLPROP_OBJECT_LIBRARY=OFF
cmake --build ../coolprop-build-linux --target CoolProp
export LUACOOLPROP_LIB="$PWD/../coolprop-build-linux/libCoolProp.so"
\end{LCPExample}

Again, copying the library to \texttt{/usr/local/lib} is optional and normally
unnecessary.  An unprivileged, versioned build directory plus the absolute
\texttt{LUACOOLPROP\_LIB} path is easier to audit and does not require
\texttt{sudo}.

\subsection{Windows: download or compile \texttt{CoolProp.dll}}

The simplest Windows installation is an official precompiled x64 DLL.  Extract
it to a stable directory outside the LuaCoolProp tree and set the environment
variable to its absolute path.  A source build needs Git, CMake, Python, and
either Ninja with a compatible C++ compiler or Visual Studio.  In PowerShell,
the Ninja workflow is:

\begin{LCPExample}{Windows PowerShell checkout and Ninja build}
git clone https://github.com/CoolProp/CoolProp.git ..\coolprop-src
git -C ..\coolprop-src fetch --tags
git -C ..\coolprop-src checkout --detach v8.0.0
git -C ..\coolprop-src submodule update --init --recursive
cmake -S ..\coolprop-src -B ..\coolprop-build-windows -G Ninja `
  -DCMAKE_BUILD_TYPE=Release `
  -DCOOLPROP_SHARED_LIBRARY=ON `
  -DCOOLPROP_STATIC_LIBRARY=OFF `
  -DCOOLPROP_OBJECT_LIBRARY=OFF
cmake --build ..\coolprop-build-windows --target CoolProp
$env:LUACOOLPROP_LIB =
  (Resolve-Path ..\coolprop-build-windows\CoolProp.dll).Path
\end{LCPExample}

With a Visual Studio generator, select the same architecture as TeX Live and
remember that it is a multi-configuration build:

\begin{LCPExample}{Windows PowerShell and Visual Studio 2022}
cmake -S ..\coolprop-src -B ..\coolprop-build-vs `
  -G "Visual Studio 17 2022" -A x64 `
  -DCOOLPROP_SHARED_LIBRARY=ON `
  -DCOOLPROP_STATIC_LIBRARY=OFF `
  -DCOOLPROP_OBJECT_LIBRARY=OFF
cmake --build ..\coolprop-build-vs --config Release --target CoolProp
$env:LUACOOLPROP_LIB =
  (Resolve-Path ..\coolprop-build-vs\Release\CoolProp.dll).Path
\end{LCPExample}

CMake versions and generators can place the DLL in a slightly different
configuration subdirectory.  If the shown path does not exist, locate the
newly built \texttt{CoolProp.dll} inside the selected build directory rather
than copying an unrelated DLL into it.  The PowerShell assignment lasts for the
current terminal.  In \texttt{cmd.exe}, use
\texttt{set "LUACOOLPROP\_LIB=C:\string\absolute\string\path\string\CoolProp.dll"}.

\subsection{Verify the runtime installation}

After setting \texttt{LUACOOLPROP\_LIB}, compile a minimal trusted document.
Use the command appropriate to the format, always in the same terminal in which
the environment variable was set:

\begin{LCPExample}{Minimal installation check}
% check-luacoolprop.tex
\documentclass{article}
\usepackage{luacoolprop}
\begin{document}
\LCPPHDiagram[fluid=R134a,quality values={0,0.5,1}]
\end{document}
\end{LCPExample}

\begin{LCPExample}{Run the LaTeX installation check}
lualatex --shell-escape check-luacoolprop.tex
\end{LCPExample}

The library path is consumed when the Lua module first loads.  If it changes,
start a fresh TeX process.  Loader errors should first be checked for an
incorrect absolute path, an architecture mismatch, or a missing dependent
runtime library.

The package is format-generic. Its public files have distinct roles:
\begin{itemize}
  \item \pkg{luacoolprop.tex} is the canonical generic TeX implementation;
  \item \pkg{luacoolprop.sty} is the LaTeX wrapper;
  \item \pkg{t-luacoolprop.tex} is the ConTeXt wrapper; and
  \item \pkg{p-luacoolprop.tex} is the plain TeX wrapper.
\end{itemize}

\subsection{LaTeX, plain TeX, and ConTeXt}

Each format uses its native wrapper and engine.  A LaTeX document loads
\pkg{luacoolprop.sty} and is compiled with LuaLaTeX:

\begin{LCPExample}{LaTeX wrapper}
\documentclass{article}
\usepackage{luacoolprop}
\begin{document}
\LCPPHDiagram[fluid=R134a]
\end{document}
\end{LCPExample}

\begin{LCPExample}{LaTeX command line}
lualatex --shell-escape myfile.tex
\end{LCPExample}

A plain TeX document loads \pkg{p-luacoolprop.tex} directly and is compiled
with LuaTeX, not with a LaTeX executable:

\begin{LCPExample}{Plain TeX wrapper}
\input p-luacoolprop.tex
\pgfplotsset{compat=1.18}
\LCPPHDiagram[fluid=R134a]
\bye
\end{LCPExample}

\begin{LCPExample}{Plain TeX command line}
luatex --shell-escape myfile.tex
\end{LCPExample}

A ConTeXt document selects the \texttt{t-} module explicitly.  This avoids an
ambiguous module search in a directory that also contains the \texttt{p-}
plain TeX wrapper.

\begin{LCPExample}{ConTeXt wrapper}
\usemodule[t][luacoolprop]
\pgfplotsset{compat=1.18}
\starttext
\LCPPHDiagram[fluid=R134a]
\stoptext
\end{LCPExample}

\begin{LCPExample}{ConTeXt command line}
context --luatex --shell-escape myfile.tex
\end{LCPExample}

The \texttt{--luatex} switch selects ConTeXt MkIV.  It is required because
LuaCoolProp uses LuaTeX FFI; LuaMetaTeX/LMTX does not provide that module.  The
ConTeXt wrapper loads the ConTeXt PGFPlots frontend before the generic layer.

\subsection{LaTeX number and unit formatting with \pkg{siunitx}}

The LaTeX wrapper formats generated unit-bearing values with
\macro{\qty}, unit-only axis labels with \macro{\unit}, and dimensionless
numbers such as vapour quality with \macro{\num}.  If \pkg{siunitx} is not yet
loaded, \pkg{luacoolprop} loads it with its standard defaults.  If it is already
loaded, the wrapper neither reloads it with options nor calls \macro{\sisetup};
the document's locale, decimal marker, grouping, rounding, font, and per-mode
choices are therefore preserved.  The user may also call \macro{\sisetup}
after loading \pkg{luacoolprop}.

\begin{LCPExample}{Configure siunitx before LuaCoolProp}
\documentclass[border=3mm]{standalone}
\usepackage[
  output-decimal-marker={,},
  group-separator={\,},
  group-minimum-digits=4,
  per-mode=symbol
]{siunitx}
\usepackage{luacoolprop}
\begin{document}
  \LCPPHDiagram[
    quality values={0,0.5,1},
    isotherm=true,temperature values={-20,0,20},
    labels=true]
\end{document}
\end{LCPExample}

The complete executable version is
\texttt{examples/siunitx-formatting.tex}.  Its title contains
\texttt{\string\num\{12345.6\}} and its generated curve labels demonstrate
that the same pre-existing decimal and unit policy is retained:

\IfFileExists{examples/siunitx-formatting.pdf}{%
  \begin{center}
    \includegraphics[width=.82\textwidth]{examples/siunitx-formatting.pdf}
  \end{center}
}{\begin{LCPWarning}[siunitx example PDF not built]
Run \texttt{scripts/build-examples.sh} before compiling the manual.
\end{LCPWarning}}

Numerical process-export macros intentionally remain plain decimal tokens so
that PGFPlots and TeX arithmetic can consume them.  When displaying such a
dimensionless macro in LaTeX, wrap it in \macro{\num}; for example,
\texttt{\string\num\{\string\CycleABToQuality\}}.  For a value with a unit,
use \macro{\qty}, for example
\texttt{\string\qty\{\string\CycleABToTemperatureK\}\{\string\kelvin\}}.
Plain TeX and ConTeXt keep the generic math-mode formatting and do not depend
on \pkg{siunitx}.

The input grammar is deliberately independent of this output formatting.
LuaCoolProp accepts only a full stop as an input decimal separator, even when
\pkg{siunitx} displays a comma.  Decimal floating-point and scientific
notation are supported, including a directly attached unit:
\texttt{pressure=1.2E5Pa}.  Thus \texttt{1,2bar} is an error, not another
spelling of \texttt{1.2bar}.  This fixed convention keeps the same source
unambiguous under LaTeX, plain TeX, ConTeXt, and every operating-system locale.

\subsection{Repository build workflow}

The reproducible build script recognizes the format from each example name:
\texttt{plain-*.tex} uses \texttt{luatex}, \texttt{context-*.tex} uses
ConTeXt MkIV, and every other example uses \texttt{lualatex}.  The manual uses
three \texttt{lualatex} passes so its table of contents, cross-references, and
two indexes are stable.

\begin{LCPExample}{Build every bundled document}
LUACOOLPROP_LIB=/path/to/libCoolProp.dylib ./scripts/build-examples.sh
\end{LCPExample}

Example logs are written to \texttt{examples/logs/}; manual logs are written
to \texttt{build-logs/}; each completed PDF is installed next to its source.
The script stops at the first engine failure or actionable TeX diagnostic.
Sandboxed builds may set \texttt{LUACOOLPROP\_TEX\_CACHE} to a writable
directory for the LuaLaTeX and plain LuaTeX font cache.

\section{Basic users: quick start}

\subsection{A fully automatic diagram}

The high-level command creates the complete TikZ picture and the PGFPlots axis.
The default units are \(h\) in \(\mathrm{kJ\,kg^{-1}}\) and \(p\) in bar.

\begin{LCPExample}{Minimal PH diagram}
\documentclass[border=3mm]{standalone}
\usepackage{luacoolprop}

\begin{document}
\LCPPHDiagram[fluid=R134a]
\end{document}
\end{LCPExample}

A more informative chart is obtained by enabling the main isoline families.
Label placement is delegated to \pkg{pgfplots-autonode}; \key{label
placement=autonode} is the default, but it is shown explicitly below.

\begin{LCPExample}{Automatic refrigeration-style diagram}
\LCPPHDiagram[
  fluid=R134a,
  isotherm=true,
  isentrope=true,
  isochore=true,
  quality step=0.1,
  temperature mode=preset,
  temperature preset=refrigeration,
  entropy mode=preset,
  entropy preset=refrigeration,
  specific volume mode=preset,
  specific volume preset=refrigeration,
  labels=true,
  sloped labels=true,
  label placement=autonode,
  axis options={
    auto node algorithm=repair,
    auto node bbox mode=oriented
  }
]
\end{LCPExample}

\subsection{Another fluid}

CoolProp fluid names can be used directly.  The useful isoline range depends on
the fluid and on the intended engineering application.

\begin{LCPExample}{Propane quick diagram}
\LCPPHDiagram[
  fluid=Propane,
  isotherm=true,
  isentrope=true,
  quality step=0.2,
  temperature values={-40,-20,0,20,40,60,80},
  entropy values={1.4,1.6,1.8,2.0,2.2},
  labels=true,
  sloped labels=true,
  label placement=autonode
]
\end{LCPExample}

\IfFileExists{examples/ph-propane-quick.pdf}{\begin{center}\includegraphics[width=.9\textwidth]{examples/ph-propane-quick.pdf}\end{center}}{}

\section{Diagram identifiers and stable API}

Diagram identifiers are uppercase coordinate pairs.  This release implements
\texttt{PH}, \texttt{PV}, and \texttt{TS}.  The generic dispatcher keeps the calling
convention independent of the coordinate pair:

\begin{LCPExample}{Generic PH dispatcher}
\LCPDiagram{PH}[fluid=R134a, isotherm=true]
\end{LCPExample}

\begin{LCPExample}{Generic PV dispatcher}
\LCPDiagram{PV}[fluid=R134a, isotherm=true]
\end{LCPExample}

\begin{LCPExample}{Generic TS dispatcher}
\LCPDiagram{TS}[fluid=R134a, isenthalp=true]
\end{LCPExample}

Inside an existing PGFPlots axis, use one of these generic forms:
\begin{itemize}
  \item \texttt{\string\LCPAddDiagramPlots\{PH\}} or
        \texttt{\string\LCPAddDiagramPlots\{PV\}} or
        \texttt{\string\LCPAddDiagramPlots\{TS\}};
  \item \texttt{\string\LCPAddDiagramFamily\{PV\}\{isotherm\}}; or
  \item \texttt{\string\LCPAddDiagramProcess\{PV\}}.
\end{itemize}
Each accepts its options in brackets after the mandatory diagram and family
identifiers.

Convenience commands put the same uppercase identifier between the
\texttt{LCP} prefix and the operation name.  Thus \macro{\LCPPHDiagram} and
\macro{\LCPPVDiagram}, \macro{\LCPTSDiagram}, and \macro{\LCPHSDiagram} are
equivalent to the generic calls with \texttt{PH}, \texttt{PV}, \texttt{TS},
and \texttt{HS}.  Additional implementations can add other identifiers
without changing the generic dispatch commands.

The canonical family names are \key{quality}, \key{isotherm},
\key{isentrope}, \key{isochore}, \key{isenthalp}, and \key{isobar}.  Each
diagram exposes only the families it can calculate. Public option names use
complete English property names. Examples include \key{quality step},
\key{temperature values}, and \key{specific volume unit}.

The canonical advanced key roots pair
\texttt{/luacoolprop/diagram} and \texttt{/luacoolprop/process} with an
uppercase \texttt{PH}, \texttt{PV}, \texttt{TS}, \texttt{HS}, or
\texttt{PT} branch.

\begin{LCPNote}[Accepted aliases]
The public aliases include:
\begin{itemize}
  \item \macro{\LCPPhDiagram}, \macro{\LCPAddPHIsoQuality}, and the mixed-case
  \texttt{Ph} family commands;
  \item the alternate \texttt{/luacoolprop/ph diagram} and
  \texttt{/luacoolprop/ph process} paths; and
  \item plural family switches and abbreviated property keys.
\end{itemize}
The canonical names are recommended.  Their vocabulary is shared by all
registered diagram types.
\end{LCPNote}

\section{Advanced users: manual PGFPlots axes}

Advanced users can write the PGFPlots axis by hand and add each isoline family
separately.  The fluid can be declared at the axis level with \key{lcp fluid}.
Each family macro emits its \macro{\addplot} data immediately, followed by the
corresponding \macro{\pgfplotsautonode} label command when labels are enabled.
PGFPlots therefore sees the data during its survey phase and can still compute
axis limits natively.  Because this is a manually created axis, the
\key{auto node placement} lifecycle key is required whenever automatic labels
are requested; omitting it produces an explicit warning.

\begin{LCPExample}{Manual construction in an axis}
\begin{tikzpicture}
\begin{axis}[
  lcp fluid=R134a,
  xlabel={$h/{\rm kJ\,kg^{-1}}$},
  ylabel={$p/{\rm bar}$},
  ymode=log,
  grid=both,
  auto node placement,
  width=14cm,
  height=9cm,
  auto node algorithm=repair,
  auto node bbox mode=oriented
]
  \LCPAddPHQuality[
    quality values={0,0.1,0.3,0.5,0.7,0.9,1},
    labels=true,
    sloped labels=true
  ]
  \LCPAddPHIsotherms[
    temperature values={-30,-10,10,30,50,70,90},
    labels=true,
    sloped labels=true
  ]
  \LCPAddPHIsentropes[
    entropy values={1.0,1.2,1.4,1.6,1.8},
    labels=true,
    sloped labels=true
  ]
  \LCPAddPHIsochores[
    specific volume values={0.001,0.002,0.005,0.01,0.02,0.05,0.1},
    labels=true,
    sloped labels=true
  ]
\end{axis}
\end{tikzpicture}
\end{LCPExample}

\subsection{Curve-family macros}

\begin{center}
\begin{tabular}{ll}
\toprule
Macro & Purpose \\
\midrule
\macro{\LCPAddPHQuality} & add \(Q=\mathrm{constant}\) curves \\
\macro{\LCPAddPHIsotherms} & add \(T=\mathrm{constant}\) curves \\
\macro{\LCPAddPHIsentropes} & add \(s=\mathrm{constant}\) curves \\
\macro{\LCPAddPHIsochores} & add \(v=\mathrm{constant}\) curves \\
\macro{\LCPAddPHProcess} & add one thermodynamic process segment \\
\bottomrule
\end{tabular}
\end{center}

\subsection{Value grids}

All isoline families accept preset, linear, logarithmic, and explicit value
lists.  Select a preset with both \key{temperature mode=preset} (or the
corresponding family mode) and the desired \key{... preset} name.  Explicit
lists are often best for publication-quality diagrams and take precedence over
the mode.

\begin{LCPExample}{Explicit value lists}
\LCPAddPHQuality[quality values={0,0.25,0.5,0.75,1}]
\LCPAddPHIsotherms[temperature values={-20,0,20,40,60,80}]
\LCPAddPHIsentropes[entropy values={1.0,1.2,1.4,1.6,1.8}]
\LCPAddPHIsochores[specific volume values={0.001,0.002,0.005,0.01,0.02}]
\end{LCPExample}

Isochores use density internally because CoolProp's high-level interface accepts
mass density directly.  LuaCoolProp converts \(v\) to \(\rho=1/v\).

\section{Automatic labels with pgfplots-autonode}

\pkg{pgfplots-autonode} is the only automatic label-placement backend.  It is
fully part of this project and is implemented as the autonomous PGFPlots
library \key{autonode}; LuaCoolProp loads that library automatically.  The
canonical implementation file is
\texttt{pgflibrarypgfplots.autonode.code.tex}, with numerical algorithms in
\texttt{pgfplots-autonode.lua}.  The LaTeX package and direct code loader
are alternate entry points.

\subsection{Loading the autonomous library}

To use autonode without LuaCoolProp, first load the PGFPlots frontend for the
current format and then load the library.  In LaTeX:

\begin{LCPExample}{Autonode library in LaTeX}
\usepackage{pgfplots}
\usepgfplotslibrary{autonode}
\end{LCPExample}

In plain TeX:

\begin{LCPExample}{Autonode library in plain TeX}
\input pgfplots.tex
\usepgfplotslibrary{autonode}
\end{LCPExample}

In ConTeXt MkIV:

\begin{LCPExample}{Autonode library in ConTeXt}
\usemodule[t][pgfplots]
\usepgfplotslibrary{autonode}
\end{LCPExample}

The files \texttt{autonode-latex-basic.tex},
\texttt{plain-autonode-basic.tex}, and \texttt{context-autonode-basic.tex} in
the \texttt{examples} directory are executable tests of these three loading
paths.

\subsection{Labelling a PGFPlots path}

The command \macro{\pgfplotsautonode} is a PGFPlots trailing path command.  Its
option argument is mandatory, even when empty:

\begin{LCPExample}{Standalone autonode label}
\begin{axis}[
  auto node placement,
  auto node algorithm=repair
]
  \addplot[domain=0:4] {x}
    \pgfplotsautonode[preferred pos=0.65,sloped=true]{$y=x$};
\end{axis}
\end{LCPExample}

For each labelled thermodynamic curve, LuaCoolProp emits the same structure:

\begin{LCPExample}{Generated structure}
\addplot[<curve style>] coordinates {<points>}
  \pgfplotsautonode[<autonode keys>]{<label text>};
\end{LCPExample}

The label-placement problem is therefore solved by \pkg{pgfplots-autonode} using
PGFPlots path geometry, rather than by approximate Lua-side boxes.  This is more
consistent with PGFPlots' survey/visualization lifecycle and avoids duplicating
axis-transform logic in Lua.

\begin{LCPExample}{Dense autonode-labelled PH chart}
\begin{tikzpicture}
\begin{axis}[
  lcp fluid=R134a,
  xlabel={$h/{\rm kJ\,kg^{-1}}$},
  ylabel={$p/{\rm bar}$},
  ymode=log,
  grid=both,
  auto node placement,
  auto node algorithm=repair,
  auto node bbox mode=oriented,
  auto node failure mode=error,
  width = 20cm,
  height = 15cm,
]
  \LCPAddPHQuality[
    quality values={0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1},
    labels=true,
    sloped labels=true
  ]
  \LCPAddPHIsotherms[
    temperature values={-20,0,20,40,60,80,100},
    labels=true,
    sloped labels=true
  ]
  \LCPAddPHIsentropes[
    entropy values={1.0,1.2,1.4,1.6,1.8},
    labels=true,
    sloped labels=true
  ]
  \LCPAddPHIsochores[
    specific volume values={0.001,0.002,0.005,0.01,0.02,0.05,0.1},
    labels=true,
    sloped labels=true
  ]
\end{axis}
\end{tikzpicture}
\end{LCPExample}

\IfFileExists{examples/ph-r134a-autonode-full-network.pdf}{\begin{center}\includegraphics[width=\textwidth]{examples/ph-r134a-autonode-full-network.pdf}\end{center}}{}

\subsection{LuaCoolProp keys forwarded to autonode}

\begin{center}
\begin{tabularx}{\textwidth}{>{\raggedright\arraybackslash}p{.43\textwidth}X}
\toprule
Key & Meaning \\
\midrule
\key{autonode candidates} & number of candidate positions sampled on each plot path \\
\key{autonode candidate strategy} & distribution strategy for candidates \\
\key{autonode clearance} & additional clearance around measured labels \\
\key{autonode inner sep} & node padding used by the backend \\
\key{autonode normal shift} & shift normal to the sampled curve \\
\key{autonode preferred weight} & penalty for moving away from the preferred position \\
\key{autonode overlap weight} & multiplier for label-label overlap area during repair and local search \\
\bottomrule
\end{tabularx}
\end{center}

Native \pkg{pgfplots-autonode} axis keys such as \key{auto node algorithm},
\key{auto node bbox mode}, \key{auto node failure mode}, and \key{auto node
border margin} can be passed directly to the PGFPlots axis, or through
\key{axis options} when using \macro{\LCPPHDiagram}.

For a pair of overlapping labels, the solver uses the arithmetic mean of their
two overlap weights.  This symmetric cost can trade displacement against a
residual collision in repair and local-search modes; the final failure policy
still decides whether such a collision is accepted, reported, or hidden.

Sloped autonode labels are kept upright by default.  Use \key{label allow upside
down=true} only when the raw path orientation is desired.

\section{Curve names and manual overrides}

Each curve receives a stable name.  Examples are:

\begin{LCPExample}{Generated curve identifiers}
lcp-ph-q-0p4
lcp-ph-T-20C
lcp-ph-s-1p4kJkgK
lcp-ph-v-0p005m3kg
\end{LCPExample}

Manual corrections are expressed as key-value overrides.  With autonode these
corrections are translated to preferred positions, admissible intervals, node
styles, or visibility flags.

\begin{LCPExample}{Manual label and curve overrides}
label pos for={lcp-ph-T-0C}{0.72}
label fixed for={lcp-ph-T-0C}{true}
label style for={lcp-ph-T-0C}{fill=yellow!20,draw=yellow!50!black}
label show for={lcp-ph-v-0p005m3kg}{false}
curve style for={lcp-ph-v-0p01m3kg}{very thick,orange!90!black}
\end{LCPExample}

\section{Thermodynamic process paths}

The macro \macro{\LCPAddPHProcess} draws a process segment on top of an existing
PH diagram.  The type determines the conserved property and LuaCoolProp validates
whether enough information has been provided.
Arrow-tip styles in the following snippets require
\texttt{\string\usetikzlibrary\{arrows.meta\}} in the document preamble.

\begin{LCPExample}{Isentropic compression from saturated vapor}
\LCPAddPHProcess[
  fluid=R134a,
  type=isentrope,
  from={pressure=1bar,quality=1},
  to={pressure=10bar},
  style={very thick,-Latex},
  color=black,
  label={$1\to2$},
  mark endpoints=true
]
\end{LCPExample}

\begin{LCPExample}{Constant-quality segment}
\LCPAddPHProcess[
  fluid=R134a,
  type=quality,
  quality=0.5,
  from={temperature=50C},
  to={temperature=-20C},
  style={very thick,-Latex},
  color=purple!80!black,
  label={$Q=0.5$}
]
\end{LCPExample}

If an endpoint is under-specified, the error message indicates the missing
thermodynamic property.  For instance, \key{from=
\{quality=1\}} is not enough to define a saturated state; the endpoint also
needs a saturation coordinate such as \key{pressure=1bar} or
\key{temperature=0C}.

\part{Complete TeX reference}

\input{docs/luacoolprop-tex-reference.tex}
\input{docs/pgfplots-autonode-reference.tex}

\part{Developer reference}

\section{Architecture}

The package is split into three layers:

\begin{center}
\begin{tabularx}{\textwidth}{>{\raggedright\arraybackslash}p{.43\textwidth}X}
\toprule
File & Responsibility \\
\midrule
\texttt{luacoolprop.lua} & FFI declarations, CoolProp wrappers, state resolution, adaptive sampling, curve-record construction, process validation, and PGFPlots code emission \\
\texttt{luacoolprop.tex} & generic TeX interface, \pkg{pgfkeys} trees, PGFPlots integration, wrappers around Lua calls \\
\texttt{luacoolprop.sty} & LaTeX wrapper loading PGFPlots and the generic TeX layer \\
\texttt{t-luacoolprop.tex} & ConTeXt wrapper loading the ConTeXt PGFPlots frontend and the generic TeX layer \\
\texttt{p-luacoolprop.tex} & plain TeX wrapper loading the generic TeX layer \\
\texttt{pgflibrarypgfplots.\allowbreak autonode.\allowbreak code.tex} & format-generic PGFPlots library for measuring, registering, and drawing automatic labels \\
\texttt{pgfplots-autonode.lua} & numerical candidate generation, collision detection, and label assignment for the autonode library \\
\bottomrule
\end{tabularx}
\end{center}

The Lua module is format-neutral.  TeX-facing functions live under \texttt{M.tex};
diagram generators live under \texttt{M.diagram}.  Internal helpers use
\texttt{snake\_case}; TeX macros use the \macro{\LCP...} prefix; PGF keys are
lowercase words separated by spaces.

\section{PGFPlots lifecycle and label emission}

PGFPlots first collects plot data, determines axis limits and transformations,
and renders the plots only at the end of the axis.  LuaCoolProp respects this
lifecycle: curve-family macros emit the full \macro{\addplot} coordinate list
immediately.  When a curve has a label, the label is emitted as a trailing
\macro{\pgfplotsautonode} command attached to that plot.  No Lua-side automatic
placement pass is run for an axis that enables automatic placement.

This design has two consequences:
\begin{itemize}
  \item PGFPlots autoscaling remains native because all numeric coordinates are
        available during the survey phase.
  \item Collision avoidance uses final PGFPlots geometry through
        \pkg{pgfplots-autonode}, rather than an approximate independent model in
        Lua.
\end{itemize}

\section{Adaptive curve sampling}

Isolines are sampled adaptively in display-like coordinates: \((h,\log p)\)
for PH, \((\log v,\log p)\) for PV, \((s,T)\) for TS, and \((s,h)\)
for HS.
The sampler starts from a coarse grid in \(\log p\), evaluates the midpoint of
each segment, compares it with the midpoint of the straight chord, and recursively
subdivides until the error estimate is below a family-specific tolerance or the
maximum depth is reached.  Isothermal two-phase plateaus are special: only the
saturated vapor and saturated liquid endpoints are emitted for the horizontal
plateau.

Constant-quality curves receive a second adaptive pass when their pressure
range enters the upper half of the subcritical interval.  This pass uses
\(\log_{10}\epsilon\), where \(\epsilon=(p_c-p)/p_c\), as its independent
variable over the upper half of the saturation-pressure range.  It applies the
same family tolerance and the projection's own display-coordinate error metric.
Consequently the rapidly changing PH, PV, TS, and HS saturation branches and
the PT coexistence locus are
resolved without a fluid-specific critical-pressure cutoff.  The common exact
critical-limit record is then appended separately because quality itself is
undefined at \(p_c\).

\section{Thermodynamic state resolution}

Endpoint and process specifications are parsed as key-value lists.  Pressures,
temperatures, enthalpies, entropies, densities, specific volumes, and qualities
are normalised to SI units before calls to CoolProp.  Saturated states require a
quality and one saturation coordinate, normally pressure or temperature.  General
single-phase states are resolved from two independent intensive properties.  When
a process type declares a conserved property, LuaCoolProp first tries to read the
property from the process keys; if it is absent, it attempts to infer it from a
sufficiently defined endpoint.

Input is strict at this boundary.  Numbers must be finite decimal values and
may use an \texttt{e} or \texttt{E} exponent; only the full stop is a decimal
separator.  Unit suffixes, boolean values, grid modes, presets, and every
numeric-list element are validated in full.  Invalid and reversed explicit
bounds are reported rather than repaired silently.

For an overdetermined state, the resolver freezes one independent CoolProp
input pair, computes the canonical state from that pair, and compares every
other explicitly supplied property with the result.  The completed endpoints
are then compared once more with the conserved process property.  A mismatch
reports the supplied and resolved SI values, their absolute difference, and
the applicable absolute/relative tolerance; consequently a valid isobar can
never contain endpoints at two different pressures.

\section{Naming conventions}

The Lua source uses \texttt{snake\_case} for local functions and table fields.
Public TeX-facing Lua entry points are grouped under \texttt{M.tex}; diagram
algorithms are grouped under \texttt{M.diagram}.  User macros use the
\macro{\LCP...} prefix.  User-level keys are lowercase English words separated by
spaces.  Generated PGFPlots curve names are stable ASCII identifiers, for
example \texttt{lcp-ph-q-0p5}, \texttt{lcp-ph-T-40C},
\texttt{lcp-ph-s-1p6kJkgK}, and \texttt{lcp-ph-v-0p01m3kg}.


\section{Lua API: scope, contracts, and safe use}

The public Lua API intentionally separates three concerns.  Top-level functions
load and wrap CoolProp's C ABI.  Functions under \texttt{M.diagram} compute
thermodynamic data and return Lua tables or PGFPlots strings.  Functions under
\texttt{M.tex} are the narrow output bridge used by the generic TeX layer.  A
standalone Lua program should normally use only the first two layers.

The complete field-by-field reference is also maintained as
\texttt{docs/luacoolprop-lua-api.md}.  Public functions carry LDoc comments next
to their implementations in \texttt{luacoolprop.lua}; the Markdown reference
adds longer examples, extension rules, and a contributor checklist.  The file
\texttt{examples/lua-api-smoke.lua} executes the principal contracts through
\texttt{texlua} as part of \texttt{scripts/build-examples.sh}.

\subsection{Loading the module and identifying both versions}

Use a local module variable.  Loading the Lua module does not by itself require
a hard-coded library path; the first property call triggers the same search as
\texttt{M.load\_library()}.

\begin{LCPExample}{Load LuaCoolProp from texlua}
local lcp = require("luacoolprop")

local _, loaded_from = lcp.load_library()
io.write("LuaCoolProp ", lcp._VERSION, "\n")
io.write("CoolProp ", lcp.version(), "\n")
io.write("CoolProp revision ", lcp.gitrevision(), "\n")
io.write("Loaded from ", loaded_from, "\n")
\end{LCPExample}

\texttt{M.\_VERSION} describes this package.  In contrast,
\texttt{M.version()} and \texttt{M.gitrevision()} query the loaded external
CoolProp library.  Reproducible reports should record all three values plus the
result of \texttt{M.loaded\_library()}.

The library search order is deterministic: an explicit path, then
\texttt{LUACOOLPROP\_LIB}, the secondary \texttt{COOLPROP\_LIB}, platform names in
the current directory, the active TeX resolver, and finally the operating
system's dynamic loader.  The first successful handle is cached.  A later call
with another path does not replace it; select the library before starting the
engine when exact dependency control matters.

\begin{LCPWarning}[Raw FFI handle]
After loading, \texttt{M.C} contains the raw FFI handle.  It is an implementation
escape hatch, not a stable LuaCoolProp interface.  Prefer documented wrappers:
they validate Lua argument types, convert C strings, and translate reported C
errors into contextual Lua errors.
\end{LCPWarning}

\subsection{SI-unit contract}

Low-level calls preserve CoolProp's SI convention.  No implicit display-unit
conversion occurs at this boundary.

\begin{center}
\begin{tabularx}{\textwidth}{>{\raggedright\arraybackslash}p{.25\textwidth}>{\raggedright\arraybackslash}p{.18\textwidth}X}
\toprule
Quantity & Lua field & Unit \\
\midrule
pressure & \texttt{p} & Pa \\
temperature & \texttt{T} & K \\
mass enthalpy & \texttt{h} & J/kg \\
mass entropy & \texttt{s} & J/(kg K) \\
mass density & \texttt{rho} & kg/m\textsuperscript{3} \\
specific volume & \texttt{v} & m\textsuperscript{3}/kg \\
vapor quality & \texttt{q} & dimensionless, normally from 0 to 1 \\
\bottomrule
\end{tabularx}
\end{center}

Diagram point records additionally contain scaled plot coordinates.  For PH,
the defaults give \texttt{x=h*1e-3} in kJ/kg and \texttt{y=p*1e-5} in bar.  For
PV, \texttt{x=v} is in m\textsuperscript{3}/kg and the same pressure scale is
used.  TS defaults to \texttt{x=s*1e-3} and \texttt{y=T}; HS defaults to
\texttt{x=s*1e-3} and \texttt{y=h*1e-3}.  Semantic axis-unit options couple
these conversions to their labels, including the additive Celsius offset on a
linear TS ordinate.  Raw SI fields remain present on every point and should be
used for thermodynamic work.

\subsection{Errors and recovery boundaries}

Load failures, invalid public arguments, reported CoolProp errors, and invalid
process specifications raise Lua errors.  They do not return an ambiguous
\texttt{nil,error} pair.  Catch an error only where recovery or additional
context is possible.  In particular, wrapping every property call in
\texttt{pcall} would conceal invalid fluid models.  The adaptive sampler is an
intentional exception: it protects individual sample evaluations because a
valid curve can cross a region where one property pair is undefined.

\begin{LCPExample}{Catch an error at an application boundary}
local lcp = require("luacoolprop")

local ok, result = xpcall(function()
  return lcp.propsSI("H", "P", 1e5, "Q", 1, "R134a")
end, debug.traceback)

if not ok then
  io.stderr:write(result, "\n")
  os.exit(1)
end

io.write(string.format("h = %.3f kJ/kg\n", result * 1e-3))
\end{LCPExample}

Diagram entry points accept only CoolProp fluids reported as pure.  They reject
mixtures before applying algorithms which assume one triple point, one critical
point, and one saturation dome; low-level wrappers remain mixture-capable.
Enthalpy and entropy are relative properties.  Select \texttt{DEF},
\texttt{IIR}, \texttt{ASHRAE}, or \texttt{NBP} with
\texttt{set\_reference\_state} during initialization.  The first diagram call
locks that choice for its fluid, and process metadata records it.  Energy
balances should use enthalpy differences computed under one convention.

Vapor quality is defined only in the two-phase region below the critical
state.  When a plotted quality curve reaches the critical pressure, LuaCoolProp
adds a common graphical limiting record evaluated from $T_c$ and $\rho_c$.
That record is marked \texttt{critical\_limit=true} and
\texttt{quality\_defined=false}; it has no thermodynamic quality value.  Thus
the shared endpoint represents convergence of the subcritical curves, not the
simultaneous existence of every quality at the critical state.

\subsection{Library metadata and direct property wrappers}

\paragraph{String metadata.}
The string-returning metadata functions are:
\begin{itemize}
  \item \path{M.global_param_string(param,n)};
  \item \path{M.parameter_information_string(param,n)}; and
  \item \path{M.fluid_param_string(fluid,param,n)}.
\end{itemize}
Their buffer length \texttt{n} is optional and defaults to 4096 bytes.
\path{M.fluid_param_string_len(fluid,param)} can be used before requesting
unusually long metadata.

\begin{LCPExample}{Read fluid and parameter metadata}
local lcp = require("luacoolprop")

local aliases = lcp.fluid_param_string("R134a", "aliases")
local information = lcp.parameter_information_string("Hmass")
io.write(aliases, "\n", information, "\n")
\end{LCPExample}

\paragraph{Two-input property call.}
\texttt{M.propsSI(output,name1,value1,name2,value2,fluid)} is the principal
high-level CoolProp wrapper.  All keys are CoolProp identifiers and both inputs
are SI values.  The mixed-case \texttt{M.PropsSI} name is an accepted alias.

\begin{LCPExample}{Compute two properties at a saturated state}
local lcp = require("luacoolprop")
local pressure = 2.5e5
local quality = 1.0

local h = lcp.propsSI("Hmass", "P", pressure, "Q", quality, "R134a")
local T = lcp.propsSI("T", "P", pressure, "Q", quality, "R134a")
\end{LCPExample}

\paragraph{Other direct wrappers.}
\path{M.props1SI(fluid,output)} obtains a single fluid property.
\path{M.hapropsSI(...)} accepts the three input pairs required by humid-air
calculations.  \path{M.phaseSI(...)} returns a textual phase name.
Accepted aliases preserve CoolProp's spellings \texttt{Props1SI},
\texttt{HAPropsSI}, and \texttt{PhaseSI}.

\paragraph{Indices and validation.}
\path{M.is_valid_fluid_string(fluid)} returns a boolean.
\path{M.param_index(param)} and \path{M.input_pair_index(pair)} return
numeric indices owned by the loaded CoolProp version.  An application may cache
an index for a hot loop, but must never persist it or reuse it with another
CoolProp build.  \path{M.saturation_ancillary(...)} exposes the corresponding
C-API correlation and follows its SI-unit contract.

\paragraph{Global CoolProp configuration.}
\path{M.set_config_string}, \path{M.set_config_double},
\path{M.set_config_bool}, and \path{M.set_debug_level} mutate
process-wide CoolProp state.  Configure them once before calculations and record
non-default settings.  Pass a real Lua boolean to \path{set_config_bool}:
the string \texttt{"false"} is truthy in Lua and therefore means true.
\path{M.get_debug_level()} reads the current numeric level.

\subsection{Managed AbstractState objects}

\texttt{M.AbstractState(backend,fluids)} constructs a Lua wrapper around one
native CoolProp handle.  The method table is exposed as \texttt{M.State} for
introspection, but instances must be created by the constructor.

\begin{itemize}
  \item \texttt{state:update(pair,value1,value2)} accepts a CoolProp input-pair
        name or numeric index, updates the native object, and returns the same
        state for chaining;
  \item \texttt{state:keyed\_output(param)} accepts a property name or index
        and returns an SI value;
  \item \texttt{state:phase()} returns the numeric CoolProp phase index;
  \item \texttt{state:fluid\_names()} returns the native fluid specification;
        and
  \item \texttt{state:free()} releases the native handle and is idempotent.
\end{itemize}

Always free a state deterministically.  The attached FFI finalizer is only a
safety net because Lua does not guarantee when garbage collection occurs.

\begin{LCPExample}{AbstractState with one deterministic cleanup point}
local lcp = require("luacoolprop")
local state

local ok, result = xpcall(function()
  state = lcp.AbstractState("HEOS", "R134a")
  state:update("PT_INPUTS", 3e5, 293.15)
  return {
    h = state:keyed_output("Hmass"),
    s = state:keyed_output("Smass"),
    phase = state:phase(),
  }
end, debug.traceback)

if state ~= nil then
  state:free()
  state = nil
end
if not ok then error(result, 0) end

io.write(string.format("h=%.3f kJ/kg, s=%.4f kJ/(kg K)\n",
  result.h * 1e-3, result.s * 1e-3))
\end{LCPExample}

Never copy \texttt{state.handle}, invoke the native free function directly, or
use a state after \texttt{free()}.  Finalizers must remain non-throwing and no
application logic should depend on their execution order at engine shutdown.

\subsection{Fluid-constant table}

\texttt{M.fluid\_constants(fluid,library)} returns a fresh table.  The optional
\texttt{library} path is passed to the cached loader.  Unavailable properties
may be nil.

\begin{center}
\begin{tabularx}{\textwidth}{>{\raggedright\arraybackslash}p{.22\textwidth}X>{\raggedright\arraybackslash}p{.16\textwidth}}
\toprule
Field & Meaning & Unit \\
\midrule
\texttt{fluid} & requested fluid specification & -- \\
\texttt{pcrit} & critical pressure & Pa \\
\texttt{ptriple} & triple-point pressure & Pa \\
\texttt{Tcrit} & critical temperature & K \\
\texttt{Ttriple} & triple-point temperature & K \\
\texttt{Tmin}, \texttt{Tmax} & model temperature bounds & K \\
\texttt{rhocrit} & critical mass density & kg/m\textsuperscript{3} \\
\bottomrule
\end{tabularx}
\end{center}

\subsection{Structured diagram interface}

The preferred accessors are \path{M.diagram.get_type("PH")},
\path{M.diagram.get_type("PV")}, and
\path{M.diagram.get_type("TS")}, and \path{M.diagram.get_type("HS")}.  They decouple
application code from the lowercase storage fields \texttt{M.diagram.ph},
\path{M.diagram.pv}, \path{M.diagram.ts}, and \path{M.diagram.hs}.  All implementations contain
these renderers:

\begin{itemize}
  \item \texttt{axis\_style(opts)} returns data-dependent PGFPlots axis keys;
  \item \texttt{plots(opts)} returns all enabled families as PGFPlots code;
  \item \texttt{family(opts,name)} returns one canonical family;
  \item \texttt{process\_points(opts)} returns sampled data and metadata;
  \item \texttt{process\_plot(opts)} returns PGFPlots code and metadata;
  \item each entry in the implementation's \texttt{families} table provides
        its own \texttt{curve} and \texttt{plots} functions; and
  \item the corresponding \texttt{*\_plots(opts)} functions select values and
        serialize one family.
\end{itemize}

The computation layer never calls \texttt{tex.sprint}.  This separation makes
the renderers usable from \texttt{texlua}, test code, and custom TeX bridges.

\subsection{Point-record schema and adaptive sampling}

Every curve renderer returns an array in plotting order.  Every point contains
\texttt{x}, \texttt{y}, \texttt{p}, and \texttt{t=log10(p)}.  PH points contain
\texttt{h} and use scaled enthalpy for X; PV points contain \texttt{v} and
\texttt{rho} and use scaled specific volume for X.  TS points use scaled
entropy for X and an offset, scaled temperature for Y while retaining both raw
SI properties.  Family-specific records may
also contain \texttt{q}, \texttt{T}, or \texttt{s}, always in SI units.  Samples
are not uniformly spaced: midpoint-error subdivision adapts to curvature, and
invalid state pairs can split a curve into valid segments.

For a quality curve that enters the upper half of the subcritical interval,
the high-pressure portion is also sampled adaptively in
\texttt{log10((pcrit-p)/pcrit)}.  This reduced-pressure pass uses the same
\texttt{tolerance}, \texttt{max\_depth}, and projection-specific coordinate
weights as the main pass.  Tightening the quality-family sampling controls
therefore refines TS and HS curves all the way to their common critical limit;
it does not merely select a denser fixed pressure table.

The array also exposes topology metadata: \texttt{segments},
\path{domain_gap_count}, \path{omitted_sample_count}, and
\path{partially_omitted}.  Its flat
compatibility view marks each new component with \texttt{\_break\_before}; the
serializer inserts a PGFPlots jump.  Thus no line is ever fabricated across an
invalid CoolProp domain.  The \texttt{domain\_policy} option selects silent,
warning, or error diagnostics without changing the topology.

\begin{LCPExample}{Generate and inspect one PH isotherm}
local lcp = require("luacoolprop")
local ph = lcp.diagram.get_type("PH")

local points = ph.isotherm_curve({
  fluid = "R134a",
  pressure_min = 1e5,
  pressure_max = 2e6,
  enthalpy_scale = 1e-3,
  pressure_scale = 1e-5,
  isotherm_tolerance = 0.25,
}, 293.15)

for index, point in ipairs(points) do
  io.write(string.format("%d %.8g %.8g\n", index, point.x, point.y))
end
\end{LCPExample}

\begin{LCPExample}{Generate and inspect one log-log PV isotherm}
local lcp = require("luacoolprop")
local pv = lcp.diagram.get_type("PV")

local points = pv.isotherm_curve({
  fluid = "R134a",
  pressure_min = 1e5,
  pressure_max = 5e6,
  specific_volume_scale = 1,
  pressure_scale = 1e-5,
  isotherm_tolerance = 0.18,
}, 293.15)

for index, point in ipairs(points) do
  io.write(string.format("%d %.8g %.8g %.8g\n",
    index, point.x, point.y, point.rho))
end
\end{LCPExample}

\begin{LCPExample}{Generate and inspect one TS isenthalp}
local lcp = require("luacoolprop")
local ts = lcp.diagram.get_type("TS")

local points = ts.isenthalp_curve({
  fluid = "R134a",
  pressure_min = 1e5,
  pressure_max = 2e6,
  entropy_scale = 1e-3,
  temperature_scale = 1,
  isenthalp_tolerance = 0.10,
}, 300e3)

for index, point in ipairs(points) do
  io.write(string.format("%d %.8g %.8g %.8g\n",
    index, point.x, point.y, point.h))
end
\end{LCPExample}

Single-curve functions require explicit, positive pressure limits and expect the
second argument in SI units: quality is dimensionless, temperature is kelvin,
entropy is J/(kg K), enthalpy is J/kg, and specific volume is
m\textsuperscript{3}/kg.

\subsection{Shared option contract and declared capabilities}

Lua option names use \texttt{snake\_case}.  The structured API accepts
semantic \texttt{*\_axis\_unit} fields whenever a named display unit is
required.  Raw scales are advanced low-level controls.  Every type
normalizes through the same boundary and validates singular canonical family
switches against its own registry declaration.

\begin{center}
\begin{tabularx}{\textwidth}{>{\raggedright\arraybackslash}p{.29\textwidth}>{\raggedright\arraybackslash}p{.18\textwidth}X}
\toprule
Field & Default & Contract \\
\midrule
\texttt{fluid} & \texttt{"R134a"} & CoolProp fluid name \\
\texttt{library} & search & explicit shared-library candidate \\
\texttt{reference\_state} & \texttt{"DEF"} & locked DEF, IIR, ASHRAE, or NBP convention \\
\texttt{pressure\_min} & fluid based & lower pressure in Pa \\
\texttt{pressure\_max} & fluid based & upper pressure in Pa \\
\texttt{enthalpy\_scale} & \texttt{1e-3} & multiplier from SI enthalpy to plot x \\
\texttt{pressure\_scale} & \texttt{1e-5} & multiplier from SI pressure to plot y \\
\texttt{coord\_digits} & 6 & serialization precision \\
\texttt{initial\_intervals} & family based & initial log-pressure intervals \\
\texttt{max\_depth} & family based & maximum adaptive recursion depth \\
\texttt{tolerance} & family based & midpoint error threshold \\
\texttt{domain\_policy} & ignore & omitted-domain diagnostic \\
\texttt{log\_weight} & 30 & pressure contribution to error \\
\texttt{quality} & true & include quality curves \\
\texttt{isotherm} & false & include isotherms \\
\texttt{isentrope} & false & include isentropes \\
\texttt{isochore} & false & include isochores \\
\texttt{labels} & false & request labels \\
\texttt{label\_placement} & autonode & label backend selection \\
\bottomrule
\end{tabularx}
\end{center}

PH, PV, TS, HS, and PT are peer implementations.  Their declared background
families are:
\begin{itemize}
  \item PH: quality, isotherm, isentrope, and isochore;
  \item PV: quality, isotherm, and isentrope;
  \item TS: quality and isenthalp; and
  \item HS: quality, isochore, isotherm, and isobar.
\end{itemize}
An unavailable family switch is an error.  Process paths nevertheless support
all six conserved properties in every projection.

PV uses \path{specific_volume_scale} (default 1) and
\path{log_x_weight} (default 30) to control the volume term in the log--log
midpoint error.

TS uses the shared pressure, quality, label, style, autonode, and override fields.
Its coordinate fields are \path{entropy_scale} (default \texttt{1e-3}) and
\path{temperature_scale} (default \texttt{1}).  Set
\path{isenthalp=true} to enable its second background family and select that
family with \path{enthalpy_values}, \path{enthalpy_min},
\path{enthalpy_max}, \path{enthalpy_step}, or \path{enthalpy_count}.  Explicit
values default to kJ/kg; \path{enthalpy_unit="si"} selects J/kg.  The
\path{temperature_weight} field converts vertical deviations in kelvin into
the adaptive midpoint error for the linear TS axis.

Family-specific sampling controls prefix the generic name, for example
\path{isotherm_tolerance}, \path{isentrope_max_depth}, and
\path{isochore_initial_intervals}.  Selection fields include
\path{quality_values}, \path{temperature_values},
\path{entropy_values}, and \path{specific_volume_values}.  A Lua value
list is currently passed as a comma-separated string because the same normalized
representation arrives from the generic TeX layer.

\begin{LCPExample}{Return PGFPlots code without printing to TeX}
local lcp = require("luacoolprop")
local ph = lcp.diagram.get_type("PH")

local code = ph.plots({
  fluid = "R134a",
  quality = true,
  isotherm = true,
  isentrope = false,
  isochore = false,
  quality_values = "0,0.25,0.5,0.75,1",
  temperature_values = "-20,0,20,40",
  temperature_unit = "celsius",
  labels = true,
})
io.write(code, "\n")
\end{LCPExample}

The returned string contains trusted TeX source.  Styles and label text are not
security-sanitized; never forward untrusted external input directly into them.

\subsection{Lua process API}

Every built-in diagram table provides \texttt{process\_points(opts)}.  Thus
\texttt{ph}, \texttt{pv}, \texttt{ts}, and \texttt{hs} each require a process
\texttt{type} plus
\texttt{from} and \texttt{to} state strings.  Canonical process types are
\texttt{isobar}, \texttt{isotherm}, \texttt{isentropic},
\texttt{isenthalpic}, \texttt{quality}, and \texttt{isochore}.  Bare state
numbers use SI units; explicit suffixes such as \texttt{bar} and \texttt{C}
make application code easier to review.

\begin{LCPExample}{Resolve a process and consume its metadata}
local lcp = require("luacoolprop")
local ph = lcp.diagram.get_type("PH")

local points, metadata = ph.process_points({
  fluid = "R134a",
  type = "isentropic",
  from = "pressure=2bar,quality=1",
  to = "pressure=10bar",
})

io.write(metadata.id, " ", metadata.kind, "\n")
io.write(string.format("%d points, h2=%.3f kJ/kg\n",
  #points, metadata.to.h * 1e-3))
\end{LCPExample}

Metadata contains a stable \texttt{id}, the normalized \texttt{kind}, the SI
\texttt{constant}, and completed \texttt{from} and \texttt{to} state tables.
The corresponding \texttt{process\_plot(opts)} returns serialized PGFPlots code followed by
the same metadata.  Process style fields include \texttt{process\_color},
\texttt{process\_style}, \texttt{mark\_endpoints}, \texttt{marker\_style},
\texttt{label}, \texttt{label\_pos}, and \texttt{label\_style}.

TS process metadata follows the same contract.  Isotherms and isentropes are
exact horizontal and vertical TS segments.  Isobars use entropy as their
sampling variable so a two-phase evaporation or condensation includes the
constant-temperature saturation plateau; isochores use temperature, and
quality paths and isenthalps use pressure.

HS process metadata likewise retains all SI properties while plotting scaled
entropy as $x$ and scaled enthalpy as $y$.  Isentropes and isenthalps are exact
vertical and horizontal segments; isobars use entropy, isotherms include their
exact two-phase segment, isochores use temperature, and qualities use pressure.

PT process metadata plots scaled/offset temperature as $x$ and scaled pressure
as $y$.  Isobars and isotherms are exact horizontal and vertical segments;
isentropes, isenthalps, isochores, and quality paths use logarithmic pressure.
Every quality path projects onto the same liquid--vapour coexistence locus,
because PT coordinates do not contain enough information to recover quality.

\subsection{The built-in registry and custom diagram types}

\path{M.diagram.register_type(code,implementation)} requires an uppercase
identifier, rejects duplicate registration, assigns \path{implementation.type},
and returns the registered table.  \path{M.diagram.get_type(code)} either
returns it or raises an error listing available types.

A new implementation should provide the following fields:
\begin{itemize}
  \item \texttt{plots} and \path{axis_style};
  \item \texttt{family} and a \texttt{families} registry; and
  \item \path{process_points} and \path{process_plot}.
\end{itemize}
Numerical renderers must remain independent of the global \texttt{tex} table.

PH, PV, TS, HS, and PT are registered by the built-in module.  They expose the same
core renderer and process shape; PH families are quality, isotherm, isentrope,
and isochore; PV families are quality, isotherm, and isentrope; TS families are
quality and isenthalp; HS families are quality, isochore, isotherm, and isobar;
PT families are phase envelope, isentrope, isenthalp, and isochore.

\begin{LCPExample}{Minimal shape of a custom XY extension}
local lcp = require("luacoolprop")

local xy = {
  plots = function(opts)
    assert(type(opts) == "table", "opts must be a table")
    return "% XY plots would be serialized here"
  end,
  axis_style = function(_)
    return "xlabel={$T$},ylabel={$p$}"
  end,
  families = {},
}

lcp.diagram.register_type("XY", xy)
\end{LCPExample}

An extension module should not mutate the built-in PH, PV, TS, HS, or PT
tables. Registration is
process-local; a duplicate error should reveal an accidental double load rather
than being silently ignored.

\subsection{TeX bridge and direct entry points}

\texttt{M.tex.print\_diagram(type,operation,opts,family)} is the structured
bridge.  It accepts \texttt{plots}, \texttt{axis\_style}, \texttt{family}, or
\texttt{process}, selects the registered renderer, and writes the returned code
with \texttt{tex.sprint}.  It therefore requires an active LuaTeX \texttt{tex}
table and is not a standalone \texttt{texlua} API.

The flat \texttt{M.diagram.ph\_*} and \texttt{M.tex.print\_ph\_*} functions
provide direct PH entry points. Mixed-case property functions mirror CoolProp
names. Generic Lua integrations use lowercase wrappers and select an
implementation with one of these equivalent registry calls:
\begin{itemize}
  \item \texttt{M.diagram.get\_type("PH")};
  \item \texttt{M.diagram.get\_type("PV")};
  \item \texttt{M.diagram.get\_type("TS")};
  \item \texttt{M.diagram.get\_type("HS")}; or
  \item \texttt{M.diagram.get\_type("PT")}.
\end{itemize}
They use singular family names and full property names.

\begin{LCPNote}[Lua implementation checklist]
New Lua code must keep module state local, declare temporaries with
\texttt{local}, validate arguments at public boundaries, preserve SI units at
the CoolProp boundary, free native resources deterministically, keep finalizers
non-throwing, and separate calculation from TeX output.  Diagram functions
return tables or strings; only \texttt{M.tex} writes to TeX.  Automatic label
placement remains exclusively in \pkg{pgfplots-autonode}.
\end{LCPNote}

\section{Curve records and the autonode bridge}

A curve record stores the curve identifier, family, thermodynamic value, points,
PGFPlots style, label text, preferred label position, label style, priority, and
per-curve overrides.  The function \texttt{add\_coords\_plot\_with\_autonode}
serializes one record as:

\begin{LCPExample}{Autonode serialization}
\addplot[name path=<id>,<style>] coordinates {<points>}
  \pgfplotsautonode[<options>]{<label>};
\end{LCPExample}

The bridge is intentionally small.  LuaCoolProp forwards high-level preferences
such as \key{label pos for}, \key{label fixed for}, \key{label style for}, and
\key{label show for}; \pkg{pgfplots-autonode} remains responsible for candidate
search, overlap minimization, border constraints, and final node drawing.

\subsection{Autonode Lua backend contract}

The TeX library owns text, styles, PGFPlots path sampling, and final TikZ nodes.
Its Lua module owns only numerical geometry and exposes the following bridge
functions:

\begin{description}
  \item[\texttt{reset()}] Clear labels, candidates, and axis geometry.
  \item[\texttt{configure(options)}] Select the solver, bounding-box model,
  failure policy, border constraints, and debugging options.
  \item[\texttt{set\_axis\_rect(left,right,bottom,top)}] Register the visible
  plot rectangle in TeX points.
  \item[\texttt{add\_label(...)}] Register measured label geometry, weights,
  rotation policy, and priority.
  \item[\texttt{add\_candidate(id,pos,x,y)}] Add a PGFPlots path sample for a
  label.
  \item[\texttt{emit\_candidate\_positions(...)}] Ask TeX to sample the
  deterministic candidate positions selected by a strategy.
  \item[\texttt{solve()}] Compute an assignment without drawing it.
  \item[\texttt{solve\_and\_emit()}] Compute the assignment and emit the TeX
  drawing callbacks.
\end{description}

These functions are documented next to their implementations in
\texttt{pgfplots-autonode.lua}.  They do not depend on CoolProp or on any
thermodynamic structure.

\section{Process validation}

Process paths are specified by a \key{type}, a starting endpoint, and an ending
endpoint.  LuaCoolProp first parses units, then attempts to resolve each endpoint
with CoolProp.  If the conserved property is missing, the validator tries to
infer it from one sufficiently defined endpoint.  When inference is impossible,
the error message names the missing property and suggests the minimal additional
state variable, for example adding \key{p=...} or \key{T=...} to a saturated
state defined by \key{Q=...}.

Redundant inputs are assertions, not display-only annotations.  After choosing
one independent pair and resolving the full state, LuaCoolProp checks all
additional supplied properties against CoolProp.  It subsequently checks the
declared constant at both completed endpoints.  Contradictions stop the build
and the diagnostic contains both values, their difference, and the tolerance;
no inconsistent value is silently retained in exported metadata.

\clearpage
\printindex[commands]
\clearpage
\printindex[keys]

\end{document}
