Backend Options#
Per-instance backend configuration is passed through the factory string as a JSON suffix. The mechanism is opt-in per backend, immutable after construction, available identically from every language that wraps CoolProp, and works through both the high-level and low-level interfaces.
Grammar#
Append ?<options> to the factory string:
SVDSBTL&HEOS::Water ← no options, all schema defaults
SVDSBTL&HEOS::Water?{"critical_patch":"off"} ← inline JSON
SVDSBTL&HEOS::Water?@/path/to/cfg.json ← file indirection
HEOS::Water?{} ← empty options (no-op)
The parser splits on the first ? and treats the entire remainder
as a single JSON value (or, if it begins with @, as a file path
read verbatim). Any subsequent ? characters — inside a JSON string
value, in a URL embedded in a value, in a file path after @ — are
preserved as-is.
Options live in either the backend or the fluid half of the factory
string (whichever your call site finds convenient). If both halves
carry an options suffix, factory construction throws a ValueError
to flag the typo.
Use from the high-level interfaces#
The factory string is parsed in exactly one place inside CoolProp
(AbstractState::factory). Every wrapper that takes a fluid name
forwards it verbatim, so the ?<options> syntax works from any
language with no per-wrapper changes:
Python
from CoolProp.CoolProp import PropsSI
PropsSI("D", "T", 300, "P", 1e5,
'SVDSBTL&HEOS::Water?{"critical_patch":"off"}')
FORTRAN
d = PropsSI('D'//c_null_char, 'T'//c_null_char, 300.0_dp, &
'P'//c_null_char, 1.0e5_dp, &
'SVDSBTL&HEOS::Water?@/opt/coolprop/h2o.json'//c_null_char)
Excel / LibreOffice Calc
=PropsSI("D";"T";300;"P";1E5;"SVDSBTL&HEOS::Water?@C:\Configs\h2o.json")
MATLAB
d = py.CoolProp.CoolProp.PropsSI('D','T',300,'P',1e5, ...
'SVDSBTL&HEOS::Water?@~/coolprop/opts.json');
For Excel / FORTRAN / similar interfaces where embedding inline JSON
gets painful (quote escaping, cell length limits, etc.), the
?@<path> form is the recommended path — write the JSON once to a
file, reference the path on every call.
Strict validation#
Every options-aware backend ships a JSON Schema (Draft 2020-12) that the factory validates against on every call:
Unknown keys throw — typos surface immediately rather than silently defaulting.
Type mismatches throw — strings vs numbers vs booleans are checked per the schema.
Required keys missing throw with the dotted path of the missing field.
Enum violations throw with the offending value plus the allowed set.
Schemas live alongside the headers in
include/CoolProp/schemas/<backend>.schema.json. Each opted-in
backend exposes its schema as a constant string literal compiled into
the binary — no runtime file lookups, no version-skew between docs and
runtime.
The schema is itself versioned ("schema": 1 at the top level) so
the layout can evolve with explicit migration support.
Reproducibility — build_options_json() and cache keys#
Two callers need a single canonical form per logically-equal options value: cache filename hashing (so two callers passing the same options in different key orders hit the same cache file) and reproducibility logging. The shared helper:
Sorts object keys recursively.
Preserves array order.
Uses RapidJSON’s default formatting for scalars (deterministic within a single CoolProp build).
It is not strict RFC 8785 (JCS) — no NFC string normalisation, no ECMAScript number rounding — but it’s deterministic within a single CoolProp build, which is all the cache key needs.
Every AbstractState exposes:
virtual std::string build_options_json() const;
Returns the canonical-JSON string the instance was built with
(defaults expanded). Default is the empty string; backends that
accept options override to return the canonical form so callers can
copy-paste it into a fresh factory() call and reproduce the
construction byte-for-byte.
For caching backends (SVDSBTL today), the cache filename incorporates
a 16-hex-character FNV-1a 64 prefix of the canonical bytes alongside
fluid name, source backend, and symbolic input-pair name. FNV-1a 64
is the same hash family CoolProp already uses for the superancillary
source_eos_hash freshness stamp — no new dependencies, identical
determinism guarantees across compilers.
Opting a backend in#
Backends that want to consume options publish a JSON Schema and
override the options-aware AbstractStateGenerator virtual:
class MyBackendGenerator : public AbstractStateGenerator {
public:
AbstractState* get_AbstractState(
const std::vector<std::string>& fluid_names,
const std::string& options_json) override {
rapidjson::Document opts;
if (!options_json.empty()) opts.Parse(options_json);
else opts.SetObject();
validate_against_schema(opts, kMyBackendOptionsSchemaJson);
// ...construct using the parsed options...
}
AbstractState* get_AbstractState(
const std::vector<std::string>& fluid_names) override {
return get_AbstractState(fluid_names, "");
}
};
The default implementation of the options-aware overload forwards to
the no-options overload when options_json is empty / "{}" and
throws NotImplementedError otherwise — so backends that haven’t
opted in reject options loudly instead of silently dropping them.
Migration from Configuration globals#
The Configuration mechanism is process-global and stringly-typed.
Several backend knobs that are currently Configuration globals are
better expressed as per-instance options. Over time the following
mappings will land as deprecations of the corresponding globals:
Backend |
Today (Configuration global) |
After migration (options key) |
|---|---|---|
SVDSBTL |
(none — new backend) |
|
BICUBIC |
|
|
TTSE |
|
|
REFPROP |
|
|
Each follow-up PR ships its backend’s schema, an opt-in for the options-aware overload, and a deprecation warning emitted whenever the corresponding Configuration global is set. Existing scripts that rely on the Configuration globals keep working through at least one release cycle.
Design rationale#
A short tour of why the design lands where it does:
Why not mutators?
set_<knob>()methods make caching brittle (cache key would have to reflect every post-construction setting) and pessimise reproducibility. The factory-string form is the single source of truth, baked into the cache key, and round-trips throughbuild_options_json().Why not Configuration globals? Process-global state forbids mixing two backends with different policies in the same process, silently inherits across unrelated code, and rides outside the cache key. The factory-string form is per-instance and explicit.
Why not a builder API? Wrapper bridges (SWIG, Cython, Mathematica) would each need per-method bindings. The string-only factory entry-point requires zero new wrapper code.
Why JSON over query-string (“``?critical_patch=off&NT=200``”)? JSON gives typed nested structures (
critical_patch.tolerancenext tocritical_patch.bboxwithout flat-key collisions), and the schema evolution story is straightforward. The high-level?@path.jsonindirection sidesteps the inline-quoting tax.Why split on the first ``?`` only? Any other rule re-introduces parser ambiguity for URLs and regexes inside JSON string values. JSON’s quoting rules are the source of truth for everything after the first
?.
See the design document
docs/superpowers/specs/2026-05-16-backend-options-string-design.md
for the full decision trail.