CoolSolve ships with a single-page web application (SPA) served by an
embedded HTTP server (cpp-httplib) inside the CoolSolve binary. Running
coolsolve --gui starts the server on port 8550 (by default) and opens the
user's default browser — no external runtime dependencies are required.
The GUI works identically in two deployment scenarios:
| Scenario | Backend | Frontend |
|---|---|---|
| Local | CoolSolve binary starts an embedded HTTP server on localhost and opens the user's default browser |
Same SPA served by the embedded server |
| Online | CoolSolve binary behind a reverse-proxy (nginx / Caddy) with session-based sandboxing | Same SPA served by the proxy or CDN |
Design principle: the GUI is a pure frontend layer. All core functionality
stays in the existing libraries (parser.cpp, solver.cpp, evaluator.cpp,
runner.h). The HTTP layer is a thin adapter that calls CoolSolveRunner
exactly as main.cpp does in CLI mode.
| Component | Library | Notes |
|---|---|---|
| HTTP server | cpp-httplib (header-only) | Zero-dependency, MIT-licensed, supports SSE and multipart uploads |
| JSON serialization | nlohmann/json (existing dependency) | REST request/response bodies |
| Real-time progress | Server-Sent Events (SSE) via cpp-httplib chunked content provider | Streams per-block solve progress without adding a WebSocket library |
| Static asset embedding | cmake/embed_assets.cmake custom build step |
Embeds the compiled SPA into the binary as C++ byte arrays |
| Component | Library | Version |
|---|---|---|
| Framework | React + TypeScript | 18.3 / 5.6 |
| Build tool | Vite | 5.4 |
| Code editor | Monaco Editor (@monaco-editor/react) |
4.7 |
| Plotting | Plotly.js (react-plotly.js + plotly.js-basic-dist-min) |
3.3 |
| State management | Zustand | 5.0 |
| Icons | Lucide React | 0.564 |
AG Grid (planned for the variable table) and Radix UI / Tailwind CSS (planned for UI chrome) were considered but not yet adopted — the current UI uses plain React and custom CSS with light/dark theme variables.
An Electron or Tauri-based desktop app was evaluated and rejected:
- Electron ships a full Chromium (~150 MB) and Node.js runtime. CoolSolve already carries CoolProp (~30 MB); adding Electron triples the binary size.
- Tauri requires a Rust toolchain and introduces a second systems language.
- The embedded-server approach delivers the same UX with zero additional runtime dependencies, and the same frontend works online without modification.
| File | Purpose |
|---|---|
src/server.cpp |
HTTP server — all route handlers, session management, asset serving |
include/coolsolve/server.h |
Public API: ServerOptions struct + startServer() declaration |
cmake/embed_assets.cmake |
CMake script: reads gui/dist/*, generates build/embedded_assets.cpp and build/embedded_assets.h |
| File | Change |
|---|---|
main.cpp |
Added --gui [port] and --no-browser flags; calls coolsolve::startServer() |
CMakeLists.txt |
Added COOLSOLVE_BUILD_GUI option (default ON); FetchContent for cpp-httplib; asset embedding build step |
solver.h / solver.cpp |
Added ProgressCallback to SolverOptions for real-time SSE progress |
server.cppis only linked whenCOOLSOLVE_BUILD_GUI=ON.- cpp-httplib is guarded behind
#ifdef COOLSOLVE_GUI. - Frontend assets are embedded as
const unsigned char[]arrays; never touched unless the server starts. - When
progressCallbackisnullptr(CLI mode), the compiler eliminates the null check — zero overhead on every Newton iteration.
All endpoints are under /api/v1/. Request and response bodies are JSON
unless noted otherwise.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/health |
Health check; returns {"status":"ok","coolpropReady":true/false} |
POST |
/api/v1/warmup |
Trigger CoolProp warmup (called automatically on startup) |
On server start, CoolProp warmup runs in a background thread. The frontend
polls /health and shows a loading indicator until coolpropReady is true
(typically 1–3 s on first launch).
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/new |
Create a new empty session (clears code, initials, sol, debug output) |
POST |
/api/v1/back |
Restore the previous session snapshot (one-level undo) |
GET |
/api/v1/model-name |
Get the current model name |
PUT |
/api/v1/model-name |
Set the model name |
Sessions are managed via a coolsolve_session cookie (32-hex-char ID).
Each session gets isolated in-memory state. A SessionSnapshot is saved
before each destructive operation to support the "Back" button.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/files/eescode |
Get current source code |
PUT |
/api/v1/files/eescode |
Set source code; body: {"content":"..."} |
GET |
/api/v1/files/initials |
Get initial guesses text |
PUT |
/api/v1/files/initials |
Set initial guesses text |
GET |
/api/v1/files/sol |
Get last solution file content |
GET |
/api/v1/files/conf |
Get solver configuration (as text) |
PUT |
/api/v1/files/conf |
Set solver configuration |
POST |
/api/v1/files/open |
Open a .eescode file from disk; auto-discovers companion .initials and coolsolve.conf |
GET |
/api/v1/files/bundle |
Download a ZIP bundle (eescode + initials + sol + conf + debug files) |
POST |
/api/v1/files/upload |
Upload a multipart .zip bundle or individual files |
GET |
/api/v1/examples |
List built-in example files |
The file workflow is ZIP-centric:
- Open loads files from disk (local mode) or from an uploaded ZIP.
- Save produces a ZIP bundle containing all companion files plus any debug output.
- Examples are read-only on the server and opened via
POST /api/v1/files/open.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/solve |
Start an async solve; returns immediately with {"status":"started"} |
GET |
/api/v1/solve/result |
Get the last solve result (variables, residuals, timing, blocks) |
GET |
/api/v1/solve/stream |
SSE endpoint: streams blockStart, blockDone, and result events in real time |
POST |
/api/v1/solve/cancel |
Cancel a running solve; returns 409 if no solve is in progress |
POST |
/api/v1/update-guesses |
Copy last .sol → .initials (update guesses from solution) |
The solve runs in a background thread. A ProgressCallback in SolverOptions
pushes events onto the SSE channel between blocks. The frontend uses the
browser's native EventSource API to consume these events.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/parse |
Parse source code (no solve); returns equation/variable counts and parse errors |
GET |
/api/v1/variables |
Get solved variable values (name, value, isArray) |
GET |
/api/v1/variables/inferred |
Get solved variables enriched with inferred property and fluid metadata |
/api/v1/parse is called with a short debounce as the user types in the
editor, enabling real-time syntax error markers.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/debug/files |
List debug output files for the current session |
GET |
/api/v1/debug/file?name=... |
Get content of a specific debug file |
A debug solve is triggered by calling POST /api/v1/solve with
"debug": true in the request body. This runs the solver with
enableTracing=true and calls runner.generateDebugOutput(), producing
~15 files (report.md, equations.md, variables.md, analysis.json,
solver_trace.md, incidence.md, tearing.md, evaluator.md, residuals.txt,
equations.tex, etc.) in a session-isolated temp directory.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/coolprop/fluids |
List all registered fluids with name, type, hasDome, and modelFluids from the current session |
POST |
/api/v1/coolprop/saturation |
Compute saturation dome for a fluid; body: {"fluid":"R134a","nPoints":200}; returns liquid+vapor arrays for T, P, H, S, D and critical/triple-point data |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/parametric |
Start an async parametric sweep; body: {"sweepVariables":[{"name":"T_in","values":[20,25,30]}]}; supports 1D (1 variable) and 2D (2 variables, Cartesian product) |
GET |
/api/v1/parametric/result |
Get the result of the last parametric study |
The parametric study runs in a background thread with warm-starting:
each grid point uses the nearest previously-converged solution as initial
guesses. For the first point, the current .initials file is used. Progress
events are streamed via the existing SSE channel (/api/v1/solve/stream).
The sweep works by modifying the source code at each grid point: imposed
variable assignments (e.g. T_in = 25) are replaced with the sweep value
using regex substitution. Each point runs a full CoolSolveRunner::solve().
Parse endpoint enhancement: POST /api/v1/parse now returns additional
fields per variable:
isImposed(boolean):trueif the variable appears asvar = constantimposedValue(number): the constant value (for imposed variables)unitSource(string):"code","inferred", or""indicating unit origin
Bundle persistence: ZIP bundles now include parametric_studies.json
containing saved parametric study results for load/restore round-trips.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/tables |
List all lookup tables in the current session; returns [{name, columns, rows}] |
GET |
/api/v1/tables/:name |
Get the raw CSV content of a named table |
PUT |
/api/v1/tables/:name |
Create or replace a table; body is plain text/csv |
DELETE |
/api/v1/tables/:name |
Remove a table from the session |
Table names are restricted to [A-Za-z0-9_-]+. Tables are stored in the
session and written to the solver's working directory as <name>.csv before
each solve or parametric run. They are included in the ZIP bundle and
restored on upload.
When a model contains an equation-based INTEGRAL(...) call, the solve produces
a time-series trajectory. It is delivered to the frontend in two forms and rides
on the standard solve response — no separate solve call is needed:
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/integral/result |
Last integral solve as columnar JSON (integrationVar, columns, data, numRows, csvName, totalSteps, rejectedSteps); 404 when no integral has been solved |
GET |
/api/v1/integral/csv |
Raw trajectory CSV text (text/csv); 404 when none. Exposes the CSV restored from a bundle round-trip where the columnar JSON is not available |
In addition, the POST /api/v1/solve "done" SSE event and
GET /api/v1/solve/result both carry optional integralTable (columnar) and
integralCsvName fields whenever an integral was solved. The auto-written
<model>-integral.csv is exported in the ZIP bundle (see §7.2) and restored to
the session on upload, so the Integral tab repopulates on a bundle round-trip
(download → upload → same columns and rows).
Session state is stored in a std::map<string, shared_ptr<Session>>, keyed
by a 32-hex-char ID. The session ID is issued as a coolsolve_session cookie
on the first request and persists across calls.
Each Session holds:
eescode,initials,sol,conf— current file content stringslastResult— lastSolveResultfrom the solverinferredVariables— enriched variable list (property, fluid, units) extracted after solvemodelFluids— unique fluids detected via variable inferencesessionSnapshot— one-level undo snapshotdebugDir— temporary directory path for debug output filessolveInProgress,cancelToken— concurrency control for async solvelookupTableCSVs— map from table name to raw CSV text (in-memory store for lookup tables)lastIntegralResult/lastIntegralCSV/lastIntegralCsvName— last integral-solve trajectory (columnar JSON + raw CSV), guarded by a dedicated mutex; cleared on new/open/reset
Session-scoped temp directories live at /tmp/coolsolve_sessions/{id}/.
The "New" action clears all in-memory state while keeping the session alive.
┌──────────────────────────────────────────────────────────────────────────┐
│ TOOLBAR │
│ [← Back] [New] [📂 Open] [⬇ Save] [▶ Solve] [🐛 Debug] [⏹ Stop] │
│ [↻ Update Guesses] [{ } Comment] [" " Comment] [☀/☾ Theme] │
├────────────────────────────┬─────────────────────────────────────────────┤
│ │ │
│ CODE EDITOR (Monaco) │ RIGHT PANEL (Tabs) │
│ │ ┌──────────────────────────────────────┐ │
│ T_in = 25 │ │Variables│Arrays│Config│Debug│Diagrams│ │
│ │ │ (right panel unchanged) │ │
│ P = 101325 │ ├──────────────────────────────────────┤ │
│ h = enthalpy('Water', │ │ Variable table (filtered, sortable) │ │
│ T=T_in, P=P) │ │ ┌──────┬──────┬───────┬──────┐ │ │
│ ... │ │ │ Name │Value │Initial│Units │ │ │
│ │ │ ├──────┼──────┼───────┼──────┤ │ │
│ │ │ │ T_in │ 25 │ 25 │ │ │ │
│ │ │ │ P │101325│101325 │ │ │ │
│ │ │ │ h │104929│ — │ │ │ │
│ │ └──────────────────────────────────────┘ │
├────────────────────────────┴─────────────────────────────────────────────┤
│ BOTTOM PANEL (collapsible, tabbed) │
│ Console │ Parametric │ Lookup Tables │ Integral │
└──────────────────────────────────────────────────────────────────────────┘
Layout is managed by SplitPane.tsx, a reusable resizable split-pane
component (horizontal/vertical, min/max constraints, drag divider). The
editor/right-panel and main/console splits are both independently resizable.
Implemented in Toolbar.tsx. All actions include keyboard shortcuts:
| Action | Shortcut | Description |
|---|---|---|
| Back | — | Restore previous session snapshot |
| New | — | Clear editor and start fresh |
| Open | Ctrl+O |
Load .eescode from disk + companion files |
| Save | Ctrl+S |
Download ZIP bundle |
| Solve | Ctrl+Enter |
Run async solve, stream progress via SSE |
| Try Harder | Ctrl+Enter (after a failure) |
Re-run with deepSearch: true — substitutes deepSearchPipeline, forces tearing + symbolic reduction on, and consults multiStartMode. The Solve button morphs into Try Harder automatically when the previous run failed; editing the model, initials, or config reverts it to Solve. |
| Debug Solve | Ctrl+Shift+Enter |
Solve with tracing enabled, generate debug output |
| Stop | Escape |
Cancel running solve |
| Update Guesses | Ctrl+G |
Copy solved values into initial guesses |
Toggle { } Comment |
Ctrl+/ |
Wrap/unwrap selection in brace comments |
Toggle " " Comment |
Ctrl+Shift+/ |
Wrap/unwrap selection in double-quote comments |
| Theme | — | Toggle light/dark theme (persisted in localStorage) |
Monaco Editor with a custom language definition (languages/ees.ts):
- Keywords:
FUNCTION,PROCEDURE,END,CALL,IF,THEN,ELSE,ENDIF,REPEAT,UNTIL,GOTO,$ifnot,$endif,$common,$include - Built-in functions (highlighted as
support.function):enthalpy,entropy,density,pressure,temperature,quality,cp,cv,specheat,conductivity,viscosity,volume,soundspeed,surfacetension,sin,cos,tan,exp,log,log10,sqrt,abs,min,max,ln - Comment styles:
"..."(visible/documentation),{...}(invisible),//...(line comment) - String literals:
'R134a','Water'(fluid names) - Number literals: integer and floating-point with optional exponent
- Bracket matching, auto-indentation, find/replace, minimap
Real-time parse feedback: a debounced POST /api/v1/parse call on every
edit returns parse errors, which are shown as red squiggly underlines via
Monaco setModelMarkers.
Comment toggle (toolbar buttons + keyboard shortcuts):
{ }invisible comment: wraps selected lines in{ ... }. If already wrapped, removes the braces. Uses MonacoexecuteEdits." "visible comment: wraps selection in"...". Idempotent toggle.
A filterable HTML table (search box at top) with columns:
| Column | Editable | Description |
|---|---|---|
| Name | No | Variable name; array vars (T[1], T[2]) grouped visually |
| Value | No | Solved value (blank before solving) |
| Initial | Yes | Editable initial guess; parses .initials text, commits on Enter/blur via PUT /api/v1/files/initials |
| Units | No | Unit annotation from inferred metadata (where available) |
A spreadsheet grid for variables of the form var[i]:
- Rows = integer indices (sorted numerically)
- Columns = distinct base names (sorted alphabetically)
- Array detection regex:
^[A-Za-z_]\w*\[\d+\]$ - Filter input at top
- Shows "N arrays × M indices" count
- "Copy to Clipboard" exports as TSV
If no array variables exist, a placeholder message is shown.
A form-based editor for coolsolve.conf values, organised into collapsible
groups covering all SolverOptions keys (~40+ fields):
| Group | Key fields |
|---|---|
| Main Iteration | maxIterations, tolerance, relativeTolerance, verbose |
| Line Search | lsAlpha, lsRho, lsMaxIterations |
| Trust Region | trInitialRadius, trMaxRadius, trEta |
| Levenberg-Marquardt | lmInitialLambda, lmLambdaFactor |
| Partitioned Solver | partitionedMaxIterations, partitionedTolerance |
| Tearing | enableTearing, tearingMinBlockSize, tearingMethod |
| Pipeline | solverPipeline (ordered list), pipelineMode (radio) |
| Deep Search Pipeline | deepSearchPipeline, deepSearchPipelineMode — used by the Try Harder button |
| Multi-Start | multiStartMode (always / deepsearch / never), multiStartMaxRestarts, multiStartNumCores |
| Safety | timeoutSeconds |
| Output | writeSolFile, writeResiduals |
| Integration | integralMethod, integralFixedStep, integralMaxSteps, integralRelTol, integralAbsTol, integralMinStep, integralMaxStep, integralRichardson, integralOutputInterval |
Changed values are highlighted. A "Reset All" button restores defaults.
Changes sync to the session via PUT /api/v1/files/conf on "Apply" or "Solve".
Displayed after a Debug Solve. Shows the contents of the debug output folder:
- File list panel (priority-sorted: report.md first, then equations.md, variables.md, etc.)
- Content viewer for the selected file
- Refresh button
Files typically generated (~15):
report.md, equations.md, variables.md, analysis.json,
solver_trace.md, incidence.md, tearing.md, evaluator.md,
residuals.txt, equations.tex, and block-level debug files.
Interactive thermodynamic property diagrams powered by Plotly. The tab is located in the right panel alongside Variables, Arrays, Config, and Debug.
- Fluid dropdown: populated from
GET /api/v1/coolprop/fluids. Model fluids (detected via variable inference) appear first. Only fluids withhasDome: true(i.e. CoolPropRealtype) are selectable; ideal gases and humid air are shown greyed out. - Diagram type: T-s, P-h, h-s, T-h (P-h uses logarithmic y-axis)
- Generate button: calls
POST /api/v1/coolprop/saturation; shows spinner during computation (~100–300 ms for 200 points)
The backend generates the dome using N logarithmically spaced temperature points between the triple point and critical point:
- For each T: compute Q=0 (liquid) and Q=1 (vapor) values of P, H, S, D
via
CoolProp::PropsSI() - Concatenate liquid branch + critical point + vapor branch
- Return arrays for T, P, H, S, D plus critical and triple-point data (all SI units)
Each dome generation requires ~200 × 8 = 1,600 PropsSI calls, taking
approximately 0.1–0.3 s in practice — acceptable for a user-initiated action.
After solving, checking "Overlay solved states" calls
GET /api/v1/variables/inferred and:
- Filters variables matching the selected fluid (
inferredFluid == selectedFluid) - Groups by index
ifor array variables, or by name-pattern matching for scalars - Plots each group as a labeled scatter marker on the diagram (where both axis properties are directly available from inference)
Checking "Overlay array path" lets the user select X and Y base variable names. The component:
- Extracts paired values
(X[i], Y[i])for all matching indices - Sorts by index order
- Plots as a connected scatter+line trace with index labels
- Optional "Close cycle" checkbox connects the last point back to the first
PNG and SVG export via Plotly's built-in Plotly.downloadImage().
Auto-scrolling log panel with color-coded lines:
- Parse results (equation/variable count, errors)
- Solve progress via SSE (block N/M, iteration, residual norm)
- Final result: SUCCESS / FAIL with timing breakdown
- CoolProp warnings and errors
A spreadsheet-style manager for CSV lookup tables in the bottom panel.
Displays a table of all tables in the current session (name, row count,
column names). Populated by GET /api/v1/tables automatically after each
successful solve and when the tab is opened.
- Create: enter a table name and press Enter or click Create; an empty
2-column template is written to the session via
PUT /api/v1/tables/:name. - Delete: click the trash icon; prompts for confirmation.
- Open: click any row to open the grid editor for that table.
An inline editable HTML table:
- Header row cells are editable (rename columns).
- Data cells are plain text inputs (numeric or empty for NaN).
- + Row / + Col buttons append rows and columns.
- × buttons (per row/column) delete that row or column (header row is protected; at least 1 data row and 1 column are always kept).
- Import CSV: loads a
.csvfile from disk, replacing the current grid. - Export CSV: downloads the current grid as a
.csvfile. - Save: calls
PUT /api/v1/tables/:namewith the serialised CSV and updates the in-memory store (modelStore.lookupTableCSVs).
Tables saved here are automatically used in the next solve — no need to restart or re-upload files.
A read-only viewer for the trajectory of an equation-based dynamic (INTEGRAL)
model, shown as a sibling Integral tab in the bottom panel (alongside
Console / Parametric / Lookup Tables). It mirrors the Parametric study UX.
The tab resolves its data from two sources, in priority order:
- The live columnar JSON from the solve response (
integralTable). - The restored CSV text (
integralCSV), parsed client-side with a small CSV splitter that honours quoting — this is the path taken after a pure bundle load (no live solve since server restart), where only the CSV survives the round-trip.
Contents:
- A scrollable columnar table with the integration variable as the first
(highlighted) column, followed by the
$IntegralTablevariables. - A row/step count and, when available, the accepted/rejected step counts (RK45).
- A Y-variable selector +
PlotlyChartline plot of one tabulated variable versus the integration variable. - An Export CSV button that re-serialises the table to a downloadable
file named after the model (
<csvName>).
The tab is read-only (EES Integral Tables are not user-editable). An empty-state message is shown when no integral has been solved. It does not auto-switch on solve — it only ensures the bottom panel is open — so the user keeps console focus during a solve; the tab populates immediately once clicked.
CoolSolve works directly with files on disk:
my_model/
├── my_model.eescode # Source code
├── my_model.initials # Initial guesses
├── my_model.sol # Solution (generated)
├── coolsolve.conf # Solver configuration
└── my_model_coolsolve/ # Debug output (generated)
- Open (
POST /api/v1/files/open): reads.eescodeand auto-discovers.initials,.sol, andcoolsolve.confin the same directory. - Save (
GET /api/v1/files/bundle): downloads a ZIP bundle named<model_name>_coolsolve.zip, containing eescode + initials + sol + conf + debug files. The browser's File System Access API (showSaveFilePicker) is used when available, with a download-prompt fallback.
The folder is the project — no proprietary format is needed.
The ZIP uses a minimal uncompressed implementation (custom CRC32-based ZIP
writer/reader in server.cpp — no external library):
| Entry | Contents |
|---|---|
<name>.eescode |
Current editor content |
<name>.initials |
Current initial guesses |
<name>.sol |
Last solution (if solve was successful) |
coolsolve.conf |
Solver configuration |
<tablename>.csv |
One entry per lookup table in the session |
<name>-integral.csv |
Integral trajectory (when an INTEGRAL model was solved) |
debug/* |
Debug output files (if present) |
Before each destructive operation (New, Open, Upload), the current session
state is saved to a SessionSnapshot. The POST /api/v1/back endpoint
restores it. This provides one-level undo.
On server start, a background thread calls warmupCoolProp():
Server start
└── warmupCoolProp() // ~1–3 s
└── PropsSI("T", "P", 101325, "Q", 0, "Water")
The frontend polls GET /api/v1/health and shows "Initialising thermodynamic
engine…" until coolpropReady is true.
CoolProp AbstractState instances are cached per fluid (via
CoolPropConfig::cacheEnabled). Each solve task runs in its own background
thread with its own CoolSolveRunner instance — no global state is shared.
inferVariables(IR&) is called as step 3 of every runner.run() (regardless
of whether debug mode is on). It populates VariableInfo.inferredProperty
(T/P/H/S/D/Q/V/L/C) and VariableInfo.inferredFluid for every variable that
appears as an argument to a thermodynamic function.
After the solve thread completes, the server extracts this data into the
session's inferredVariables and modelFluids fields (~0.1 ms overhead).
This extraction is the only new per-solve cost — negligible.
The FluidRegistry (src/fluids.cpp) registers 90+ real fluids, 11 ideal
gases, 2 incompressibles, and 2 mixtures. Only fluids of type FluidType::Real
have saturation curves and are enabled in the diagram fluid selector.
cmake/embed_assets.cmake runs at CMake configure time:
- Reads all files under
gui/dist/ - For each file, generates a
const unsigned char[]array inbuild/embedded_assets.cppwith the correct MIME type - Generates a
lookupEmbeddedAsset(path)function inbuild/embedded_assets.h
The server's catch-all route (GET /.*) serves embedded assets when built
with COOLSOLVE_EMBEDDED_ASSETS defined, or reads from the filesystem at
COOLSOLVE_GUI_DEV_DIR during development.
# Terminal 1: start backend
cd build && cmake .. && make -j$(nproc) coolsolve
./coolsolve --gui --no-browser
# Backend at http://localhost:8550
# Terminal 2: start Vite dev server
cd gui && npm install && npm run dev
# Frontend at http://localhost:5173
# API calls proxied to port 8550 via vite.config.ts# Build frontend
cd gui && npm run build
# Produces gui/dist/ (~5 MB JS + ~10 kB CSS)
# Rebuild backend — CMake embeds gui/dist/ into the binary
cd build && cmake .. && make -j$(nproc) coolsolve
# Run
./coolsolve --gui
# Opens browser at http://localhost:8550| Option | Default | Effect |
|---|---|---|
COOLSOLVE_BUILD_GUI |
ON |
Compile server.cpp, link cpp-httplib, embed frontend assets |
When OFF, the binary is identical to the pre-GUI version in all respects.
The server's catch-all route (GET /.*) returns index.html (HTTP 200) for
any non-API path not found in embedded assets. This supports client-side
routing if it is ever added.
gui/
├── package.json # Dependencies: react, @monaco-editor/react, zustand, lucide-react, plotly.js-basic-dist-min
├── tsconfig.json / tsconfig.app.json / tsconfig.node.json
├── vite.config.ts # Vite config; /api proxy → port 8550
├── eslint.config.js
├── index.html # HTML entry point
├── public/
│ └── favicon.svg
└── src/
├── main.tsx # React entry point
├── App.tsx # Root layout: toolbar + resizable split panes + tabs
├── App.css # Full CSS with light/dark theme variables
├── plotly.d.ts # Plotly type declarations
├── vite-env.d.ts
├── api/
│ ├── client.ts # Typed fetch wrappers for all API endpoints
│ └── types.ts # TypeScript interfaces for all API responses (incl. parametric types)
├── stores/
│ ├── modelStore.ts # Zustand: eescode, initials, sol, conf, parse errors, solve result, parametric studies, lookup tables, integral table, user unit overrides
│ └── uiStore.ts # Zustand: theme, active tabs (incl. 'lookuptables', 'integral'), panel visibility (persisted to localStorage)
├── languages/
│ └── ees.ts # Monaco Monarch language definition for CoolSolve syntax
└── components/
├── Toolbar.tsx # Top toolbar with all actions + keyboard shortcuts
├── CodeEditor.tsx # Monaco editor wrapper with syntax highlighting + debounced parse
├── VariableTable.tsx # Variable table with units column (color-coded), imposed badges, filter modes
├── ArrayTable.tsx # Spreadsheet view for array variables (var[i])
├── ConfigEditor.tsx # Form-based coolsolve.conf editor (9 groups, 30+ fields)
├── DebugViewer.tsx # Debug output file list + content viewer
├── Console.tsx # Auto-scrolling console with color-coded log lines
├── ThermoDiagram.tsx # Plotly thermo diagrams (T-s, P-h, h-s, T-h) with overlays + export
├── ParametricStudy.tsx # Parametric sweep: variable selector, range inputs, 1D/2D plots, results table
├── LookupTableEditor.tsx # Lookup table manager: list view + inline editable grid + CSV import/export
├── IntegralTable.tsx # Read-only trajectory viewer for INTEGRAL models: table + Plotly plot + CSV export
├── SplitPane.tsx # Reusable resizable split-pane (horizontal/vertical)
└── PlotlyChart.tsx # Thin Plotly wrapper component
All the following tests pass against the current codebase:
| Test | Result |
|---|---|
| Backend compilation (C++17) | ✅ Pass |
| All C++ unit tests (708 assertions, 94 cases) | ✅ Pass |
GET /api/v1/health |
✅ {"status":"ok","coolpropReady":true} |
POST /api/v1/parse with source code |
✅ Returns equation/variable counts |
POST /api/v1/files/open with example file |
✅ Loads file + discovers companion files |
POST /api/v1/solve with rankine1 example |
✅ Full solve result with block details |
GET /api/v1/variables |
✅ Returns solved variable values |
GET /api/v1/solve/stream (SSE, curl) |
✅ 30/30 blocks streamed; final result JSON embedded |
POST /api/v1/solve async return |
✅ Returns {"status":"started"} immediately |
POST /api/v1/files/save-as |
✅ Creates file at new path with companion files |
POST /api/v1/solve/cancel (no solve) |
✅ Returns 409 "No solve is in progress" |
POST /api/v1/solve/cancel (during solve) |
✅ Cancels at block 79/112; SSE shows "Solve cancelled by user" |
| Session cookie | ✅ Set-Cookie: coolsolve_session=... on first request |
| Session isolation | ✅ New session gets empty state; original retains files |
| ZIP bundle download | ✅ Valid ZIP, 4 files, 6972 bytes |
| ZIP bundle upload | ✅ Re-uploaded ZIP; all companion files detected |
| Debug files (before solve) | ✅ {"files":[]} |
| Debug files (after debug solve) | ✅ 15+ files generated |
Debug file content (report.md) |
✅ Full analysis content |
GET /api/v1/coolprop/fluids |
✅ 128 fluids; Water hasDome:true; Air hasDome:false |
GET /api/v1/coolprop/fluids (modelFluids, before parse) |
✅ "modelFluids":[] |
POST /api/v1/coolprop/saturation — Water, 50 pts |
✅ critical.T≈647.1 K (raw SI from CoolProp; converted to °C in the JSON response), computeTime_ms≈67 |
POST /api/v1/coolprop/saturation — R134a, 100 pts |
✅ critical.T≈374.2 K (raw SI from CoolProp; converted to °C in the JSON response) |
POST /api/v1/coolprop/saturation — invalid fluid |
✅ HTTP 400 |
POST /api/v1/coolprop/saturation — Air (ideal gas) |
✅ HTTP 400 "no dome" |
GET /api/v1/variables/inferred (before solve) |
✅ 0 variables |
GET /api/v1/variables/inferred (after solve) |
✅ 39 vars with T/P/H/S/Q for R134a |
GET /api/v1/coolprop/fluids (modelFluids after solve) |
✅ "modelFluids":["R134a"] |
| modelFluids cleared on new model | ✅ Reset to [] after POST /api/v1/new |
| Test | Result |
|---|---|
| TypeScript type-check (zero errors) | ✅ Pass |
Production build (npm run build) |
✅ ~5 MB JS (includes plotly.js) + 9.4 kB CSS in gui/dist/ |
| Embedded assets (index.html, JS, CSS, SVG) | ✅ Served with correct MIME types |
| SPA fallback routing | ✅ Unknown paths return index.html (HTTP 200) |
| React app load from embedded binary | ✅ Pass (end-to-end) |
coolsolve --gui [port] [--no-browser]
Opens the default browser at http://localhost:<port> after a short warmup.
--no-browser suppresses automatic browser launch (useful for headless
environments or when tunnelling).
Browser ──HTTPS──► nginx/Caddy (reverse proxy) ──HTTP──► coolsolve --gui
A Docker image packages the binary with the embedded frontend:
FROM ubuntu:24.04 AS runtime
COPY coolsolve /usr/local/bin/coolsolve
EXPOSE 8550
ENTRYPOINT ["coolsolve", "--gui", "8550"]| Concern | Solution |
|---|---|
| Browser opening | xdg-open (Linux), open (macOS), start (Windows) |
| File paths | Normalised with std::filesystem; REST API always uses forward slashes |
| Port conflicts | Default 8550; if busy, prints error and exits — auto-increment is a future improvement |
| Binary distribution | Static linking of CoolProp + cpp-httplib + embedded frontend; single binary for Linux x86_64, macOS arm64/x86_64, Windows x86_64 |
| Threat | Mitigation |
|---|---|
| Arbitrary code execution | CoolSolve only parses .eescode — the PEG grammar is the sandbox; no eval or shell execution |
| Resource exhaustion | Per-session timeoutSeconds, session expiry on idle |
| File system access | Online sessions use isolated temp directories; /files/open works with uploaded content only |
| Input size | Max upload size enforced by cpp-httplib |
End-to-end verification without a browser:
# 1. Build backend
cd build && cmake -DCOOLSOLVE_BUILD_GUI=ON .. && make -j$(nproc)
# 2. Run C++ unit tests
./coolsolve_tests
# 3. Build frontend
cd ../gui && npm ci && npm run build
# 4. Re-embed assets and rebuild binary
cd ../build && make -j$(nproc)
# 5. Start server in background
./coolsolve --gui 8551 &
SERVER_PID=$!
sleep 4 # CoolProp warmup
# 6. Run API integration tests
bash check.sh
# 7. Stop server
kill $SERVER_PIDEvery step has a binary pass/fail exit code. Steps 1–4 are decoupled and can be run independently.
The following items are not yet implemented. They are listed roughly in priority order.
POST /api/v1/report endpoint to:
- Generate a
.texreport from the current model (variables, equations, solve results) using the existingrunner.generateDebugOutput()data - Download as
.texsource or, ifpdflatexis available on the server, as a compiled PDF - Frontend: "LaTeX Report" button in the toolbar with format selector
Open question: server-side pdflatex vs. client-side WASM LaTeX engine
(e.g. SwiftLaTeX) for PDF compilation.
| Item | Description |
|---|---|
| Session expiry | Automatically destroy sessions after N minutes of inactivity; clean up temp dirs |
| Docker image | Publish a minimal Docker image (ubuntu:24.04 base with static binary) |
| nginx config | Reference reverse-proxy config with TLS, rate limiting, max body size |
| Authentication | OAuth (GitHub/Google) for a multi-user hosted service; cookie sessions are sufficient for a public demo |
The core parametric sensitivity analysis feature is now implemented (see sections below). Remaining enhancements:
| Item | Description |
|---|---|
| CSV export | Download sweep results as CSV |
| Multi-output plots | Multiple output variables on the same 1D chart |
| 3D surface plots | plotly.js-basic-dist-min → plotly.js-dist-min for 3D surfaces (currently using heatmap/contour) |
| Progress bar | Visual progress bar during parametric study (currently shows text status) |
| Cancel button | Abort a running parametric study via POST /api/v1/parametric/cancel |
- Imposed variable detection: Parse endpoint detects
var = constantpatterns and reportsisImposed,imposedValueper variable - Units column: Variable table shows units with color coding — blue (code-specified), green (inferred from CoolProp), orange (user-specified)
- Parametric sweep API:
POST /api/v1/parametricaccepts 1 or 2 sweep variables, runs grid with warm-starting in background thread, sends progress via SSE, results viaGET /api/v1/parametric/result - Parametric UI: Bottom panel "Parametric" tab with sweep variable selectors, range inputs, 1D line plots and 2D contour/heatmap plots (Plotly), results table with swept variables highlighted
- Bundle persistence: Parametric study results are saved/loaded in ZIP
bundles (
parametric_studies.json)
These are extensions to the already-implemented ThermoDiagram.tsx:
| Item | Description |
|---|---|
| Unit conversion | Display properties in additional user-friendly units (kPa, kJ/kg, …) alongside the current °C/Pa/J defaults |
| Isolines | Isotherms, isobars, iso-quality lines on the saturation dome |
| Multiple fluids | Overlay domes for two fluids on the same chart for comparison |
| CoolProp.js (WASM) | Compute missing state properties in the browser without backend calls |
| Interactive hover | Show property values on hover over dome or state points |
| Diagram state persistence | Save/restore diagram configuration in the ZIP bundle |
Replace the current plain HTML table in VariableTable.tsx with AG Grid
Community for:
- Sorting and multi-column filtering
- Column pinning and reordering
- Faster rendering for models with hundreds of variables
- In-cell editing with validation
| Item | Description |
|---|---|
| Auto-save | In local mode, auto-save editor content every 30 s to .coolsolve.autosave; offer recovery on next startup |
| Variable quick-jump | Click variable in table → highlight in editor; Ctrl+Click variable in editor → popup showing value/units/determining equation |
| Equation dependency graph | D3.js or Mermaid graph of block structure: which blocks depend on which, block sizes, converged/failed status |
| Smart initial guesses | Highlight variables with no initial guess in orange; suggest ranges for thermodynamic properties based on fluid and context |
| Diff view | After solving, side-by-side diff of initial guesses vs. converged values; large jumps highlighted |
| Unit conversion helper | Small utility panel for common unit conversions (°C↔K, bar↔Pa, kJ↔J) |
| Export to Python/Julia | Generate a Python or Julia script replicating the model using CoolProp bindings |
| Responsive layout | On narrow screens (tablets), right panel moves below editor; bottom panel collapses to a status bar |
| Port auto-increment | If default port 8550 is busy, try 8551, 8552, … and print the actual URL |
| Item | Description |
|---|---|
| Vitest unit tests | VariableTable.test.tsx, ArrayTable.test.tsx, CodeEditor.test.tsx, ConfigEditor.test.tsx, stores/modelStore.test.ts, utils/arrayVars.test.ts |
| Playwright E2E tests | solve-flow.spec.ts, file-roundtrip.spec.ts, comment-toggle.spec.ts, config-edit.spec.ts |
| Example sweep script | tests/test_all_examples_api.sh — solve every example in examples/ via the REST API |
| CI pipeline | GitHub Actions workflow: backend build + unit tests, frontend build + unit tests, API integration sweep |
For future sensitivity and plotting features, a richer .coolsolve project
format (JSON) could bundle:
- CoolSolve source code
- Initial guesses and solver configuration
- A list of "analysis cells" (sensitivity sweeps, diagram configs)
- Cached results (last solution, sweep data)
This is deferred until the sensitivity analysis feature is designed in detail.
| Item | Description |
|---|---|
| WASM build | Compile CoolSolve + CoolProp to WebAssembly — run entirely in the browser with no server |
| Multi-model comparison | Solve two .eescode files side by side |
| Collaborative editing | Online shared sessions via CRDT or OT |
| Mobile support | Responsive breakpoints for tablet use (code editor experience on phones is inherently limited) |