This software is a C++-based thermodynamic analysis system for Rankine cycles. It adopts a modular design, uses JSON configuration files to describe the thermodynamic system structure, and implements automatic creation of equipment objects, port data sharing, and iterative cycle calculations.
- JSON-Driven Configuration: Describes system structure and parameters through JSON files
- Equipment Factory Pattern: Automatically creates and manages equipment objects
- Port Data Sharing: Achieves automatic data synchronization between equipment through pointer sharing
- Polling Device-Readiness: Finds calculation order by repeatedly traversing and computing ready equipment
- IAPWS-IF97 Standard: Uses
seuif97library for accurate thermodynamic property calculations - Modular Design: Easy to extend with new equipment types
- C++17
- Custom JSON parser (no third-party dependencies)
- IAPWS-IF97 water and steam thermodynamic properties library (seuif97)
Dynamic creation of equipment objects through the ClassReg class, supporting automatic instantiation based on JSON configuration at runtime.
typedef std::map<std::string, std::function<CompBase *(umComponent)>> compfactory;
class ClassReg {
compfactory compinstance;
template <typename T>
void register_type(const std::string &name);
void register_type_all();
};Equipment consists of ports, and systems consist of equipment and connectors, forming a hierarchical object structure.
Iterates through equipment for state calculations and balance calculations, supporting iterative solving of complex systems.
RankineCycle (Cycle System)
├── Comps (Equipment Collection)
│ ├── Boiler
│ ├── TurbineEx0/TurbineEx1 (Turbine)
│ ├── Pump
│ ├── Condenser
│ └── OpenedHeaterDw0 (Open Heater)
├── curcon (Connector)
│ └── Nodes (System Port Node List)
└── Performance Metrics
├── netpoweroutput
├── efficiency
├── HeatRate
└── SteamRate
// JSON component data
typedef unordered_map<string, any> umComponent;
// Port data
typedef tuple<string, string> tupPort;
typedef tuple<tupPort, tupPort> tupConnector;
// Equipment collection
typedef map<string, Port *> mapPortObj;
typedef unordered_map<string, CompBase *> mComponentObj;
// Cycle system
typedef vector<umComponent> Components;
typedef vector<tupConnector> Connectors;
typedef tuple<Components, Connectors> rankinecycle;
typedef vector<rankinecycle> rankinecycles;Stores and calculates thermodynamic state parameters, serving as the basic unit for data exchange between equipment.
| Property | Type | Description |
|---|---|---|
| index | int | Index of the port in system nodes |
| p | double | Pressure (MPa) |
| t | double | Temperature (°C) |
| h | double | Enthalpy (kJ/kg) |
| s | double | Entropy (kJ/kg·K) |
| x | double | Quality |
| fdot | double | Mass flow fraction |
| mdot | double | Actual mass flow rate (kg/s) |
| Method | Known Parameters | Calculated Parameters |
|---|---|---|
pt_prop() |
p, t | h, s, x |
px_prop() |
p, x | t, h, s |
tx_prop() |
t, x | p, h, s |
ps_prop() |
p, s | t, h, x |
ph_prop() |
p, h | t, s, x |
Based on the IAPWS-IF97 industrial standard, providing high-precision water and steam thermodynamic property calculations through the seuif97 library. The constructor automatically calls the corresponding calculation method based on known parameters:
Port(mPort curmPort) {
// Initialize parameters to NAN
if (!isnan(p) && !isnan(t)) {
pt_prop();
} else if (!isnan(t) && !isnan(x)) {
tx_prop();
} else if (!isnan(p) && !isnan(x)) {
px_prop();
}
}Implements data sharing between equipment ports and maintains the system port node list.
void Connector::AddConnector(tupConnector tconn, mComponentObj &comps) {
auto [comp0, port0] = get<0>(tconn);
auto [comp1, port1] = get<1>(tconn);
// 1. Get port index in Nodes
index = Nodes.size();
comps[comp0]->portdict[port0]->index = index;
// 2. Initialize Node[index] with port0
Nodes.push_back(comps[comp0]->portdict[port0]);
// 3. Merge known parameters from port1 to Node[index]
getnodevalue(comps[comp1]->portdict[port1]);
// 4. Point port1 to Node[index] to achieve data sharing
comps[comp1]->portdict[port1] = Nodes[index];
comps[comp1]->setportaddress();
}Achieves automatic port data synchronization through pointer sharing:
- The first port (port0) serves as the basis for the system node
- Known parameters from the second port (port1) are merged into the node
- port1's pointer is replaced to point to the system node
- When any equipment modifies port parameters, all connected equipment automatically receive updates
The getnodevalue() method only copies values from the source port when the target node parameter is NAN, ensuring known parameters are not overwritten:
void Connector::getnodevalue(Port *port) {
if (isnan(Nodes[index]->p) && !isnan(port->p))
Nodes[index]->p = port->p;
// ... Other parameters follow the same rule
}Defines a unified interface for all equipment, implementing polymorphism.
class CompBase {
public:
string name; // Equipment name
ENERGY energy; // Energy type
Port *iPort; // Inlet port
Port *oPort; // Outlet port
mapPortObj portdict; // Port dictionary
// Energy statistics
double workExtracted; // Work output
double heatAdded; // Heat input
double workRequired; // Work input
// Pure virtual methods
virtual void set_port_address() = 0;
virtual void process_state() = 0;
virtual int balance_mass_energy() = 0;
virtual string result_string() = 0;
};enum ENERGY {
WORKEXTRACTED = 1, // Work-producing equipment (turbine)
HEATADDED, // Heat-adding equipment (boiler)
WORKREQUIRED, // Work-consuming equipment (pump)
INTERNAL // Internal equipment (condenser, heater)
};| Method | Description |
|---|---|
setportaddress() |
Updates port pointers (called after connector modifies portdict) |
process_state() |
Calculates outlet thermodynamic state based on known parameters |
balance_mass_energy() |
Performs mass/energy balance, returns 1 for success, 0 for failure |
resultstring() |
Formats and outputs equipment calculation results |
Heats feedwater to superheated steam, providing the heat required for the cycle.
iPort: Feedwater inletoPort: Steam outlet
int Boiler::balance_mass_energy() {
// Mass balance
if (!isnan(iPort->fdot)) {
oPort->fdot = iPort->fdot;
} else if (!isnan(oPort->fdot)) {
iPort->fdot = oPort->fdot;
}
// Energy balance
heatAdded = iPort->fdot * (oPort->h - iPort->h);
return isnan(heatAdded) ? 0 : 1;
}- Heat input:
Q = fdot × (h_out - h_in) - Energy type:
HEATADDED
Converts steam thermal energy into mechanical work, single-stage expansion (no extraction).
iPort: Steam inletoPort: Steam outlet
void TurbineEx0::process_state() {
if (ef == 1.0) {
// Isentropic expansion (ideal cycle)
oPort->s = iPort->s;
oPort->ps_prop();
} else {
// Actual expansion process
double isoh = ps(oPort->p, iPort->s, 4);
oPort->h = iPort->h - ef * (iPort->h - isoh);
oPort->ph_prop();
}
}
int TurbineEx0::balance_mass_energy() {
// Mass balance
if (!isnan(iPort->fdot)) {
oPort->fdot = iPort->fdot;
} else if (!isnan(oPort->fdot)) {
iPort->fdot = oPort->fdot;
}
// Energy balance
workExtracted = iPort->fdot * (iPort->h - oPort->h);
return isnan(workExtracted) ? 0 : 1;
}- Work output:
W = fdot × (h_in - h_out) - Efficiency:
ef(isentropic efficiency, 1.0 for ideal) - Energy type:
WORKEXTRACTED
Converts steam thermal energy into mechanical work, supports single-stage extraction.
iPort: Steam inletoPort: Steam outletePort: Extraction outlet
void TurbineEx1::process_state() {
if (ef == 1.0) {
// Isentropic expansion (ideal cycle): both stages use inlet entropy s_i
ePort->s = iPort->s;
ePort->ps_prop();
oPort->s = iPort->s;
oPort->ps_prop();
} else {
// Actual expansion (two stages)
// Stage 1: iPort → ePort, uses inlet entropy s_i
double isoh = ps(ePort->p, iPort->s, 4);
ePort->h = iPort->h - ef * (iPort->h - isoh);
ePort->ph_prop();
// Stage 2: ePort → oPort, uses extraction port entropy s_e (note: not s_i)
isoh = ps(oPort->p, ePort->s, 4);
oPort->h = ePort->h - ef * (ePort->h - isoh);
oPort->ph_prop();
}
}
int TurbineEx1::balance_mass_energy() {
// Mass balance
oPort->fdot = iPort->fdot - ePort->fdot;
// Energy balance
double ienergy = iPort->fdot * iPort->h;
double oenergy = ePort->fdot * ePort->h + oPort->fdot * oPort->h;
workExtracted = ienergy - oenergy;
return isnan(workExtracted) ? 0 : 1;
}- Work output:
W = fdot_in × h_in - fdot_e × h_e - fdot_out × h_out - Energy type:
WORKEXTRACTED
Pressurizes condensate to boiler pressure, consuming mechanical work.
iPort: InletoPort: Outlet
void Pump::process_state() {
// Isentropic compression calculation
double sout_s = iPort->s;
double hout_s = ps(oPort->p, sout_s, 4);
oPort->h = iPort->h + (hout_s - iPort->h) / ef;
oPort->ph_prop();
}
int Pump::balance_mass_energy() {
// Mass balance
if (!isnan(iPort->fdot)) {
oPort->fdot = iPort->fdot;
} else if (!isnan(oPort->fdot)) {
iPort->fdot = oPort->fdot;
}
// Energy balance: Calculate work consumption
workRequired = iPort->fdot * (oPort->h - iPort->h);
return isnan(workRequired) ? 0 : 1;
}- Work input:
W = fdot × (h_out - h_in) - Efficiency:
ef(pump efficiency) - Energy type:
WORKREQUIRED
Condenses turbine exhaust to saturated water, releasing heat.
iPort: Steam inletoPort: Condensate outlet
int Condenser::balance_mass_energy() {
// Mass balance
if (!isnan(iPort->fdot)) {
oPort->fdot = iPort->fdot;
} else if (!isnan(oPort->fdot)) {
iPort->fdot = oPort->fdot;
}
// Energy balance
heatExtracted = iPort->fdot * (iPort->h - oPort->h);
return isnan(heatExtracted) ? 0 : 1;
}- Heat rejection:
Q = fdot × (h_in - h_out) - Energy type:
INTERNAL
Uses extraction steam to heat feedwater, improving cycle efficiency.
iPort: Extraction steam inletiPort_fw: Feedwater inletoPort_fw: Feedwater outlet
int OpenedHeaterDw0::balance_mass_energy() {
// Energy balance equation
double qes1 = iPort->h - oPort_fw->h;
double qfw1 = oPort_fw->h - iPort_fw->h;
// Solve for extraction flow fraction
iPort->fdot = oPort_fw->fdot * qfw1 / (qes1 + qfw1);
// Mass balance equation
iPort_fw->fdot = oPort_fw->fdot - iPort->fdot;
heatAdded = iPort_fw->fdot * qfw1;
heatExtracted = iPort->fdot * qes1;
return isnan(heatAdded) ? 0 : 1;
}- Three-port configuration
- Energy type:
INTERNAL
Custom JSON parser, no third-party library dependencies, directly parses system configuration from JSON files.
| Method | Description |
|---|---|
load_from_file() |
Loads single cycle configuration from a JSON file |
parseComponents() |
Parses equipment array |
parseConnectors() |
Parses connector array |
parsePort() |
Parses port parameters |
extractJsonValue() |
Extracts JSON key-value |
extractJsonArray() |
Extracts JSON array |
inline rankinecycle initCyclesFromJson(const std::string& jsonFilePath) {
JsonLoader loader;
return loader.load_from_file(jsonFilePath);
}This function simplifies loading configuration:
- Accepts a JSON file path
- Returns a single
rankinecycletuple (containing Components and Connectors) - No longer stores cycles in a global container
JSON File → readFile() → extractJsonArray("components") → parseComponent() → umComponent
↓
extractJsonArray("connectors") → parseConnector() → tupConnector
↓
rankinecycle {Components, Connectors}
{
"name": "Rankine Cycle 81",
"description": "Basic Rankine cycle with ideal components",
"components": [
{
"name": "Turbine1",
"classstr": "TurbineEx0",
"ef": 1.0,
"iPort": { "p": null, "t": null, "x": null, "fdot": null },
"oPort": { "p": 0.008, "t": null, "x": null, "fdot": null }
}
],
"connectors": [
{
"ports": [
{ "component": "Turbine1", "port": "oPort" },
{ "component": "Condenser", "port": "iPort" }
]
}
]
}- Declarative Description: Only need to describe system structure and known parameters
- Flexible Configuration: Supports partially unknown parameters (null)
- Easy Maintenance: Modifying configuration does not require recompilation
class ClassReg {
public:
compfactory compinstance;
template <typename T>
void register_type(const std::string &name) {
compinstance[name] = [](umComponent item) {
return new T(item);
};
}
void register_type_all() {
register_type<Boiler>("Boiler");
register_type<Condenser>("Condenser");
register_type<Pump>("Pump");
register_type<TurbineEx0>("TurbineEx0");
register_type<TurbineEx1>("TurbineEx1");
register_type<OpenedHeaterDw0>("OpenedHeaterDw0");
}
};- Parse JSON to get equipment type and parameters
- Look up factory registration table
- Call the corresponding lambda creation function
- Return equipment object instance
Achieves automatic port data synchronization through pointer sharing, avoiding data duplication and inconsistency.
- Pointer Sharing: Multiple equipment ports point to the same system node
- Data Merging: Automatically merges known parameters during connection (NAN values are overwritten)
- Automatic Synchronization: When any equipment modifies parameters, other equipment automatically receive updates
- Address Update:
setportaddress()ensures equipment member pointers are consistent with portdict
- Reduces data copying
- Ensures data consistency
- Simplifies inter-equipment communication
void RankineCycle::component_balance() {
// 1. Initialize equipment list and status
list<string> keys;
vector<bool> balanceok;
int devnum = Comps.size();
// 2. Iterate until all equipment is balanced
int numdeviceok = 0;
int numiter = 0;
while (numiter <= devnum && numdeviceok <= devnum) {
for (int i = 0; i < keys.size(); i++) {
if (!balanceok[i]) {
try {
int rtn = Comps[keys[i]]->balance_mass_energy();
if (rtn == 1) {
balanceok[i] = true;
numdeviceok++;
}
} catch (...) {}
}
}
numiter++;
}
}- Adaptive Calculation: Only calculates calculable equipment
- Iterative Solving: Gradually resolves dependencies
- Fault Tolerance: Abnormal equipment does not affect overall calculation
- Convergence Guarantee: Maximum iterations limited to equipment count
- Iterate through all equipment
- Check if equipment has been calculated
- Attempt to execute the equipment's balance_mass_energy() method
- If successful (returns 1), mark as complete
- Repeat until all equipment is calculated or maximum iterations reached
int main() {
// 1. Register equipment types
ClassReg curclassreg;
curclassreg.register_type_all();
RankineCycle::compinstance = curclassreg.compinstance;
// 2. Load JSON configuration (returns single cycle)
rankinecycle cycle = initCyclesFromJson("./model/rankine85.json");
// 3. Create cycle object and calculate
unique_ptr<RankineCycle> curcycle(new RankineCycle(cycle));
curcycle->Simulator();
curcycle->out_results();
}RankineCycle::RankineCycle(rankinecycle cycle) {
// Extract components and connectors from cycle tuple
Components dictComps = get<0>(cycle);
Connectors vecConnectors = get<1>(cycle);
// 1. Create equipment objects
for (auto &item : dictComps) {
string classstr = get_component_class(item);
string name = get_component_name(item);
Comps[name] = compinstance[classstr](item);
}
// 2. Establish connections
curcon = new Connector();
for (auto &tconn : vecConnectors) {
curcon->add_connector(tconn, Comps);
}
}void RankineCycle::Simulator() {
// 1. State calculation (calculate outlet states for each equipment)
component_state();
// 2. Balance calculation (solve mass/energy balance)
component_balance();
// 3. Sum total energy
for (auto &comp : Comps) {
switch (comp.second->energy) {
case WORKEXTRACTED:
totalworkExtracted += comp.second->workExtracted;
break;
case HEATADDED:
totalheatAdded += comp.second->heatAdded;
break;
case WORKREQUIRED:
totalworkRequired += comp.second->workRequired;
}
}
// 4. Calculate performance metrics
netpoweroutput = totalworkExtracted - totalworkRequired;
efficiency = netpoweroutput / totalheatAdded;
HeatRate = 3600.0 / efficiency;
SteamRate = HeatRate / totalheatAdded;
}| Metric | Formula | Unit |
|---|---|---|
| Net Power Output | W_net = W_extracted - W_required |
kJ/kg |
| Cycle Efficiency | η = W_net / Q_added |
- |
| Heat Rate | HR = 3600 / η |
kJ/kWh |
| Steam Rate | SR = HR / Q_added |
kg/kWh |
Water and steam thermodynamic property calculation library based on the IAPWS-IF97 industrial standard.
| Function | Known Parameters | Output ID | Description |
|---|---|---|---|
pt(p, t, o_id) |
p, t | 4=h, 5=s, 15=x | Pressure + Temperature |
ph(p, h, o_id) |
p, h | 1=t, 5=s, 15=x | Pressure + Enthalpy |
ps(p, s, o_id) |
p, s | 1=t, 4=h, 15=x | Pressure + Entropy |
px(p, x, o_id) |
p, x | 1=t, 4=h, 5=s | Pressure + Quality |
tx(t, x, o_id) |
t, x | 0=p, 4=h, 5=s | Temperature + Quality |
| o_id | Output Parameter |
|---|---|
| 0 | Pressure (MPa) |
| 1 | Temperature (°C) |
| 4 | Enthalpy (kJ/kg) |
| 5 | Entropy (kJ/kg·K) |
| 15 | Quality |
- Independent Encapsulation: Each equipment class is independently implemented
- Unified Interface: All equipment follows the same interface
- Easy Extension: Adding new equipment types is simple
// 1. Create new equipment class
class Reheater : public CompBase {
public:
Reheater(umComponent dictComp);
~Reheater();
void setportaddress() override;
void process_state() override;
int balance_mass_energy() override;
string resultstring() override;
};
// 2. Register to factory
void register_type_all() {
register_type<Reheater>("Reheater");
}- Flexibility: Can change the system without modifying code
- Maintainability: Configuration files are easy to understand and modify
- Reusability: Same program supports multiple configurations
- Automatic equipment creation and management
- Automatic port data synchronization
- Automatic iterative solving of system dependencies
- Automatic performance metric calculation