# LuaCoolProp Lua API reference This document describes the public Lua interface implemented by `luacoolprop.lua`. It is intended for package maintainers, advanced users who generate PGFPlots code programmatically, and contributors adding diagram types. The TeX interface remains the recommended interface for ordinary documents. The reference applies to LuaCoolProp 1.1.0. CoolProp is an external shared library with its own versioned API. Always record both `lcp._VERSION` and `lcp.version()` when reproducibility matters. For published results, cite the underlying CoolProp paper and project site as specified on its [official citation page](https://coolprop.org/citation.html). LuaCoolProp was developed with substantial ChatGPT-assisted “vibe coding.” AI assistance is not a correctness or safety guarantee: review this implementation and independently validate thermodynamic results before high-consequence use. Loading CoolProp through FFI executes native code in the current process, so select only a trusted, architecture-compatible library, preferably by an absolute `LUACOOLPROP_LIB` path. For TeX typesetting, the required full `--shell-escape` mode also grants external-command capabilities to the entire document and all loaded inputs; consult the user manual's installation and security chapter before enabling it. ## Runtime and module loading The module requires the FFI implementation supplied by LuaTeX or `texlua`. It can be required from LuaLaTeX, plain LuaTeX, ConTeXt MkIV, or a `texlua` program. LuaMetaTeX/LMTX does not supply this FFI module. ```lua local lcp = require("luacoolprop") local _, loaded_from = lcp.load_library() io.write("LuaCoolProp ", lcp._VERSION, "\n") io.write("CoolProp ", lcp.version(), "\n") io.write("Library: ", loaded_from, "\n") ``` Use `local` for the module reference and for every intermediate value. Requiring the module returns one table and does not intentionally install application globals. The generic TeX layer keeps its own local module reference. ### Shared-library search order `lcp.load_library(path)` tries candidates in this order: 1. the explicit `path` argument; 2. `LUACOOLPROP_LIB`; 3. the secondary `COOLPROP_LIB` variable; 4. platform filenames in the current directory; 5. the current TeX file resolver; 6. `CoolProp` and `libCoolProp` through the platform dynamic loader. The first successful FFI handle is cached for the lifetime of the Lua process. Later calls return that same handle; they do not replace the loaded library. Set `LUACOOLPROP_LIB` before starting the engine when the exact dependency must be controlled. `lcp.loaded_library()` returns the successful path or loader name, or `nil` before the first successful load. `lcp.C` contains the raw FFI handle after a load, but it is an implementation escape hatch, not a stable package API. ## Units and numeric conventions Low-level functions use CoolProp SI units without implicit conversion. | Quantity | Lua value | SI unit | | --- | ---: | --- | | pressure `p` | number | Pa | | temperature `T` | number | K | | mass enthalpy `h` | number | J/kg | | mass entropy `s` | number | J/(kg K) | | mass density `rho` | number | kg/m³ | | specific volume `v` | number | m³/kg | | vapor quality `q` | number | dimensionless, normally 0–1 | Diagram curve points additionally contain plotting coordinates. With the defaults, PH uses `(h * 1e-3, p * 1e-5)`, PV uses `(v, p * 1e-5)`, TS uses `(s * 1e-3, T)`, and HS uses `(s * 1e-3, h * 1e-3)`. The original SI values remain available in every record. Prefer semantic coordinate-unit options. They change the numerical conversion and its declared unit as one operation: | Coordinate property | Option | Accepted values | | --- | --- | --- | | pressure | `pressure_axis_unit` | `pa`, `kpa`, `mpa`, `bar` | | enthalpy | `enthalpy_axis_unit` | `jkg`, `kjkg` | | entropy | `entropy_axis_unit` | `jkgk`, `kjkgk` | | specific volume | `specific_volume_axis_unit` | `m3kg`, `lkg` | | temperature | `temperature_axis_unit` | `kelvin`, `celsius` | The option is accepted only when that property is an axis of the selected diagram. Celsius uses `(T - 273.15)`, not a multiplicative approximation. Numeric `*_scale` fields remain an advanced compatibility interface; TeX then labels the coordinate as an explicit scaled-SI value instead of claiming a named unit. All public numeric results are ordinary Lua numbers. Callers doing independent calculations should reject NaN and infinities before serializing them. Diagram renderers already discard thermodynamic points that CoolProp cannot evaluate. ## Errors and ownership Programming errors, load failures, invalid process specifications, and reported CoolProp C-API errors raise Lua errors. The module does not use `(nil, err)` for these failures. Catch an error only at a boundary where the caller can add useful context or recover. ```lua 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)) ``` Do not wrap every property call in `pcall`: that hides invalid models and makes diagnostics less useful. The internal adaptive samplers deliberately use protected calls at individual sample points because a curve may cross a region where a property pair is undefined. CoolProp `AbstractState` handles own native resources. Call `state:free()` as soon as the state is no longer needed. The FFI finalizer is a safety net, not a substitute for deterministic cleanup. ## Module topology The returned module table has three principal layers: | Namespace | Responsibility | | --- | --- | | `lcp` | library loading and thin CoolProp C-API wrappers | | `lcp.diagram` | format-independent computation and serialization | | `lcp.tex` | functions that write to the active LuaTeX input stream | Application Lua code should prefer `lcp` and `lcp.diagram`. Only the generic TeX layer should normally call `lcp.tex`. ## Library and metadata functions ### `load_library([path]) -> handle, loaded_from` Loads and caches CoolProp. `path`, when present, must be a string. The first return value is FFI cdata; the second is the path or loader name. Prefer the second value for diagnostics and do not retain or call the raw handle unless a function is genuinely absent from LuaCoolProp's wrapper. Surrounding whitespace is removed from every path candidate, including a TeX key value or environment variable; whitespace inside the path is preserved. ### `loaded_library() -> string|nil` Reports the candidate used by the successful load. It does not trigger loading. ### `version() -> string` and `gitrevision() -> string` Return metadata embedded in the loaded CoolProp library. Both calls trigger library loading when necessary. ### String metadata - `global_param_string(param[, buffer_length])` - `parameter_information_string(param[, buffer_length])` - `fluid_param_string(fluid, param[, buffer_length])` - `fluid_param_string_len(fluid, param)` The optional buffer length defaults to 4096 bytes. Query the required fluid metadata length first only when a value can exceed that default. ```lua local lcp = require("luacoolprop") local aliases = lcp.fluid_param_string("R134a", "aliases") local description = lcp.parameter_information_string("Hmass") io.write(aliases, "\n", description, "\n") ``` ## Thin CoolProp property wrappers ### `propsSI(output, name1, value1, name2, value2, fluid) -> number` Calls CoolProp `PropsSI`. Property identifiers and numeric values are passed unchanged. `PropsSI` is an alternate spelling for the same Lua function. ```lua 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") ``` ### `props1SI(fluid, output) -> number` Calls CoolProp `Props1SI` for a fluid constant. `Props1SI` is an alternate alias. ### `hapropsSI(output, name1, value1, name2, value2, name3, value3) -> number` Calls CoolProp `HAPropsSI` for humid air. `HAPropsSI` is an alternate spelling. ### `phaseSI(name1, value1, name2, value2, fluid[, buffer_length]) -> string` Returns CoolProp's textual phase result. `PhaseSI` is an alternate spelling. ### Lookup and validation functions - `is_valid_fluid_string(fluid) -> boolean` - `param_index(param) -> number` - `input_pair_index(pair) -> number` - `saturation_ancillary(fluid, output, Q, input, value) -> number` Indices belong to the loaded CoolProp version. Resolve them at startup when they are useful for a hot loop, but never store them in generated files or use an index obtained from a different CoolProp build. ### Process-wide CoolProp configuration - `set_config_string(key, value)` - `set_config_double(key, value)` - `set_config_bool(key, value)` - `set_debug_level(level)` - `get_debug_level() -> number` These functions mutate CoolProp process-wide state. Configure the library once, before calculations, and document non-default values. For `set_config_bool`, pass an actual boolean: Lua treats every value except `nil` and `false` as true, including the string `"false"`. ### Enthalpy and entropy reference states `set_reference_state(fluid[, reference[, library]]) -> reference` accepts `DEF`, `IIR`, `ASHRAE`, or `NBP`; input is case-insensitive and the result is uppercase. `reference_state(fluid) -> string|nil` reports the convention initialized through LuaCoolProp. Reference-state changes are initialization operations. The first diagram call for a fluid locks its convention, and changing it later raises an error; repeating the current convention is harmless. All diagram and process entry points default to `DEF` and accept `reference_state` in their options. Process metadata includes the resulting `reference_state`. Enthalpy and entropy are relative properties, so compare only states computed with the same convention and prefer enthalpy differences in energy balances. ## Managed `AbstractState` ### Construction `AbstractState(backend, fluids) -> state` creates a native CoolProp state and a Lua owner object. `State` is the shared method table and is exposed for introspection; construct instances only through `AbstractState`. ### Methods - `state:update(input_pair, value1, value2) -> state` updates the native state. `input_pair` may be a CoolProp name or an already resolved numeric index. - `state:keyed_output(param) -> number` reads a property by name or index. - `state:phase() -> number` returns CoolProp's numeric phase index. - `state:fluid_names() -> string` returns the native fluid specification. - `state:free()` releases the handle. It is safe to call more than once. Use a single cleanup point when work between construction and release can fail: ```lua 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)) ``` Never copy `state.handle`, call native free functions directly, or use a state after `free()`. Do not depend on finalizer order at engine shutdown. ## Fluid constants `fluid_constants(fluid[, library]) -> constants` returns a new table. Its fields are: | Field | Meaning | Unit | | --- | --- | --- | | `fluid` | requested CoolProp fluid string | — | | `pcrit` | critical pressure | Pa | | `ptriple` | triple-point pressure | Pa | | `Tcrit` | critical temperature | K | | `Ttriple` | triple-point temperature | K | | `Tmin` | lower temperature bound | K | | `Tmax` | upper temperature bound | K | | `rhocrit` | critical mass density | kg/m³ | A property that is unavailable may be `nil`. Treat the table as a snapshot and do not mutate it if it is also passed to a renderer through `opts.constants`. ## Structured diagram API Diagram codes are uppercase coordinate identifiers such as `PH`, `PV`, `TS`, `HS`, or `PT`. The built-in implementation tables are `lcp.diagram.ph`, `lcp.diagram.pv`, `lcp.diagram.ts`, `lcp.diagram.hs`, and `lcp.diagram.pt`. Retrieve them through `lcp.diagram.get_type("PH")` or `lcp.diagram.get_type("PV")`, `lcp.diagram.get_type("TS")`, or `lcp.diagram.get_type("HS")`, or `lcp.diagram.get_type("PT")` when code should be independent of lowercase field naming. Every implementation passes through the same option-normalization and capability boundary. No built-in type is the template or fallback for another. The background-family matrix is: | Type | Axis properties | Registered families | | --- | --- | --- | | `PH` | enthalpy, pressure | quality, isotherm, isentrope, isochore | | `PV` | specific volume, pressure | quality, isotherm, isentrope | | `TS` | entropy, temperature | quality, isenthalp | | `HS` | entropy, enthalpy | quality, isochore, isotherm, isobar | | `PT` | temperature, pressure | phase_envelope, isentrope, isenthalp, isochore | Setting an unavailable family switch to true raises an error. Process paths remain able to project all six conserved-property transformations in every coordinate system. Diagram calls enforce a pure-fluid boundary with CoolProp's `pure` metadata. Explicit mixtures, `.mix` files, and predefined blends are rejected because the samplers assume one critical point and one saturation dome. Thin wrappers such as `propsSI` and managed `AbstractState` intentionally remain mixture-capable. The PH implementation exposes: ```text axis_style(opts) -> pgfplots key string plots(opts) -> pgfplots commands family(opts, family_name) -> pgfplots commands process_points(opts) -> points, metadata process_plot(opts) -> pgfplots commands, metadata quality_curve(opts, quality) -> points isotherm_curve(opts, temperature) -> points isentrope_curve(opts, entropy) -> points isochore_curve(opts, volume) -> points quality_plots(opts) -> pgfplots commands isotherm_plots(opts) -> pgfplots commands isentrope_plots(opts) -> pgfplots commands isochore_plots(opts) -> pgfplots commands ``` PV has the same core shape and these public families: ```text axis_style(opts) -> pgfplots key string plots(opts) -> pgfplots commands family(opts, family_name) -> pgfplots commands process_points(opts) -> points, metadata process_plot(opts) -> pgfplots commands, metadata quality_curve(opts, quality) -> points isotherm_curve(opts, temperature) -> points isentrope_curve(opts, entropy) -> points quality_plots(opts) -> pgfplots commands isotherm_plots(opts) -> pgfplots commands isentrope_plots(opts) -> pgfplots commands ``` TS has the same core shape and these public families: ```text axis_style(opts) -> pgfplots key string plots(opts) -> pgfplots commands family(opts, family_name) -> pgfplots commands process_points(opts) -> points, metadata process_plot(opts) -> pgfplots commands, metadata quality_curve(opts, quality) -> points isenthalp_curve(opts, enthalpy) -> points quality_plots(opts) -> pgfplots commands isenthalp_plots(opts) -> pgfplots commands ``` HS has the same core shape and these public families: ```text axis_style(opts) -> pgfplots key string plots(opts) -> pgfplots commands family(opts, family_name) -> pgfplots commands process_points(opts) -> points, metadata process_plot(opts) -> pgfplots commands, metadata quality_curve(opts, quality) -> points isochore_curve(opts, volume) -> points isotherm_curve(opts, temperature) -> points isobar_curve(opts, pressure) -> points quality_plots(opts) -> pgfplots commands isochore_plots(opts) -> pgfplots commands isotherm_plots(opts) -> pgfplots commands isobar_plots(opts) -> pgfplots commands ``` PT has the same core shape and these public families: ```text axis_style(opts) -> pgfplots key string plots(opts) -> pgfplots commands family(opts, family_name) -> pgfplots commands process_points(opts) -> points, metadata process_plot(opts) -> pgfplots commands, metadata phase_envelope_curve(opts) -> points isentrope_curve(opts, entropy) -> points isenthalp_curve(opts, enthalpy) -> points isochore_curve(opts, volume) -> points phase_envelope_plots(opts) -> pgfplots commands isentrope_plots(opts) -> pgfplots commands isenthalp_plots(opts) -> pgfplots commands isochore_plots(opts) -> pgfplots commands ``` Renderers are pure with respect to TeX: they return data or strings and never call `tex.sprint`. They may load and call the external CoolProp library. ### Point records Every curve function returns an array in plotting order. Every point has: - `x`: scaled horizontal plot coordinate; - `y`: scaled vertical plot coordinate; - `p`: pressure in Pa when resolved; and - `t`: the scalar sampling parameter (normally `log10(p)`). PH points have `h` in J/kg and use `x = h * enthalpy_scale`. PV points have `v` in m³/kg and `rho` in kg/m³ and use `x = v * specific_volume_scale`. Both use `y = p * pressure_scale`. A TS point uses `x = s * entropy_scale` and `y = (T - temperature_offset) * temperature_scale`; the defaults produce kJ/(kg K) and K. An HS point uses `x = s * entropy_scale` and `y = h * enthalpy_scale`; its defaults produce kJ/(kg K) and kJ/kg. A PT point uses `x = (T - temperature_offset) * temperature_scale` and `y = p * pressure_scale`. A family-specific point may additionally contain `q`, `T`, `s`, `h`, `v`, or `rho`, all in SI units. Consumers must not assume uniformly spaced samples: subdivision is adaptive. Each returned point array also carries non-array topology metadata: - `segments`: the connected point arrays; - `domain_gap_count`: the number of gaps between retained components; - `omitted_sample_count`: failed CoolProp sample evaluations; and - `partially_omitted`: true when valid points and omissions coexist. The flat array view marks the first point after a gap with `_break_before = true`. Serialization inserts `(nan,nan)` and `unbounded coords=jump`, so two valid regions are never connected across an invalid domain. `domain_policy` (or `discontinuity_policy`) accepts `ignore`/`silent`, `warning`/`warn`, or `error` and controls the corresponding diagnostic without changing curve topology. Quality curves have a stronger sampling and endpoint contract. When their range enters the upper half of the subcritical interval, the generator performs a second adaptive pass in `log10((pcrit-p)/pcrit)`. This pass uses the same family tolerance, recursion limit, and projection-specific midpoint metric as the main sampler. It therefore resolves the critical curvature for PH, PV, TS, and HS without a fluid-specific cutoff or a fixed point table. If `pressure_max` reaches or exceeds `pcrit`, the generator then appends a canonical critical-limit record. That final record has `critical = true`, `critical_limit = true`, `quality_defined = false`, `p = pcrit`, and the critical `T`, `rho`, and `v`; it deliberately has no `q` field. Its `x` and `y` values are bit-identical for every curve. This is a common graphical limit: vapor quality is undefined at the critical state and the record does not claim that every quality exists there. PH evaluates the exact critical enthalpy with the `(Tcrit, rhocrit)` state because `(P, Q)` is singular there. A backend that cannot evaluate that state uses the mean of the closest common saturated liquid and vapour enthalpies and sets `critical_approximated = true`. PV uses the canonical `v = 1/rhocrit` directly. TS evaluates critical entropy from `(Tcrit, rhocrit)` and falls back to the mean of the closest saturated entropies. HS evaluates both critical entropy and enthalpy at that unique state and applies the corresponding saturation-mean fallbacks. If `pressure_max` is below `pcrit`, no critical record is added. At the triple-point pressure, saturated liquid and vapour correctly remain distinct. ### Shared options and common capability fields Lua option names use `snake_case`. The most important canonical fields are: | Field | Default | Contract | | --- | --- | --- | | `fluid` | `"R134a"` | CoolProp fluid name | | `library` | search order | explicit shared-library path | | `reference_state` | `"DEF"` | locked enthalpy/entropy reference convention | | `pressure_min` | fluid dependent | lower pressure in Pa | | `pressure_max` | fluid dependent | upper pressure in Pa | | `pressure_max_factor` | `1.10` | automatic upper limit relative to `pcrit` | | `enthalpy_scale` | `1e-3` | multiplier from J/kg to plot `x` | | `pressure_scale` | `1e-5` | multiplier from Pa to plot `y` | | `enthalpy_axis_unit` | `"kjkg"` | coupled PH/HS coordinate unit (`jkg` or `kjkg`) | | `pressure_axis_unit` | `"bar"` | coupled PH/PV/PT coordinate unit (`pa`, `kpa`, `mpa`, or `bar`) | | `coord_digits` | `6` | numeric serialization precision | | `initial_intervals` | family dependent | initial logarithmic-pressure intervals | | `max_depth` | family dependent | maximum adaptive recursion depth | | `tolerance` | family dependent | midpoint error threshold in plot space | | `domain_policy` | `"ignore"` | omitted-domain diagnostic policy | | `log_weight` | `30` | pressure contribution to midpoint error | | `labels` | `false` | enable labels for enabled families | | `label_placement` | `"autonode"` | `autonode` or explicit manual placement | | `quality_symbol` | `"Q"` | TeX math material used in quality labels | | `temperature_symbol` | `"T"` | TeX math material used in isotherm labels | | `entropy_symbol` | `"s"` | TeX math material used in isentrope labels | | `specific_volume_symbol` | `"v"` | TeX math material used in isochore labels | | `quality` | `true` | enable quality curves | | `isotherm` | `false` | enable isotherms | | `isentrope` | `false` | enable isentropes | | `isochore` | `false` | enable isochores | The structured API maps `enthalpy_scale` and `pressure_scale` to the compact fields `h_scale` and `p_scale`. Canonical names are recommended for generic dispatch. Family-specific sampling fields prefix the generic name, for example `isotherm_tolerance`, `isentrope_max_depth`, and `isochore_initial_intervals`. Symbol strings are trusted TeX math material; renderers add math delimiters, so callers must not include dollar signs. ### PV options and log-log sampling PV uses the shared pressure, quality, temperature, entropy, styling, label, autonode, and per-curve override fields. It does not expose an `isochore` background family. Its coordinate-specific fields are: | Field | Default | Contract | | --- | --- | --- | | `specific_volume_scale` | `1` | multiplier from m³/kg to plot `x` | | `specific_volume_axis_unit` | `"m3kg"` | coupled coordinate unit (`m3kg` or `lkg`) | | `pressure_scale` | `1e-5` | multiplier from Pa to plot `y` | | `log_x_weight` | `30` | volume contribution to midpoint error in `log10(x)` | | `quality` | `true` | enable quality curves | | `isotherm` | `false` | enable isotherms | | `isentrope` | `false` | enable isentropes | The canonical implementation maps `specific_volume_scale` to `v_scale`. Quality, isotherm, and isentrope curves are sampled in logarithmic pressure; their midpoint error is evaluated in `(log10(v), log10(p))`. Subcritical isotherms explicitly insert saturated vapour and saturated liquid at the same pressure, yielding the exact two-phase plateau rather than asking CoolProp for undefined intermediate single-phase states. ```lua 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, log_x_weight = 30, }, 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 ``` ### TS options and sampling TS uses the shared fluid, pressure, quality, label, style, autonode, and per-curve override fields. Its coordinate and isenthalp fields are: | Field | Default | Contract | | --- | --- | --- | | `entropy_scale` | `1e-3` | multiplier from J/(kg K) to plot `x` | | `entropy_axis_unit` | `"kjkgk"` | coupled coordinate unit (`jkgk` or `kjkgk`) | | `temperature_scale` | `1` | multiplier from K to plot `y` | | `temperature_axis_unit` | `"kelvin"` | coupled coordinate and label; `celsius` subtracts 273.15 | | `quality` | `true` | enable quality curves | | `isenthalp` | `false` | enable constant-enthalpy curves | | `enthalpy_unit` | `"kjkg"` | unit used by grid values and labels; `"si"` selects J/kg | | `enthalpy_values` | empty | comma-separated explicit enthalpy list | | `enthalpy_min`, `enthalpy_max` | fluid dependent | automatic-grid limits | | `enthalpy_step` | automatic | linear step | | `enthalpy_count` | `8` | count used when no step is supplied | | `isenthalp_tolerance` | `0.10` | adaptive midpoint threshold | | `temperature_weight` | `0.01` | kelvin contribution to linear-axis midpoint error | The canonical implementation maps `entropy_scale` to the internal `s_scale`. Quality and isenthalp curves use logarithmic pressure as their main sampling parameter, but midpoint errors are evaluated in TS plot coordinates. A quality curve that reaches `pcrit` receives an additional adaptive reduced-pressure pass; `tolerance`, `max_depth`, and `temperature_weight` continue to control its refinement arbitrarily close to the critical limit. Quality curves append one canonical critical state, exactly as their PH, PV, and HS counterparts. ```lua 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 ``` ### HS options and sampling HS uses the shared fluid, pressure, quality, temperature, specific-volume, label, style, autonode, and per-curve override fields. Its coordinate-specific and constant-pressure fields are: | Field | Default | Contract | | --- | --- | --- | | `entropy_scale` | `1e-3` | multiplier from J/(kg K) to plot `x` | | `enthalpy_scale` | `1e-3` | multiplier from J/kg to plot `y` | | `entropy_axis_unit` | `"kjkgk"` | coupled horizontal coordinate unit | | `enthalpy_axis_unit` | `"kjkg"` | coupled vertical coordinate unit | | `quality` | `true` | enable constant-quality curves | | `isochore` | `false` | enable constant-specific-volume curves | | `isotherm` | `false` | enable constant-temperature curves | | `isobar` | `false` | enable constant-pressure curves | | `isobar_unit` | `"bar"` | pressure-grid and label unit; also `pa`, `kpa`, `mpa`, or `si` | | `isobar_values` | empty | comma-separated explicit pressure list | | `isobar_min`, `isobar_max` | pressure bounds | automatic-grid limits in `isobar_unit` | | `isobar_mode` | `"log"` | `log`, `linear`, or explicit `list` generation | | `isobar_count` | `7` | automatic curve count when no step is given | | `isobar_temperature_min`, `isobar_temperature_max` | fluid dependent | endpoints in `temperature_unit` | | `isobar_tolerance` | `0.10` | adaptive midpoint threshold | | `enthalpy_weight` | `0.01` | enthalpy contribution to linear-axis midpoint error | The canonical implementation maps `entropy_scale` and `enthalpy_scale` to `s_scale` and `h_scale`. Quality curves use an adaptive reduced-pressure pass near `pcrit`; its midpoint test uses HS coordinates and remains controlled by `tolerance`, `max_depth`, and `enthalpy_weight`. They end at the canonical critical state. Subcritical isotherms explicitly contain both saturation endpoints and their straight mixture segment. Isobars are sampled with entropy as their independent variable, so they cross the two-phase region continuously. Isochores use logarithmic pressure sampling. ```lua local lcp = require("luacoolprop") local hs = lcp.diagram.get_type("HS") local options = { fluid = "Water", pressure_min = 1e4, pressure_max = 3e7, entropy_scale = 1e-3, enthalpy_scale = 1e-3, temperature_unit = "celsius", isobar_temperature_min = 10, isobar_temperature_max = 500, } local points = hs.isobar_curve(options, 1e5) for index, point in ipairs(points) do assert(point.x == point.s * 1e-3) assert(point.y == point.h * 1e-3) io.write(string.format("%d %.8g %.8g %.8g\n", index, point.x, point.y, point.p)) end ``` `hs.quality_curve`, `hs.isochore_curve`, `hs.isotherm_curve`, and `hs.isobar_curve` accept their constant property in SI units. Their corresponding `*_plots` functions and `hs.family(opts, name)` serialize one family; `hs.plots` serializes every enabled family. `hs.axis_style` derives padded linear bounds unless `entropy_axis_min/max` or `enthalpy_axis_min/max` is provided. ### PT options and phase-equilibrium sampling PT uses temperature on the horizontal axis and pressure on the vertical axis. The TeX high-level renderer uses a linear temperature scale and logarithmic pressure. Its default pressure interval starts at `ptriple` and ends at `1.10 * pcrit`; the phase envelope itself is clipped to `[ptriple, pcrit]`. | Field | Default | Contract | | --- | --- | --- | | `temperature_scale` | `1` | multiplier applied after the temperature offset | | `temperature_axis_unit` | `"kelvin"` | `kelvin`, or `celsius` with an exact 273.15 K offset | | `pressure_scale` | `1e-5` | multiplier from Pa to the vertical coordinate | | `pressure_axis_unit` | `"bar"` | `pa`, `kpa`, `mpa`, or `bar` | | `phase_envelope` | `true` | enable the liquid--vapour coexistence locus | | `isentrope` | `false` | enable constant-specific-entropy curves | | `isenthalp` | `false` | enable constant-specific-enthalpy curves | | `isochore` | `false` | enable constant-specific-volume curves | | `temperature_axis_min`, `temperature_axis_max` | sampled bounds | explicit plotted temperature limits | | `phase_envelope_tolerance` | `0.08` | adaptive midpoint tolerance | For a pure fluid, saturated liquid and saturated vapour share exactly the same temperature and pressure. PT therefore exposes one `phase_envelope` family, not a `quality` family. Its first and last records are the exact CoolProp triple and critical constants and carry `triple=true` or `critical=true`; `quality_defined=false` makes the thermodynamic degeneracy explicit. Between them, both `P,Q=0` and `P,Q=1` evaluations must succeed and their temperatures are averaged to suppress harmless solver roundoff. A second adaptive pass in reduced pressure resolves the critical neighbourhood. At the opposite end, the sampler checks that the first numerical saturation temperature is not below the exact `Ttriple` constant. If a backend has a minute lower-bound mismatch, a dimensionless pressure-offset bisection finds the closest consistent sample; no fluid-specific threshold is used. Isentropes, isenthalps, and isochores use the respective CoolProp pairs `P,S`, `P,H`, and `P,Dmass` while logarithmic pressure is the sampling parameter. Failed states produce explicit disconnected components under the shared `domain_policy` contract. In the two-phase region, these curves may coincide locally with the coexistence locus: a PT diagram cannot display vapour quality because all qualities at one saturation pressure have the same PT coordinates. ```lua local lcp = require("luacoolprop") local pt = lcp.diagram.get_type("PT") local constants = lcp.fluid_constants("R134a") local options = { fluid = "R134a", pressure_min = constants.ptriple, pressure_max = constants.pcrit * 1.1, temperature_axis_unit = "celsius", pressure_axis_unit = "bar", } local coexistence = pt.phase_envelope_curve(options) assert(coexistence[1].triple) assert(coexistence[#coexistence].critical) local isentrope = pt.isentrope_curve(options, 1700) -- J/(kg K) ``` ### Generating one curve Single-curve functions require explicit positive `pressure_min` and `pressure_max` values. Temperatures, entropies, enthalpies, and volumes passed as the second argument are SI values. ```lua 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 ``` ### Generating PGFPlots commands ```lua 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") ``` The returned string contains TeX and is not escaped. Values used as PGFPlots styles or label text are trusted package input. Do not pass untrusted external text directly to a renderer. Generated labels defer number and unit presentation to private TeX frontend hooks. Under LaTeX, `luacoolprop.sty` implements those hooks with siunitx: unit-bearing values use `\qty`, unit-only axis labels use `\unit`, and dimensionless values use `\num`. The generic layer supplies traditional math-mode fallbacks for plain TeX and ConTeXt. Lua renderers must continue to emit both the siunitx unit expression and the generic fallback rather than assuming a document format. ## Process API Every registered `implementation.process_points(opts)` resolves and samples one process through the same state language. Required semantic fields are `type`, `from`, and `to`; `fluid` defaults to `R134a`. Supported canonical types are `isobar`, `isotherm`, `isentropic`, `isenthalpic`, `quality`, and `isochore`. State specifications are comma- or semicolon-separated strings. Bare numbers use SI units. Input is locale independent and accepts only `.` as the decimal separator. Signed decimal floating-point and scientific notation are valid, so `pressure=1.2E5Pa`, `quality=.5`, and `temperature=-1.0e1C` are accepted. Decimal commas are rejected. Recognized suffixes are pressure units (`Pa`, `kPa`, `MPa`, `bar`, `mbar`), kelvin or Celsius (`K`, `kelvin`, `C`, `degC`, `celsius`), `J/kg` or `kJ/kg`, `J/(kg K)` or `kJ/(kg K)`, `m3/kg`, `L/kg` or `dm3/kg`, and `kg/m3`. A suffix is checked as a complete token: an unknown unit is an error and never falls back to SI. Prefer unambiguous compact forms such as `pressure=2bar` and `temperature=20C`. Generic numeric options use the same finite decimal grammar. Boolean values, grid modes, presets, units, and every member of an explicit numeric list are validated; invalid values and reversed explicit bounds raise an error instead of silently selecting a default or repairing the range. ```lua 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)) ``` Metadata contains `id`, normalized `kind`, conserved `constant`, active `reference_state`, and completed `from` and `to` state tables. Completed states contain at least `p`, `h`, `x`, `y`, and `t`, plus every resolvable property among `T`, `s`, `v`, and `q`. When an endpoint lies in the two-phase region, quality is resolved first and the resulting PQ pair is used to obtain the other state properties robustly. When a state contains more than two explicit properties, the resolver freezes the first available independent pair in this deterministic order: `P,Q`, `T,Q`, `P,T`, `P,H`, `P,S`, `P,Dmass`, `T,H`, `T,S`, `T,Dmass`, then `H,S`. It obtains every canonical property from that pair and checks each redundant input. Process resolution also checks the conserved property at both completed endpoints. A contradiction raises a diagnostic with the supplied value, the CoolProp value, their absolute difference, and the applied tolerance; inconsistent user values are never copied into metadata. `ph.process_plot(opts)` returns the serialized plot followed by the same metadata. Styling fields include `process_color`, `process_style`, `mark_endpoints`, `marker_style`, `label`, `label_pos`, and `label_style`. The returned metadata is the direct Lua interface to numerical endpoints; `metadata.from.x`, `metadata.from.y`, `metadata.to.x`, and `metadata.to.y` are the scaled coordinates serialized for PGFPlots. `pv.process_points(opts)` follows the same state parser and metadata contract. Its endpoint `x` fields are scaled specific volumes. Isobars and isochores are exact two-point horizontal and vertical paths; isotherms, isentropes, qualities, and isenthalps are sampled adaptively in log-log coordinates. The public PV process API therefore supports all six canonical process types even though an isochore is not a PV background family. ```lua local lcp = require("luacoolprop") local pv = lcp.diagram.get_type("PV") local points, metadata = pv.process_points({ fluid = "R134a", type = "isentropic", from = "pressure=2bar,quality=1", to = "pressure=12bar", specific_volume_scale = 1, pressure_scale = 1e-5, }) io.write(string.format("%s: %.8g -> %.8g m3/kg (%d samples)\n", metadata.id, metadata.from.v, metadata.to.v, #points)) ``` `ts.process_points(opts)` also supports all six process types. Isotherms and isentropes are exact horizontal and vertical TS segments. Isobars are sampled with entropy as the independent variable, which preserves a two-phase constant-temperature plateau; isochores use temperature, and qualities and isenthalps use logarithmic pressure. Completed endpoints use scaled entropy as `x` and the selected scaled/offset temperature coordinate as `y`; raw `T` remains absolute kelvin. ```lua local lcp = require("luacoolprop") local ts = lcp.diagram.get_type("TS") local points, metadata = ts.process_points({ fluid = "R134a", type = "isentropic", from = "pressure=2bar,quality=1", to = "pressure=12bar", entropy_scale = 1e-3, temperature_scale = 1, }) io.write(string.format("%s: %.8g -> %.8g K (%d samples)\n", metadata.id, metadata.from.T, metadata.to.T, #points)) ``` `hs.process_points(opts)` uses the same six process types and state parser. Isentropes and isenthalps are exact vertical and horizontal HS segments. Isobars use entropy sampling, isotherms insert their exact saturation segment, isochores use temperature, and qualities use logarithmic pressure. Completed endpoints use scaled entropy as `x` and scaled enthalpy as `y`. ```lua local lcp = require("luacoolprop") local hs = lcp.diagram.get_type("HS") local points, metadata = hs.process_points({ fluid = "Water", type = "isentropic", from = "pressure=1bar,quality=1", to = "pressure=10bar", entropy_scale = 1e-3, enthalpy_scale = 1e-3, }) assert(metadata.from.x == metadata.to.x) io.write(string.format("%s: %.8g -> %.8g kJ/kg (%d samples)\n", metadata.id, metadata.from.y, metadata.to.y, #points)) ``` ## Registering another diagram type `diagram.register_type(code, implementation)` validates an uppercase code, rejects duplicate registration, sets `implementation.type`, stores the table, and returns it. `diagram.get_type(code)` returns the registered table or raises an error listing available types. A custom implementation should provide at least `plots`, `axis_style`, `family`, `process_points`, `process_plot`, and a `families` table. Keep numerical renderers independent of the global `tex` table. ```lua 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) ``` Registration is process-local. An extension module should guard against being loaded twice or let the duplicate-registration error expose a packaging bug. It must not modify the built-in PH, PV, TS, HS, or PT implementations. ## TeX bridge `lcp.tex.print_diagram(diagram_type, operation, opts[, family])` selects a registered renderer and writes its result with `tex.sprint`. Operations are `plots`, `axis_style`, `family`, and `process`. `axis_style` is wrapped in a PGFPlots style update; `process` dispatches to `process_plot`. For a process operation, `opts.export_coordinates` may contain a TeX control-sequence prefix without a leading backslash. The bridge then emits global definitions named `FromX`, `FromY`, `ToX`, and `ToY`. Their decimal values use `opts.coord_digits` and therefore match the coordinates in the rendered path exactly. Each endpoint also exports the available suffixes `PressureSI`, `TemperatureK`, `EnthalpySI`, `EntropySI`, `SpecificVolumeSI`, and `Quality`; for example, `ToTemperatureK`. An unavailable property is not defined. Prefixes are restricted to letters, digits, `@`, colon, and underscore, and may not begin with a digit. Set `opts.log_coordinates` to a true boolean spelling to write the path ID and both coordinate pairs through LuaTeX's `term and log` channel. Both facilities are opt-in and apply uniformly to any registered diagram implementation whose process metadata follows the required `from.x`, `from.y`, `to.x`, and `to.y` contract. This function requires an active LuaTeX `tex` table. It is not a standalone `texlua` API. The flat `print_ph_*` functions support the generic TeX layer; generic Lua integrations use `tex.print_diagram`. ## Alternate public names The following aliases are available: - `PropsSI`, `Props1SI`, `HAPropsSI`, and `PhaseSI` mirror CoolProp spelling; - flat `diagram.ph_*` curve and plot functions provide direct PH access; - flat `tex.print_ph_*` functions support existing TeX macros; - abbreviated PH option fields such as `isoquality`, `isotherms`, `h_scale`, and `p_scale` are translated by the shared option normalizer before the PH implementation is called. Generic Lua integrations should use lowercase function names, `diagram.get_type("PH")`, `diagram.get_type("PV")`, `diagram.get_type("TS")`, or `diagram.get_type("HS")`, singular family names, and full property names. ## Contributor checklist When extending the Lua layer: 1. keep module state local and return APIs through `M`; 2. use `local` for functions and temporaries unless they are intentionally public; 3. validate public arguments at the boundary and include the argument name in errors; 4. preserve SI units at the CoolProp boundary; 5. release native handles deterministically and keep finalizers non-throwing; 6. do not catch errors unless the caller can recover or add useful context; 7. keep computation and serialization separate from `tex.sprint`; 8. return tables or strings instead of printing from diagram code; 9. document table fields, defaults, units, side effects, and failure behavior; 10. retain CoolProp as an external shared-library dependency; 11. delegate automatic label placement exclusively to the autonomous `pgfplots-autonode` library; and 12. test Lua syntax plus LuaLaTeX, plain LuaTeX, and ConTeXt MkIV builds.