diff --git a/CMakeLists.txt b/CMakeLists.txt index be28398..9f3cf4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) add_compile_definitions(_USE_MATH_DEFINES) +if(MSVC) + add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN) +endif() + file(GLOB CMAKE_FILES "${CMAKE_SOURCE_DIR}/cmake/*.cmake") foreach(CMAKE_FILE ${CMAKE_FILES}) diff --git a/Doxyfile b/Doxyfile index 538f26c..2892b02 100644 --- a/Doxyfile +++ b/Doxyfile @@ -61,13 +61,13 @@ PROJECT_BRIEF = # pixels and the maximum width should not exceed 200 pixels. Doxygen will copy # the logo to the output directory. -# PROJECT_LOGO = docs/deltaFlow.png +PROJECT_LOGO = docs/assets/deltaFlow.png # With the PROJECT_ICON tag one can specify an icon that is included in the tabs # when the HTML document is shown. Doxygen will copy the logo to the output # directory. -PROJECT_ICON = docs/deltaFlow.svg +PROJECT_ICON = docs//assets/deltaFlow.svg # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is @@ -1101,7 +1101,7 @@ EXAMPLE_RECURSIVE = NO # that contain images that are to be included in the documentation (see the # \image command). -IMAGE_PATH = +IMAGE_PATH = docs/assets # The INPUT_FILTER tag can be used to specify a program that Doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program diff --git a/README.md b/README.md index 8ec5269..2ddf03c 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,87 @@ # deltaFlow -![deltaFlow](./docs/deltaFlow.png) +![deltaFlow](./docs/assets/deltaFlow.png) -**deltaFlow** is a command-line tool that solves steady-state power flow problems using numerical methods like Gauss-Seidel and Newton-Raphson. It reads input from CSV files for bus and branch data. +**deltaFlow** is a command-line power flow analysis tool for electrical power systems. +It solves the steady-state power flow equations using the Gauss-Seidel and Newton-Raphson +iterative methods, with automatic reactive power limit (Q-limit) enforcement for +voltage-controlled (PV) buses. + +deltaFlow reads standard industry input formats — IEEE Common Data Format (`.cdf`, `.txt`) +and PSS/E Raw Format (`.raw`) — and produces bus voltage/power summaries and line flow reports. --- -## Build Instructions +## Features -Requires: A C++17-compatible compiler and CMake. +- **Solvers:** Gauss-Seidel (with relaxation) and Newton-Raphson +- **Q-limit enforcement:** Automatic PV-to-PQ bus type switching when reactive limits are violated +- **Input formats:** IEEE Common Data Format and PSS/E Raw Format (v32/v33) +- **Validated:** Tested against IEEE 14, 30, 57, 118, and 300-bus standard test cases +- **Cross-platform:** Builds on Linux (GCC) and Windows (MSVC) -Build the project with: +--- -```sh -./bin/build.pl -t # Run build and tests -``` +## Dependencies -Other options: +| Library | Version | Purpose | +|---------|---------|---------| +| [Eigen](https://eigen.tuxfamily.org/) | 3.4.0 | Linear algebra | +| [fmt](https://fmt.dev/) | 10.2.1 | Formatting and logging | +| [Catch2](https://github.com/catchorg/Catch2) | 3.5.4 | Unit testing | -* `-b`, `--build`: Build only (no tests) -* `-d`, `--doc`: Generate documentation (requires Doxygen) +Dependencies are managed automatically via [Conan](https://conan.io/). --- -## Usage +## Build Instructions + +**Requirements:** C++17 compiler, CMake (>= 3.25), Conan 2, Perl ```sh -./deltaFlow [OPTIONS] +./bin/build.pl -b # Build only +./bin/build.pl -t # Build and run tests +./bin/build.pl -d # Generate documentation (requires Doxygen) ``` -### Required: - -* `-b`, `--bus `: Bus data CSV -* `-l`, `--branch `: Branch data CSV -* ``: Power flow method (`gauss-seidel` or `newton-raphson`) - -### Optional: - -* `-t`, `--tolerance `: Convergence tolerance (default: `1E-8`) -* `-m`, `--max-iterations `: Max iterations (default: `1024`) -* `-r`, `--relaxation `: Relaxation factor (Gauss-Seidel only, default: `1.0`) -* `-h`, `--help`: Show help -* `-v`, `--version`: Show version - --- -## Examples +## Usage -```sh -./deltaFlow -b data/bus.csv -l data/line.csv gauss-seidel -./deltaFlow --bus data/bus.csv --line data/line.csv newton-raphson +``` +deltaFlow [OPTIONS] ``` ---- +### Required arguments -## Notes +| Argument | Description | +|----------|-------------| +| `` | Path to input file (`.cdf`, `.txt`, or `.raw`) | +| `` | Solver method: `GAUSS` or `NEWTON` | -* Both bus and branch files are required. -* `--relaxation` is ignored with Newton-Raphson. +### Options + +| Option | Description | Default | +|--------|-------------|---------| +| `-j, --job ` | Job name (used for output labeling) | Input filename stem | +| `-t, --tolerance ` | Convergence tolerance | `1E-8` | +| `-m, --max-iterations ` | Maximum solver iterations | `1024` | +| `-r, --relaxation ` | Relaxation coefficient (Gauss-Seidel only) | `1.0` | +| `-h, --help` | Display help message | | +| `-v, --version` | Show version and exit | | --- ## Documentation -Generate with: +API documentation is generated with Doxygen: ```sh ./bin/build.pl -d ``` + +--- + +## License + +deltaFlow is licensed under the [GNU General Public License v3.0](LICENSE). diff --git a/docs/deltaFlow.svg b/docs/assets/deltaFlow.svg similarity index 100% rename from docs/deltaFlow.svg rename to docs/assets/deltaFlow.svg diff --git a/docs/deltaFlow.png b/docs/deltaFlow.png deleted file mode 100644 index 951a01e..0000000 Binary files a/docs/deltaFlow.png and /dev/null differ diff --git a/src/core/model/Admittance.C b/src/core/model/Admittance.C index a7ff488..eb58ed6 100644 --- a/src/core/model/Admittance.C +++ b/src/core/model/Admittance.C @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief Bus admittance matrix computation implementation. + */ + #include #include "Admittance.H" @@ -60,7 +65,7 @@ Eigen::MatrixXcd computeAdmittanceMatrix(const BusData& busData, const BranchDat int busIndex = busData.ID(n) - 1; // Convert to 0-based if (busIndex < 0 || busIndex >= Ybus.rows()) { - ERROR("Warning: Bus ID {} out of bounds in Ybus", busIndex + 1); + LOG_ERROR("Warning: Bus ID {} out of bounds in Ybus", busIndex + 1); continue; } diff --git a/src/core/model/Admittance.H b/src/core/model/Admittance.H index b193839..c6131fb 100644 --- a/src/core/model/Admittance.H +++ b/src/core/model/Admittance.H @@ -39,8 +39,8 @@ #include -class BranchData; -class BusData; +struct BranchData; +struct BusData; /** * @brief Computes the complex bus admittance matrix ($$ Y_{bus} $$). diff --git a/src/core/solvers/GaussSeidel.C b/src/core/solvers/GaussSeidel.C index 4d086aa..e4d3629 100644 --- a/src/core/solvers/GaussSeidel.C +++ b/src/core/solvers/GaussSeidel.C @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief Gauss-Seidel power flow solver implementation. + */ + #include #include #include @@ -25,7 +30,7 @@ #include "GaussSeidel.H" #include "Logger.H" -#include "ProgressBar.H" +#include "Progress.H" bool GaussSeidel( const Eigen::MatrixXcd& Y, @@ -52,22 +57,22 @@ bool GaussSeidel( double error = std::numeric_limits::infinity(); if (omega <= 0.0 || omega >= 2.0) { - WARN("Invalid input: Relaxation coefficient must be between 0 and 2."); - DEBUG("Setting Relaxation coefficient to 1."); + LOG_WARN("Invalid input: Relaxation coefficient must be between 0 and 2."); + LOG_DEBUG("Setting Relaxation coefficient to 1."); omega = 1.0; } if (omega < 1.0) { - CRITICAL("Under-relaxation enabled (omega < 1), this will slow down convergence."); + LOG_CRITICAL("Under-relaxation enabled (omega < 1), this will slow down convergence."); } else if (omega == 1.0) { - DEBUG("Standard Gauss-Seidel enabled (omega = 1)."); + LOG_DEBUG("Standard Gauss-Seidel enabled (omega = 1)."); } else if (omega > 1.0) { - DEBUG("Over-relaxation enabled (omega > 1), this will accelerate convergence."); + LOG_DEBUG("Over-relaxation enabled (omega > 1), this will accelerate convergence."); } - DEBUG("Relaxation Coefficient :: {}", omega); + LOG_DEBUG("Relaxation Coefficient :: {}", omega); while (error >= tolerance && iteration < maxIter) { Eigen::VectorXcd dV = Eigen::VectorXcd::Zero(N); @@ -105,8 +110,8 @@ bool GaussSeidel( if (iteration >= maxIter) { printConvergenceStatus("Gauss-Seidel", false, iteration, maxIter, error, tolerance); - WARN("Gauss-Seidel did not converge within max iterations ({}).", maxIter); - DEBUG("Final error norm was {:.6e}, tolerance is {:.6e}.", error, tolerance); + LOG_WARN("Gauss-Seidel did not converge within max iterations ({}).", maxIter); + LOG_DEBUG("Final error norm was {:.6e}, tolerance is {:.6e}.", error, tolerance); return false; } @@ -117,7 +122,7 @@ bool GaussSeidel( } printConvergenceStatus("Gauss-Seidel", true, iteration, maxIter, error, tolerance); - DEBUG("Gauss-Seidel converged in {} iterations with error norm {:.6e}.", iteration, error); + LOG_DEBUG("Gauss-Seidel converged in {} iterations with error norm {:.6e}.", iteration, error); return true; } diff --git a/src/core/solvers/GaussSeidel.H b/src/core/solvers/GaussSeidel.H index d066fbc..5652855 100644 --- a/src/core/solvers/GaussSeidel.H +++ b/src/core/solvers/GaussSeidel.H @@ -92,6 +92,7 @@ * @param maxIter Maximum number of iterations (default: 1024). * @param tolerance Convergence tolerance for bus voltage updates (default: $$ 1 \times 10^{-8} $$). * @param omega Relaxation parameter for the Successive Over-Relaxation (SOR) method (default: 1.0; SOR not applied). + * @param iterHistory Optional pointer to store iteration number and error at each step. * @return true if the algorithm converged within the specified number of iterations, false otherwise. */ bool GaussSeidel( diff --git a/src/core/solvers/Jacobian.C b/src/core/solvers/Jacobian.C index 9fe4573..ed74681 100644 --- a/src/core/solvers/Jacobian.C +++ b/src/core/solvers/Jacobian.C @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief Jacobian matrix computation implementation for Newton-Raphson solver. + */ + #include #include "Jacobian.H" diff --git a/src/core/solvers/NewtonRaphson.C b/src/core/solvers/NewtonRaphson.C index 1897fab..dd9360e 100644 --- a/src/core/solvers/NewtonRaphson.C +++ b/src/core/solvers/NewtonRaphson.C @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief Newton-Raphson power flow solver implementation. + */ + #include #include #include @@ -27,7 +32,7 @@ #include "NewtonRaphson.H" #include "Jacobian.H" #include "PowerMismatch.H" -#include "ProgressBar.H" +#include "Progress.H" #include "Data.H" #include "Utils.H" @@ -60,8 +65,8 @@ bool NewtonRaphson( while (error >= tolerance) { if (iter >= maxIter) { printConvergenceStatus("Newton-Raphson", false, iter, maxIter, error, tolerance); - WARN("Newton-Raphson did not converge within {} iterations.", maxIter); - DEBUG("Final max mismatch was {:.6e}, tolerance is {:.6e}.", error, tolerance); + LOG_WARN("Newton-Raphson did not converge within {} iterations.", maxIter); + LOG_DEBUG("Final max mismatch was {:.6e}, tolerance is {:.6e}.", error, tolerance); return false; } iter++; @@ -88,10 +93,10 @@ bool NewtonRaphson( error = mismatch.cwiseAbs().maxCoeff(); if (iterHistory) iterHistory->emplace_back(iter, error); printIterationProgress("Newton-Raphson", iter, maxIter, error, tolerance); - DEBUG("NR iteration {}: max mismatch = {:.16e}", iter, error); + LOG_DEBUG("NR iteration {}: max mismatch = {:.16e}", iter, error); } printConvergenceStatus("Newton-Raphson", true, iter, maxIter, error, tolerance); - DEBUG("Newton-Raphson converged in {} iterations with max mismatch {:.6e}", iter, error); + LOG_DEBUG("Newton-Raphson converged in {} iterations with max mismatch {:.6e}", iter, error); return true; } diff --git a/src/core/solvers/NewtonRaphson.H b/src/core/solvers/NewtonRaphson.H index 2b1c235..80b03d0 100644 --- a/src/core/solvers/NewtonRaphson.H +++ b/src/core/solvers/NewtonRaphson.H @@ -81,8 +81,8 @@ #include #include -class BranchData; -class BusData; +struct BranchData; +struct BusData; /** * @brief Solves the power flow equations using the Newton-Raphson iterative method. @@ -101,6 +101,7 @@ class BusData; * @param pq_bus_id 0-based indices of PQ buses. * @param maxIter Maximum number of iterations (default: 1024). * @param tolerance Convergence tolerance for power mismatches (default: $$ 1 \times 10^{-8} $$). + * @param iterHistory Optional pointer to store iteration number and error at each step. * @return true if converged, false otherwise. */ bool NewtonRaphson( diff --git a/src/core/solvers/PowerMismatch.C b/src/core/solvers/PowerMismatch.C index ed9b311..faea8f7 100644 --- a/src/core/solvers/PowerMismatch.C +++ b/src/core/solvers/PowerMismatch.C @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief Active and reactive power mismatch computation implementation. + */ + #include #include "PowerMismatch.H" diff --git a/src/core/solvers/Qlim.C b/src/core/solvers/Qlim.C index 5ea8663..4ae1423 100644 --- a/src/core/solvers/Qlim.C +++ b/src/core/solvers/Qlim.C @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief Reactive power limit enforcement implementation. + */ + #include #include #include @@ -65,17 +70,17 @@ bool checkQlimits( type_bus(idx) = 3; // PV to PQ busData.Qg(idx) = Qmax(idx); // Fix Q at limit for next solver run qlim_hit = true; - DEBUG("Q-limit (max) hit at bus {} : Qg = {:.4f} > Qmax = {:.4f}", idx + 1, Qg(idx), Qmax(idx)); + LOG_DEBUG("Q-limit (max) hit at bus {} : Qg = {:.4f} > Qmax = {:.4f}", idx + 1, Qg(idx), Qmax(idx)); } else if (Qg(idx) < Qmin(idx)) { type_bus(idx) = 3; // PV to PQ busData.Qg(idx) = Qmin(idx); // Fix Q at limit for next solver run qlim_hit = true; - DEBUG("Q-limit (min) hit at bus {} : Qg = {:.4f} < Qmin = {:.4f}", idx + 1, Qg(idx), Qmin(idx)); + LOG_DEBUG("Q-limit (min) hit at bus {} : Qg = {:.4f} < Qmin = {:.4f}", idx + 1, Qg(idx), Qmin(idx)); } } if (!qlim_hit) { - DEBUG("Power flow converged without hitting Q-limits."); + LOG_DEBUG("Power flow converged without hitting Q-limits."); } return qlim_hit; diff --git a/src/core/solvers/Qlim.H b/src/core/solvers/Qlim.H index 541b78e..65765a3 100644 --- a/src/core/solvers/Qlim.H +++ b/src/core/solvers/Qlim.H @@ -35,7 +35,7 @@ #include -class BusData; +struct BusData; /** * @brief Checks reactive power limits on PV buses after solver convergence. diff --git a/src/io/IEEE.C b/src/io/IEEE.C index 5404796..bb976a4 100644 --- a/src/io/IEEE.C +++ b/src/io/IEEE.C @@ -18,28 +18,29 @@ * . */ +/** + * @file + * @brief IEEE Common Data Format parser implementation. + */ + #include #include #include #include "IEEE.H" #include "Logger.H" +#include "Utils.H" -static std::string strip(const std::string& s) { - auto start = s.find_first_not_of(" \t\r\n"); - if (start == std::string::npos) return ""; - auto end = s.find_last_not_of(" \t\r\n"); - return s.substr(start, end - start + 1); -} +using Utilities::strip; void IEEECommonDataFormat::read(const std::string& filename) { std::ifstream file(filename); if (!file.is_open()) { - ERROR("Cannot open input file: {}", filename); + LOG_ERROR("Cannot open input file: {}", filename); return; } - DEBUG("Reading IEEE Common Data Format: {}", filename); + LOG_DEBUG("Reading IEEE Common Data Format: {}", filename); std::string line; std::string section; @@ -65,8 +66,8 @@ void IEEECommonDataFormat::read(const std::string& filename) { while (std::getline(file, line)) { std::string firstToken = strip(line.substr(0, 4)); - if (line.find("BUS DATA") != std::string::npos) { section = "bus"; DEBUG("Parsing BUS DATA section ..."); continue; } - if (line.find("BRANCH DATA") != std::string::npos) { section = "branch"; DEBUG("Parsing BRANCH DATA section ..."); continue; } + if (line.find("BUS DATA") != std::string::npos) { section = "bus"; LOG_DEBUG("Parsing BUS DATA section ..."); continue; } + if (line.find("BRANCH DATA") != std::string::npos) { section = "branch"; LOG_DEBUG("Parsing BRANCH DATA section ..."); continue; } if (line.find("-999") != std::string::npos) { section = ""; continue; } if (section == "bus" && !firstToken.empty() && std::all_of(firstToken.begin(), firstToken.end(), ::isdigit)) { @@ -131,5 +132,5 @@ void IEEECommonDataFormat::read(const std::string& filename) { branchData.B = Eigen::Map(B.data(), nBranch); branchData.tapRatio = Eigen::Map(tap.data(), nBranch); - DEBUG("IEEE CDF parsing complete: {} bus cards, {} branch cards", nBus, nBranch); + LOG_DEBUG("IEEE CDF parsing complete: {} bus cards, {} branch cards", nBus, nBranch); } diff --git a/src/io/IEEE.H b/src/io/IEEE.H index ff02dfe..45985a0 100644 --- a/src/io/IEEE.H +++ b/src/io/IEEE.H @@ -18,15 +18,29 @@ * . */ +/** + * @file + * @brief IEEE Common Data Format reader for deltaFlow. + */ + #ifndef IEEE_H #define IEEE_H #include "Reader.H" +/** + * @class IEEECommonDataFormat + * @brief Reads power system data from IEEE Common Data Format files. + */ class IEEECommonDataFormat: public Reader { public: + /** @brief Destructor. */ ~IEEECommonDataFormat() = default; + /** + * @brief Parse an IEEE CDF file (.cdf or .txt). + * @param filename Path to the CDF file. + */ void read(const std::string& filename) override; }; diff --git a/src/io/OutputFile.H b/src/io/OutputFile.H index 32b2af5..a3b0712 100644 --- a/src/io/OutputFile.H +++ b/src/io/OutputFile.H @@ -35,10 +35,7 @@ #include #ifdef _WIN32 - #define WIN32_LEAN_AND_MEAN - #define NOMINMAX #include - #undef ERROR #else #include #endif @@ -47,490 +44,540 @@ #include #include -#include "Banner.H" +#include "Display.H" #include "Data.H" #include "Version.H" -namespace OutputFile { - -/** - * @brief Returns the current hostname. - */ -inline std::string hostname() { -#ifdef _WIN32 - char buf[MAX_COMPUTERNAME_LENGTH + 1]; - DWORD bufLen = sizeof(buf); - if (GetComputerNameA(buf, &bufLen)) return std::string(buf); -#else - char buf[256]; - if (gethostname(buf, sizeof(buf)) == 0) return std::string(buf); -#endif - return "unknown"; -} - /** - * @brief Returns the current timestamp string. + * @namespace OutputFile + * @brief Functions for writing solver output, status, message, and data files. */ -inline std::string timestamp() { - auto now = std::time(nullptr); - return fmt::format("{:%d-%b-%Y %H:%M:%S}", fmt::localtime(now)); -} - -/** - * @brief Returns the current date string. - */ -inline std::string dateStr() { - auto now = std::time(nullptr); - return fmt::format("{:%d-%b-%Y}", fmt::localtime(now)); -} +namespace OutputFile { -/** - * @brief Returns the current time string. - */ -inline std::string timeStr() { - auto now = std::time(nullptr); - return fmt::format("{:%H:%M:%S}", fmt::localtime(now)); -} + /** + * @brief Returns the current hostname. + */ + inline std::string hostname() { + #ifdef _WIN32 + char buf[MAX_COMPUTERNAME_LENGTH + 1]; + DWORD bufLen = sizeof(buf); + if (GetComputerNameA(buf, &bufLen)) return std::string(buf); + #else + char buf[256]; + if (gethostname(buf, sizeof(buf)) == 0) return std::string(buf); + #endif + return "unknown"; + } -/** - */ -inline bool writeOutputFile( - const std::string& jobName, - const std::string& inputFile, - const std::string& solverName, - const std::string& formatName, - const BusData& busData, - const BranchData& branchData, - const Eigen::MatrixXcd& Y, - int iterations, - double finalError, - double tolerance, - double elapsedSec, - double basemva = 100.0 -) { - std::string outFile = jobName + ".out"; - std::ofstream out(outFile); - if (!out.is_open()) return false; - - int nBus = busData.V.size(); - int nBranch = branchData.From.size(); - int W = Banner::pageWidth(); - - out << Banner::fileBanner(); - - out << fmt::format("\n deltaFlow v{:<32s}Date {:>14s} Time {:>8s}\n", - deltaFlow_VERSION, dateStr(), timeStr()); - out << "\n"; - - out << Banner::sectionHeader("S Y S T E M I N F O R M A T I O N"); - out << fmt::format(" Hostname : {}\n", hostname()); - out << fmt::format(" CMake Version : {}\n", CMake_VERSION); - out << fmt::format(" Compiler Version : GCC {}\n", gcc_VERSION); - out << fmt::format(" deltaFlow Version : {}\n", deltaFlow_VERSION); - out << "\n"; - - out << Banner::sectionHeader("J O B P A R A M E T E R S"); - out << fmt::format(" Job Name : {}\n", jobName); - out << fmt::format(" Input File : {}\n", inputFile); - out << fmt::format(" Input Format : {}\n", formatName); - out << fmt::format(" Solver Method : {}\n", solverName); - out << fmt::format(" Convergence Tol. : {:.6e}\n", tolerance); - out << "\n"; - - out << Banner::sectionHeader("M O D E L S U M M A R Y"); - - int nSlack = 0, nPV = 0, nPQ = 0; - for (int i = 0; i < nBus; ++i) { - if (busData.Type(i) == 1) nSlack++; - else if (busData.Type(i) == 2) nPV++; - else nPQ++; + /** + * @brief Returns the current timestamp string. + */ + inline std::string timestamp() { + auto now = std::time(nullptr); + return fmt::format("{:%d-%b-%Y %H:%M:%S}", fmt::localtime(now)); } - out << fmt::format(" Number of Buses : {:>6d}\n", nBus); - out << fmt::format(" Slack Buses : {:>6d}\n", nSlack); - out << fmt::format(" PV Buses : {:>6d}\n", nPV); - out << fmt::format(" PQ Buses : {:>6d}\n", nPQ); - out << fmt::format(" Number of Branches : {:>6d}\n", nBranch); - out << fmt::format(" Base MVA : {:>10.1f}\n", basemva); - out << "\n"; - - out << Banner::sectionHeader("S O L V E R S U M M A R Y"); - out << fmt::format(" Method : {}\n", solverName); - out << fmt::format(" Iterations : {:>6d}\n", iterations); - out << fmt::format(" Final Error : {:.6e}\n", finalError); - out << fmt::format(" Tolerance : {:.6e}\n", tolerance); - out << fmt::format(" Status : CONVERGED\n"); - out << fmt::format(" Elapsed Time (sec) : {:.3f}\n", elapsedSec); - out << "\n"; - - out << Banner::sectionHeader("B U S D A T A R E S U L T S"); - out << fmt::format(" {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", - "Bus", "Voltage", "Angle", "Load", "Load", "Gen", "Gen", "Injected"); - out << fmt::format(" {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", - "No.", "Mag.", "Degree", "MW", "Mvar", "MW", "Mvar", "Mvar"); - out << " " << std::string(W - 4, '=') << "\n"; - - for (int i = 0; i < nBus; ++i) { - double injectedMvar = busData.Qg(i) - busData.Ql(i); - out << fmt::format(" {:>4d} {:>9.4f} {:>9.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f}\n", - i + 1, busData.V(i), busData.delta(i), - busData.Pl(i), busData.Ql(i), busData.Pg(i), busData.Qg(i), injectedMvar); + /** + * @brief Returns the current date string. + */ + inline std::string dateStr() { + auto now = std::time(nullptr); + return fmt::format("{:%d-%b-%Y}", fmt::localtime(now)); } - double totalPl = busData.Pl.sum(); - double totalQl = busData.Ql.sum(); - double totalPg = busData.Pg.sum(); - double totalQg = busData.Qg.sum(); - double totalInjected = totalQg - totalQl; - - out << " " << std::string(W - 4, '=') << "\n"; - out << fmt::format(" Total{:>27.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f}\n", - totalPl, totalQl, totalPg, totalQg, totalInjected); - out << "\n"; - - out << Banner::sectionHeader("L I N E F L O W A N D L O S S E S"); - out << fmt::format(" {:>4s} {:>4s} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s}\n", - "From", "To", "MW", "Mvar", "MVA", "Loss MW", "Loss Mvar", "Tap"); - out << " " << std::string(W - 4, '=') << "\n"; - - auto Bc = branchData.B; - int nLine = nBranch; - - Eigen::VectorXcd Vc(nBus); - Eigen::VectorXcd S(nBus); - for (int i = 0; i < nBus; ++i) { - double mag = busData.V(i); - double ang_rad = busData.delta(i) * M_PI / 180.0; - Vc(i) = std::polar(mag, ang_rad); - double P = busData.Pg(i) - busData.Pl(i); - double Q = busData.Qg(i) - busData.Ql(i); - S(i) = std::complex(P, Q); + /** + * @brief Returns the current time string. + */ + inline std::string timeStr() { + auto now = std::time(nullptr); + return fmt::format("{:%H:%M:%S}", fmt::localtime(now)); } - std::complex SLT = 0.0; + /** + * @brief Writes the main output file (.out) with full analysis results. + * @param jobName Job name (used as output filename stem). + * @param inputFile Path to the input data file. + * @param solverName Name of the solver method used. + * @param formatName Name of the input file format. + * @param busData Solved bus data. + * @param branchData Branch data. + * @param Y Bus admittance matrix. + * @param iterations Number of solver iterations performed. + * @param finalError Final convergence error. + * @param tolerance Convergence tolerance. + * @param elapsedSec Wall-clock time in seconds. + * @param basemva System base MVA (default: 100). + * @return true on success, false if file could not be opened. + */ + inline bool writeOutputFile( + const std::string& jobName, + const std::string& inputFile, + const std::string& solverName, + const std::string& formatName, + const BusData& busData, + const BranchData& branchData, + const Eigen::MatrixXcd& Y, + int iterations, + double finalError, + double tolerance, + double elapsedSec, + double basemva = 100.0 + ) { + std::string outFile = jobName + ".out"; + std::ofstream out(outFile); + if (!out.is_open()) return false; + + int nBus = busData.V.size(); + int nBranch = branchData.From.size(); + int W = Display::pageWidth; + + out << Display::fileBanner(); + + out << fmt::format("\n deltaFlow v{:<32s}Date {:>14s} Time {:>8s}\n", + deltaFlow_VERSION, dateStr(), timeStr()); + out << "\n"; + + out << Display::sectionHeader("S Y S T E M I N F O R M A T I O N"); + out << fmt::format(" Hostname : {}\n", hostname()); + out << fmt::format(" CMake Version : {}\n", CMake_VERSION); + out << fmt::format(" Compiler Version : GCC {}\n", gcc_VERSION); + out << fmt::format(" deltaFlow Version : {}\n", deltaFlow_VERSION); + out << "\n"; + + out << Display::sectionHeader("J O B P A R A M E T E R S"); + out << fmt::format(" Job Name : {}\n", jobName); + out << fmt::format(" Input File : {}\n", inputFile); + out << fmt::format(" Input Format : {}\n", formatName); + out << fmt::format(" Solver Method : {}\n", solverName); + out << fmt::format(" Convergence Tol. : {:.6e}\n", tolerance); + out << "\n"; + + out << Display::sectionHeader("M O D E L S U M M A R Y"); + + int nSlack = 0, nPV = 0, nPQ = 0; + for (int i = 0; i < nBus; ++i) { + if (busData.Type(i) == 1) nSlack++; + else if (busData.Type(i) == 2) nPV++; + else nPQ++; + } - for (int n = 1; n <= nBus; ++n) { - int n_idx = n - 1; - bool busprt = false; + out << fmt::format(" Number of Buses : {:>6d}\n", nBus); + out << fmt::format(" Slack Buses : {:>6d}\n", nSlack); + out << fmt::format(" PV Buses : {:>6d}\n", nPV); + out << fmt::format(" PQ Buses : {:>6d}\n", nPQ); + out << fmt::format(" Number of Branches : {:>6d}\n", nBranch); + out << fmt::format(" Base MVA : {:>10.1f}\n", basemva); + out << "\n"; + + out << Display::sectionHeader("S O L V E R S U M M A R Y"); + out << fmt::format(" Method : {}\n", solverName); + out << fmt::format(" Iterations : {:>6d}\n", iterations); + out << fmt::format(" Final Error : {:.6e}\n", finalError); + out << fmt::format(" Tolerance : {:.6e}\n", tolerance); + out << fmt::format(" Status : CONVERGED\n"); + out << fmt::format(" Elapsed Time (sec) : {:.3f}\n", elapsedSec); + out << "\n"; + + out << Display::sectionHeader("B U S D A T A R E S U L T S"); + out << fmt::format(" {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", + "Bus", "Voltage", "Angle", "Load", "Load", "Gen", "Gen", "Injected"); + out << fmt::format(" {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", + "No.", "Mag.", "Degree", "MW", "Mvar", "MW", "Mvar", "Mvar"); + out << " " << std::string(W - 4, '=') << "\n"; + + for (int i = 0; i < nBus; ++i) { + double injectedMvar = busData.Qg(i) - busData.Ql(i); + out << fmt::format(" {:>4d} {:>9.4f} {:>9.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f}\n", + i + 1, busData.V(i), busData.delta(i), + busData.Pl(i), busData.Ql(i), busData.Pg(i), busData.Qg(i), injectedMvar); + } - for (int L = 0; L < nLine; ++L) { - if (!busprt) { - double P_inj = busData.Pg(n_idx) - busData.Pl(n_idx); - double Q_inj = busData.Qg(n_idx) - busData.Ql(n_idx); - double S_mag = std::abs(S(n_idx)) * basemva; - out << fmt::format(" {:>4d} {:>9.3f} {:>9.3f} {:>9.3f}\n", - n, P_inj, Q_inj, S_mag); - busprt = true; - } + double totalPl = busData.Pl.sum(); + double totalQl = busData.Ql.sum(); + double totalPg = busData.Pg.sum(); + double totalQg = busData.Qg.sum(); + double totalInjected = totalQg - totalQl; + + out << " " << std::string(W - 4, '=') << "\n"; + out << fmt::format(" Total{:>27.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f}\n", + totalPl, totalQl, totalPg, totalQg, totalInjected); + out << "\n"; + + out << Display::sectionHeader("L I N E F L O W A N D L O S S E S"); + out << fmt::format(" {:>4s} {:>4s} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s}\n", + "From", "To", "MW", "Mvar", "MVA", "Loss MW", "Loss Mvar", "Tap"); + out << " " << std::string(W - 4, '=') << "\n"; + + auto Bc = branchData.B; + int nLine = nBranch; + + Eigen::VectorXcd Vc(nBus); + Eigen::VectorXcd S(nBus); + for (int i = 0; i < nBus; ++i) { + double mag = busData.V(i); + double ang_rad = busData.delta(i) * M_PI / 180.0; + Vc(i) = std::polar(mag, ang_rad); + double P = busData.Pg(i) - busData.Pl(i); + double Q = busData.Qg(i) - busData.Ql(i); + S(i) = std::complex(P, Q); + } - auto writeLineFlow = [&](int from, int to, int L) { - int f_idx = from - 1; - int t_idx = to - 1; - double aL = (branchData.tapRatio(L) == 0.0) ? 1.0 : branchData.tapRatio(L); - - std::complex In, Ik; - if (branchData.From(L) == from) { - In = (Vc(f_idx) - aL * Vc(t_idx)) * Y(L) / (aL * aL) + Bc(L) / (aL * aL) * Vc(f_idx); - Ik = (Vc(t_idx) - Vc(f_idx) / aL) * Y(L) + Bc(L) * Vc(t_idx); - } else { - In = (Vc(f_idx) - Vc(t_idx) / aL) * Y(L) + Bc(L) * Vc(f_idx); - Ik = (Vc(t_idx) - aL * Vc(f_idx)) * Y(L) / (aL * aL) + Bc(L) / (aL * aL) * Vc(t_idx); - } - std::complex Snk = Vc(f_idx) * std::conj(In) * basemva; - std::complex Skn = Vc(t_idx) * std::conj(Ik) * basemva; - std::complex SL = Snk + Skn; - SLT += SL; - - if (aL != 1.0) { - out << fmt::format(" {:>4d} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f}\n", - to, std::real(Snk), std::imag(Snk), std::abs(Snk), - std::real(SL), std::imag(SL), aL); - } else { - out << fmt::format(" {:>4d} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f}\n", - to, std::real(Snk), std::imag(Snk), std::abs(Snk), - std::real(SL), std::imag(SL)); + std::complex SLT = 0.0; + + for (int n = 1; n <= nBus; ++n) { + int n_idx = n - 1; + bool busprt = false; + + for (int L = 0; L < nLine; ++L) { + if (!busprt) { + double P_inj = busData.Pg(n_idx) - busData.Pl(n_idx); + double Q_inj = busData.Qg(n_idx) - busData.Ql(n_idx); + double S_mag = std::abs(S(n_idx)) * basemva; + out << fmt::format(" {:>4d} {:>9.3f} {:>9.3f} {:>9.3f}\n", + n, P_inj, Q_inj, S_mag); + busprt = true; } - }; - if (branchData.From(L) == n) { - writeLineFlow(n, branchData.To(L), L); - } else if (branchData.To(L) == n) { - writeLineFlow(n, branchData.From(L), L); + auto writeLineFlow = [&](int from, int to, int L) { + int f_idx = from - 1; + int t_idx = to - 1; + double aL = (branchData.tapRatio(L) == 0.0) ? 1.0 : branchData.tapRatio(L); + + std::complex In, Ik; + if (branchData.From(L) == from) { + In = (Vc(f_idx) - aL * Vc(t_idx)) * Y(L) / (aL * aL) + Bc(L) / (aL * aL) * Vc(f_idx); + Ik = (Vc(t_idx) - Vc(f_idx) / aL) * Y(L) + Bc(L) * Vc(t_idx); + } else { + In = (Vc(f_idx) - Vc(t_idx) / aL) * Y(L) + Bc(L) * Vc(f_idx); + Ik = (Vc(t_idx) - aL * Vc(f_idx)) * Y(L) / (aL * aL) + Bc(L) / (aL * aL) * Vc(t_idx); + } + std::complex Snk = Vc(f_idx) * std::conj(In) * basemva; + std::complex Skn = Vc(t_idx) * std::conj(Ik) * basemva; + std::complex SL = Snk + Skn; + SLT += SL; + + if (aL != 1.0) { + out << fmt::format(" {:>4d} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f}\n", + to, std::real(Snk), std::imag(Snk), std::abs(Snk), + std::real(SL), std::imag(SL), aL); + } else { + out << fmt::format(" {:>4d} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f} {:>9.3f}\n", + to, std::real(Snk), std::imag(Snk), std::abs(Snk), + std::real(SL), std::imag(SL)); + } + }; + + if (branchData.From(L) == n) { + writeLineFlow(n, branchData.To(L), L); + } else if (branchData.To(L) == n) { + writeLineFlow(n, branchData.From(L), L); + } } } + + SLT /= 2.0; + out << "\n"; + out << fmt::format(" Total loss {:>9.3f} {:>9.3f}\n", + std::real(SLT), std::imag(SLT)); + out << "\n"; + + out << "\n"; + out << "\n"; + out << " JOB TIME SUMMARY\n"; + out << fmt::format(" TOTAL CPU TIME (SEC) = {:>12.5f}\n", elapsedSec); + out << fmt::format(" WALLCLOCK TIME (SEC) = {:>12d}\n", static_cast(std::round(elapsedSec))); + out << "\n"; + + out << Display::sectionHeader("A N A L Y S I S C O M P L E T E"); + out << Display::center("THE ANALYSIS HAS BEEN COMPLETED SUCCESSFULLY") << "\n"; + out << "\n"; + + out.close(); + return true; } - SLT /= 2.0; - out << "\n"; - out << fmt::format(" Total loss {:>9.3f} {:>9.3f}\n", - std::real(SLT), std::imag(SLT)); - out << "\n"; - - out << "\n"; - out << "\n"; - out << " JOB TIME SUMMARY\n"; - out << fmt::format(" TOTAL CPU TIME (SEC) = {:>12.5f}\n", elapsedSec); - out << fmt::format(" WALLCLOCK TIME (SEC) = {:>12d}\n", static_cast(std::round(elapsedSec))); - out << "\n"; - - out << Banner::sectionHeader("A N A L Y S I S C O M P L E T E"); - out << Banner::center("THE ANALYSIS HAS BEEN COMPLETED SUCCESSFULLY") << "\n"; - out << "\n"; - - out.close(); - return true; -} + /** + * @brief Writes the status file (.sta) with a compact solver summary. + * @param jobName Job name (used as output filename stem). + * @param inputFile Path to the input data file. + * @param solverName Name of the solver method used. + * @param formatName Name of the input file format. + * @param nBus Number of buses. + * @param nBranch Number of branches. + * @param iterations Number of solver iterations performed. + * @param finalError Final convergence error. + * @param tolerance Convergence tolerance. + * @param converged Whether the solver converged. + * @param elapsedSec Wall-clock time in seconds. + * @return true on success, false if file could not be opened. + */ + inline bool writeStatusFile( + const std::string& jobName, + const std::string& inputFile, + const std::string& solverName, + const std::string& formatName, + int nBus, + int nBranch, + int iterations, + double finalError, + double tolerance, + bool converged, + double elapsedSec + ) { + std::string staFile = jobName + ".sta"; + std::ofstream out(staFile); + if (!out.is_open()) return false; + + out << fmt::format("deltaFlow v{:<36s}Date {:>14s} Time {:>8s}\n", + deltaFlow_VERSION, dateStr(), timeStr()); + + out << " SUMMARY OF JOB INFORMATION:\n"; + out << fmt::format(" {:>6s} {:>6s} {:>10s} {:>12s} {:>12s} {:>8s}\n", + "ITER", "STATUS", "ERROR", "TOLERANCE", "ELAPSED", "RESULT"); + + out << fmt::format(" {:>6d} {:>6s} {:>10.3e} {:>12.3e} {:>12.3f} {:>8s}\n", + iterations, + converged ? "CONV" : "FAIL", + finalError, + tolerance, + elapsedSec, + converged ? "OK" : "FAILED"); + + out << "\n"; + if (converged) { + out << " THE ANALYSIS HAS COMPLETED SUCCESSFULLY\n"; + } else { + out << " THE ANALYSIS HAS FAILED TO CONVERGE\n"; + } -/** - * @brief Writes the status file (.sta) — compact solver summary - */ -inline bool writeStatusFile( - const std::string& jobName, - const std::string& inputFile, - const std::string& solverName, - const std::string& formatName, - int nBus, - int nBranch, - int iterations, - double finalError, - double tolerance, - bool converged, - double elapsedSec -) { - std::string staFile = jobName + ".sta"; - std::ofstream out(staFile); - if (!out.is_open()) return false; - - out << fmt::format("deltaFlow v{:<36s}Date {:>14s} Time {:>8s}\n", - deltaFlow_VERSION, dateStr(), timeStr()); - - out << " SUMMARY OF JOB INFORMATION:\n"; - out << fmt::format(" {:>6s} {:>6s} {:>10s} {:>12s} {:>12s} {:>8s}\n", - "ITER", "STATUS", "ERROR", "TOLERANCE", "ELAPSED", "RESULT"); - - out << fmt::format(" {:>6d} {:>6s} {:>10.3e} {:>12.3e} {:>12.3f} {:>8s}\n", - iterations, - converged ? "CONV" : "FAIL", - finalError, - tolerance, - elapsedSec, - converged ? "OK" : "FAILED"); - - out << "\n"; - if (converged) { - out << " THE ANALYSIS HAS COMPLETED SUCCESSFULLY\n"; - } else { - out << " THE ANALYSIS HAS FAILED TO CONVERGE\n"; + out << "\n"; + out << "Jobinfo File:\n"; + out << fmt::format("Inputfile: {}\n", inputFile); + out << fmt::format("Solver: {}\n", solverName); + out << fmt::format("Format: {}\n", formatName); + out << fmt::format("Hostname: {}\n", hostname()); + out << "\n"; + + out << "solver and hardware info\n"; + out << "------------------------\n"; + out << fmt::format("version : {}\n", deltaFlow_VERSION); + out << fmt::format("compiler : GCC {}\n", gcc_VERSION); + out << fmt::format("cmake : {}\n", CMake_VERSION); + out << fmt::format("hostname : {}\n", hostname()); + out << "\n"; + + out << "model info\n"; + out << "----------\n"; + out << fmt::format(" # of buses : {}\n", nBus); + out << fmt::format(" # of branches : {}\n", nBranch); + out << "\n"; + + out << "run and timing info\n"; + out << "-------------------\n"; + out << fmt::format("simulation end : {}\n", timestamp()); + out << fmt::format("elapsed time : {:.3f} seconds\n", elapsedSec); + out << fmt::format("iterations : {}\n", iterations); + out << fmt::format("final error : {:.6e}\n", finalError); + out << fmt::format("termination status: {}\n", converged ? "normal" : "failed"); + out << "\n"; + + out.close(); + return true; } - out << "\n"; - out << "Jobinfo File:\n"; - out << fmt::format("Inputfile: {}\n", inputFile); - out << fmt::format("Solver: {}\n", solverName); - out << fmt::format("Format: {}\n", formatName); - out << fmt::format("Hostname: {}\n", hostname()); - out << "\n"; - - out << "solver and hardware info\n"; - out << "------------------------\n"; - out << fmt::format("version : {}\n", deltaFlow_VERSION); - out << fmt::format("compiler : GCC {}\n", gcc_VERSION); - out << fmt::format("cmake : {}\n", CMake_VERSION); - out << fmt::format("hostname : {}\n", hostname()); - out << "\n"; - - out << "model info\n"; - out << "----------\n"; - out << fmt::format(" # of buses : {}\n", nBus); - out << fmt::format(" # of branches : {}\n", nBranch); - out << "\n"; - - out << "run and timing info\n"; - out << "-------------------\n"; - out << fmt::format("simulation end : {}\n", timestamp()); - out << fmt::format("elapsed time : {:.3f} seconds\n", elapsedSec); - out << fmt::format("iterations : {}\n", iterations); - out << fmt::format("final error : {:.6e}\n", finalError); - out << fmt::format("termination status: {}\n", converged ? "normal" : "failed"); - out << "\n"; - - out.close(); - return true; -} + /** + * @brief Writes the message file (.msg) with iteration history. + * @param jobName Job name (used as output filename stem). + * @param solverName Name of the solver method used. + * @param iterationHistory Vector of (iteration, error) pairs. + * @param tolerance Convergence tolerance. + * @param converged Whether the solver converged. + * @return true on success, false if file could not be opened. + */ + inline bool writeMessageFile( + const std::string& jobName, + const std::string& solverName, + const std::vector>& iterationHistory, + double tolerance, + bool converged + ) { + std::string msgFile = jobName + ".msg"; + std::ofstream out(msgFile); + if (!out.is_open()) return false; + + out << Display::fileBanner(); + + out << fmt::format("\n deltaFlow v{:<32s}Date {:>14s} Time {:>8s}\n", + deltaFlow_VERSION, dateStr(), timeStr()); + out << "\n"; + + out << Display::sectionHeader("I T E R A T I O N H I S T O R Y"); + + out << fmt::format(" {:>6s} {:>16s} {:>12s} {:>8s}\n", + "Iter", "Max Mismatch", "Tolerance", "Status"); + out << " " << std::string(Display::pageWidth - 4, '-') << "\n"; + + for (const auto& [iter, error] : iterationHistory) { + std::string status = (error < tolerance) ? "CONV" : ""; + out << fmt::format(" {:>6d} {:>16.6e} {:>12.6e} {:>8s}\n", + iter, error, tolerance, status); + } -/** - * @brief Writes the message file (.msg) - */ -inline bool writeMessageFile( - const std::string& jobName, - const std::string& solverName, - const std::vector>& iterationHistory, - double tolerance, - bool converged -) { - std::string msgFile = jobName + ".msg"; - std::ofstream out(msgFile); - if (!out.is_open()) return false; - - out << Banner::fileBanner(); - - out << fmt::format("\n deltaFlow v{:<32s}Date {:>14s} Time {:>8s}\n", - deltaFlow_VERSION, dateStr(), timeStr()); - out << "\n"; - - out << Banner::sectionHeader("I T E R A T I O N H I S T O R Y"); - - out << fmt::format(" {:>6s} {:>16s} {:>12s} {:>8s}\n", - "Iter", "Max Mismatch", "Tolerance", "Status"); - out << " " << std::string(Banner::pageWidth() - 4, '-') << "\n"; - - for (const auto& [iter, error] : iterationHistory) { - std::string status = (error < tolerance) ? "CONV" : ""; - out << fmt::format(" {:>6d} {:>16.6e} {:>12.6e} {:>8s}\n", - iter, error, tolerance, status); - } + out << " " << std::string(Display::pageWidth - 4, '-') << "\n"; + out << "\n"; - out << " " << std::string(Banner::pageWidth() - 4, '-') << "\n"; - out << "\n"; + if (converged) { + out << " " << solverName << " CONVERGED\n"; + } else { + out << " *** WARNING: " << solverName << " DID NOT CONVERGE\n"; + } - if (converged) { - out << " " << solverName << " CONVERGED\n"; - } else { - out << " *** WARNING: " << solverName << " DID NOT CONVERGE\n"; + out << "\n"; + out.close(); + return true; } - out << "\n"; - out.close(); - return true; -} + /** + * @brief Writes the detailed data file (.dat) with full input/output records. + * @param jobName Job name (used as output filename stem). + * @param inputFile Path to the input data file. + * @param solverName Name of the solver method used. + * @param formatName Name of the input file format. + * @param busData Solved bus data. + * @param branchData Branch data. + * @param iterationHistory Vector of (iteration, error) pairs. + * @param totalIterations Total number of iterations performed. + * @param finalError Final convergence error. + * @param tolerance Convergence tolerance. + * @param converged Whether the solver converged. + * @param elapsedSec Wall-clock time in seconds. + * @param basemva System base MVA (default: 100). + * @return true on success, false if file could not be opened. + */ + inline bool writeDatFile( + const std::string& jobName, + const std::string& inputFile, + const std::string& solverName, + const std::string& formatName, + const BusData& busData, + const BranchData& branchData, + const std::vector>& iterationHistory, + int totalIterations, + double finalError, + double tolerance, + bool converged, + double elapsedSec, + double basemva = 100.0 + ) { + std::string datFile = jobName + ".dat"; + std::ofstream out(datFile); + if (!out.is_open()) return false; + + int nBus = busData.V.size(); + int nBranch = branchData.From.size(); + int W = Display::pageWidth; + + out << Display::fileBanner(); + out << fmt::format("\n deltaFlow v{:<32s}Date {:>14s} Time {:>8s}\n", + deltaFlow_VERSION, dateStr(), timeStr()); + + out << Display::sectionHeader("I N P U T P R O C E S S I N G"); + out << fmt::format(" Input File : {}\n", inputFile); + out << fmt::format(" Input Format : {}\n", formatName); + + int nSlack = 0, nPV = 0, nPQ = 0; + for (int i = 0; i < nBus; ++i) { + if (busData.Type(i) == 1) nSlack++; + else if (busData.Type(i) == 2) nPV++; + else nPQ++; + } -/** - * @brief Writes the .dat file - */ -inline bool writeDatFile( - const std::string& jobName, - const std::string& inputFile, - const std::string& solverName, - const std::string& formatName, - const BusData& busData, - const BranchData& branchData, - const std::vector>& iterationHistory, - int totalIterations, - double finalError, - double tolerance, - bool converged, - double elapsedSec, - double basemva = 100.0 -) { - std::string datFile = jobName + ".dat"; - std::ofstream out(datFile); - if (!out.is_open()) return false; - - int nBus = busData.V.size(); - int nBranch = branchData.From.size(); - int W = Banner::pageWidth(); - - out << Banner::fileBanner(); - out << fmt::format("\n deltaFlow v{:<32s}Date {:>14s} Time {:>8s}\n", - deltaFlow_VERSION, dateStr(), timeStr()); - - out << Banner::sectionHeader("I N P U T P R O C E S S I N G"); - out << fmt::format(" Input File : {}\n", inputFile); - out << fmt::format(" Input Format : {}\n", formatName); - - int nSlack = 0, nPV = 0, nPQ = 0; - for (int i = 0; i < nBus; ++i) { - if (busData.Type(i) == 1) nSlack++; - else if (busData.Type(i) == 2) nPV++; - else nPQ++; - } + out << fmt::format(" Number of Buses : {:>6d}\n", nBus); + out << fmt::format(" Slack Buses : {:>6d}\n", nSlack); + out << fmt::format(" PV Buses : {:>6d}\n", nPV); + out << fmt::format(" PQ Buses : {:>6d}\n", nPQ); + out << fmt::format(" Number of Branches : {:>6d}\n", nBranch); + out << fmt::format(" Base MVA : {:>10.1f}\n", basemva); + + std::string solverBanner; + for (const char &c : solverName) { + solverBanner += std::toupper(c); + solverBanner += ' '; + } + out << Display::sectionHeader(solverBanner + " S O L V E R E X E C U T I O N"); + out << fmt::format(" Method : {}\n", solverName); + out << fmt::format(" Max Iterations : {:>6d}\n", static_cast(iterationHistory.size()) > 0 ? + static_cast(iterationHistory.back().first) : totalIterations); + out << fmt::format(" Convergence Tol. : {:.6e}\n", tolerance); + out << "\n"; + + out << fmt::format(" {:>6s} {:>16s} {:>12s} {:>8s}\n", + "Iter", "Max Mismatch", "Tolerance", "Status"); + out << " " << std::string(W - 4, '-') << "\n"; + + for (const auto& [iter, error] : iterationHistory) { + std::string status; + if (error < tolerance) + status = "CONV"; + else + status = "----"; + out << fmt::format(" {:>6d} {:>16.6e} {:>12.6e} {:>8s}\n", + iter, error, tolerance, status); + } - out << fmt::format(" Number of Buses : {:>6d}\n", nBus); - out << fmt::format(" Slack Buses : {:>6d}\n", nSlack); - out << fmt::format(" PV Buses : {:>6d}\n", nPV); - out << fmt::format(" PQ Buses : {:>6d}\n", nPQ); - out << fmt::format(" Number of Branches : {:>6d}\n", nBranch); - out << fmt::format(" Base MVA : {:>10.1f}\n", basemva); - - std::string solverBanner; - for (const char &c : solverName) { - solverBanner += std::toupper(c); - solverBanner += ' '; - } - out << Banner::sectionHeader(solverBanner + " S O L V E R E X E C U T I O N"); - out << fmt::format(" Method : {}\n", solverName); - out << fmt::format(" Max Iterations : {:>6d}\n", static_cast(iterationHistory.size()) > 0 ? - static_cast(iterationHistory.back().first) : totalIterations); - out << fmt::format(" Convergence Tol. : {:.6e}\n", tolerance); - out << "\n"; - - out << fmt::format(" {:>6s} {:>16s} {:>12s} {:>8s}\n", - "Iter", "Max Mismatch", "Tolerance", "Status"); - out << " " << std::string(W - 4, '-') << "\n"; - - for (const auto& [iter, error] : iterationHistory) { - std::string status; - if (error < tolerance) - status = "CONV"; - else - status = "----"; - out << fmt::format(" {:>6d} {:>16.6e} {:>12.6e} {:>8s}\n", - iter, error, tolerance, status); - } + out << " " << std::string(W - 4, '-') << "\n"; + out << "\n"; - out << " " << std::string(W - 4, '-') << "\n"; - out << "\n"; + if (converged) { + out << fmt::format(" {} CONVERGED after {} iterations.\n", solverName, totalIterations); + out << fmt::format(" Final max mismatch = {:.6e}\n", finalError); + } else { + out << fmt::format(" *** WARNING: {} DID NOT CONVERGE after {} iterations.\n", solverName, totalIterations); + out << fmt::format(" Final max mismatch = {:.6e}\n", finalError); + } - if (converged) { - out << fmt::format(" {} CONVERGED after {} iterations.\n", solverName, totalIterations); - out << fmt::format(" Final max mismatch = {:.6e}\n", finalError); - } else { - out << fmt::format(" *** WARNING: {} DID NOT CONVERGE after {} iterations.\n", solverName, totalIterations); - out << fmt::format(" Final max mismatch = {:.6e}\n", finalError); - } + out << Display::sectionHeader("B U S D A T A R E S U L T S"); + out << fmt::format(" {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", + "Bus", "Voltage", "Angle", "Load", "Load", "Gen", "Gen", "Injected"); + out << fmt::format(" {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", + "No.", "Mag.", "Degree", "MW", "Mvar", "MW", "Mvar", "Mvar"); + out << " " << std::string(W - 4, '=') << "\n"; + + for (int i = 0; i < nBus; ++i) { + double injectedMvar = busData.Qg(i) - busData.Ql(i); + out << fmt::format(" {:>4d} {:>9.4f} {:>9.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f}\n", + i + 1, busData.V(i), busData.delta(i), + busData.Pl(i), busData.Ql(i), busData.Pg(i), busData.Qg(i), injectedMvar); + } - out << Banner::sectionHeader("B U S D A T A R E S U L T S"); - out << fmt::format(" {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", - "Bus", "Voltage", "Angle", "Load", "Load", "Gen", "Gen", "Injected"); - out << fmt::format(" {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", - "No.", "Mag.", "Degree", "MW", "Mvar", "MW", "Mvar", "Mvar"); - out << " " << std::string(W - 4, '=') << "\n"; - - for (int i = 0; i < nBus; ++i) { - double injectedMvar = busData.Qg(i) - busData.Ql(i); - out << fmt::format(" {:>4d} {:>9.4f} {:>9.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f}\n", - i + 1, busData.V(i), busData.delta(i), - busData.Pl(i), busData.Ql(i), busData.Pg(i), busData.Qg(i), injectedMvar); - } + out << " " << std::string(W - 4, '=') << "\n"; - out << " " << std::string(W - 4, '=') << "\n"; + double totalPl = busData.Pl.sum(); + double totalQl = busData.Ql.sum(); + double totalPg = busData.Pg.sum(); + double totalQg = busData.Qg.sum(); + double totalInjected = totalQg - totalQl; - double totalPl = busData.Pl.sum(); - double totalQl = busData.Ql.sum(); - double totalPg = busData.Pg.sum(); - double totalQg = busData.Qg.sum(); - double totalInjected = totalQg - totalQl; + out << fmt::format(" Total{:>27.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f}\n", + totalPl, totalQl, totalPg, totalQg, totalInjected); - out << fmt::format(" Total{:>27.4f} {:>10.4f} {:>10.4f} {:>10.4f} {:>10.4f}\n", - totalPl, totalQl, totalPg, totalQg, totalInjected); + out << "\n\n"; + out << " JOB TIME SUMMARY\n"; + out << fmt::format(" TOTAL CPU TIME (SEC) = {:>12.5f}\n", elapsedSec); + out << fmt::format(" WALLCLOCK TIME (SEC) = {:>12d}\n", static_cast(std::round(elapsedSec))); + out << "\n"; - out << "\n\n"; - out << " JOB TIME SUMMARY\n"; - out << fmt::format(" TOTAL CPU TIME (SEC) = {:>12.5f}\n", elapsedSec); - out << fmt::format(" WALLCLOCK TIME (SEC) = {:>12d}\n", static_cast(std::round(elapsedSec))); - out << "\n"; + out << Display::sectionHeader("A N A L Y S I S C O M P L E T E"); + if (converged) { + out << Display::center("THE ANALYSIS HAS BEEN COMPLETED SUCCESSFULLY") << "\n"; + } else { + out << Display::center("*** THE ANALYSIS HAS NOT CONVERGED ***") << "\n"; + } + out << "\n"; - out << Banner::sectionHeader("A N A L Y S I S C O M P L E T E"); - if (converged) { - out << Banner::center("THE ANALYSIS HAS BEEN COMPLETED SUCCESSFULLY") << "\n"; - } else { - out << Banner::center("*** THE ANALYSIS HAS NOT CONVERGED ***") << "\n"; + out.close(); + return true; } - out << "\n"; - - out.close(); - return true; -} } diff --git a/src/io/PSSE.C b/src/io/PSSE.C index ab69149..36c2265 100644 --- a/src/io/PSSE.C +++ b/src/io/PSSE.C @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief PSS/E Raw data format parser implementation. + */ + #include #include #include @@ -26,20 +31,10 @@ #include "Logger.H" #include "PSSE.H" +#include "Utils.H" -static std::string strip(const std::string& s) { - auto start = s.find_first_not_of(" \t\r\n"); - if (start == std::string::npos) return ""; - auto end = s.find_last_not_of(" \t\r\n"); - return s.substr(start, end - start + 1); -} - -static std::string stripQuotes(const std::string& s) { - std::string t = strip(s); - if (t.size() >= 2 && t.front() == '\'' && t.back() == '\'') - return strip(t.substr(1, t.size() - 2)); - return t; -} +using Utilities::strip; +using Utilities::stripQuotes; static std::vector splitFields(const std::string& line) { std::vector fields; @@ -65,11 +60,11 @@ static bool isSectionEnd(const std::string& line) { void PSSERawFormat::read(const std::string& filename) { std::ifstream file(filename); if (!file.is_open()) { - ERROR("Cannot open input file: {}", filename); + LOG_ERROR("Cannot open input file: {}", filename); return; } - DEBUG("Reading PSS/E Raw Format: {}", filename); + LOG_DEBUG("Reading PSS/E Raw Format: {}", filename); std::string line; @@ -82,10 +77,10 @@ void PSSERawFormat::read(const std::string& filename) { if (hdr.size() >= 2) sbase = std::stod(hdr[1]); if (hdr.size() >= 3) version = std::stoi(hdr[2]); - DEBUG("PSS/E RAW format version {} detected (SBASE = {:.2f} MVA)", version, sbase); + LOG_DEBUG("PSS/E RAW format version {} detected (SBASE = {:.2f} MVA)", version, sbase); if (version != 32 && version != 33) { - WARN("PSS/E version {} is not explicitly supported. Attempting to parse as v{}.", + LOG_WARN("PSS/E version {} is not explicitly supported. Attempting to parse as v{}.", version, (version >= 33 ? 33 : 32)); } @@ -93,7 +88,7 @@ void PSSERawFormat::read(const std::string& filename) { std::string title1, title2; std::getline(file, title1); std::getline(file, title2); - DEBUG("PSS/E case: {}", strip(title1)); + LOG_DEBUG("PSS/E case: {}", strip(title1)); // Bus data // v32: I,'NAME',BASKV,IDE,AREA,ZONE,OWNER,VM,VA @@ -134,7 +129,7 @@ void PSSERawFormat::read(const std::string& filename) { } int nBus = busCount; - DEBUG(" {} buses read", nBus); + LOG_DEBUG(" {} buses read", nBus); // Per-bus accumulators std::vector Pl(nBus, 0.0), Ql(nBus, 0.0); @@ -265,7 +260,7 @@ void PSSERawFormat::read(const std::string& filename) { if (K != 0) { // 3-winding: skip extra winding line (line 5) std::getline(file, line); - WARN("3-winding transformer ({}-{}-{}) encountered, skipping.", I, J, K); + LOG_WARN("3-winding transformer ({}-{}-{}) encountered, skipping.", I, J, K); continue; } @@ -302,6 +297,6 @@ void PSSERawFormat::read(const std::string& filename) { branchData.B = Eigen::Map(B_vec.data(), nBranch); branchData.tapRatio = Eigen::Map(tap_vec.data(), nBranch); - DEBUG("PSS/E v{} file parsed: {} buses, {} branches (incl. transformers)", + LOG_DEBUG("PSS/E v{} file parsed: {} buses, {} branches (incl. transformers)", version, nBus, nBranch); } diff --git a/src/io/PSSE.H b/src/io/PSSE.H index e38117c..8fdf09f 100644 --- a/src/io/PSSE.H +++ b/src/io/PSSE.H @@ -18,15 +18,29 @@ * . */ +/** + * @file + * @brief PSS/E Raw Data Format reader for deltaFlow. + */ + #ifndef PSSE_H #define PSSE_H #include "Reader.H" +/** + * @class PSSERawFormat + * @brief Reads power system data from PSS/E Raw format files (v32/v33). + */ class PSSERawFormat: public Reader { public: + /** @brief Destructor. */ ~PSSERawFormat() = default; + /** + * @brief Parse a PSS/E .raw file. + * @param filename Path to the .raw file. + */ void read(const std::string& filename) override; }; diff --git a/src/io/Reader.C b/src/io/Reader.C index e2a34e5..19e5ac4 100644 --- a/src/io/Reader.C +++ b/src/io/Reader.C @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief Base data file reader implementation. + */ + #include "Reader.H" const BusData& Reader::getBusData() const noexcept { diff --git a/src/io/Reader.H b/src/io/Reader.H index 9cb2830..e38a014 100644 --- a/src/io/Reader.H +++ b/src/io/Reader.H @@ -18,6 +18,11 @@ * . */ +/** + * @file + * @brief Abstract base class for power system data file readers. + */ + #ifndef READER_H #define READER_H @@ -25,19 +30,39 @@ #include "Data.H" +/** + * @class Reader + * @brief Abstract reader interface for power system input formats. + * + * Subclasses implement the read() method for specific file formats + * (e.g., IEEE Common Data Format, PSS/E Raw). + */ class Reader { public: + /** @brief Virtual destructor. */ virtual ~Reader() = default; + /** + * @brief Read and parse the input file. + * @param filename Path to the input data file. + */ virtual void read(const std::string& filename) = 0; + /** + * @brief Get the parsed bus data. + * @return Const reference to BusData. + */ const BusData& getBusData() const noexcept; + /** + * @brief Get the parsed branch data. + * @return Const reference to BranchData. + */ const BranchData& getBranchData() const noexcept; protected: - BusData busData; - BranchData branchData; + BusData busData; ///< Parsed bus data + BranchData branchData; ///< Parsed branch data }; #endif diff --git a/src/io/Writer.C b/src/io/Writer.C index bb52d58..0c49326 100644 --- a/src/io/Writer.C +++ b/src/io/Writer.C @@ -18,11 +18,16 @@ * . */ +/** + * @file + * @brief Terminal output writer implementation for bus data and line flow results. + */ + #include #include #include -#include "Banner.H" +#include "Display.H" #include "Data.H" #include "Logger.H" #include "Writer.H" @@ -31,11 +36,11 @@ void dispBusData(const BusData& busData) { int nbus = busData.V.size(); - Banner::printSectionHeader("B U S D A T A R E S U L T S"); + Display::printSectionHeader("B U S D A T A R E S U L T S"); - fmt::print(fg(Banner::BRAND_COLOR), " {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", + fmt::print(fg(Display::LOGO_COLOR), " {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", "Bus", "Voltage", "Angle", "Load", "Load", "Gen", "Gen", "Injected"); - fmt::print(fg(Banner::BRAND_COLOR), " {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", + fmt::print(fg(Display::LOGO_COLOR), " {:>4s} {:>9s} {:>9s} {:>10s} {:>10s} {:>10s} {:>10s} {:>10s}\n", "No.", "Mag.", "Degree", "MW", "Mvar", "MW", "Mvar", "Mvar"); fmt::print(" {}\n", std::string(76, '=')); @@ -65,7 +70,7 @@ void dispBusData(const BusData& busData) { fmt::print("\n"); // Also log to file - INFO("Bus Data Summary: {} buses", nbus); + LOG_INFO("Bus Data Summary: {} buses", nbus); } void dispLineFlow( @@ -93,9 +98,9 @@ void dispLineFlow( std::complex SLT = 0.0; - Banner::printSectionHeader("L I N E F L O W A N D L O S S E S"); + Display::printSectionHeader("L I N E F L O W A N D L O S S E S"); - fmt::print(fg(Banner::BRAND_COLOR), " {:>4s} {:>4s} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s}\n", + fmt::print(fg(Display::LOGO_COLOR), " {:>4s} {:>4s} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s} {:>9s}\n", "From", "To", "MW", "Mvar", "MVA", "Loss MW", "Loss Mvar", "Tap"); fmt::print(" {}\n", std::string(76, '=')); @@ -180,7 +185,7 @@ void dispLineFlow( fmt::print("\n"); // Log summary - INFO("Line Flow computed: Total loss P={:.3f} MW, Q={:.3f} Mvar", + LOG_INFO("Line Flow computed: Total loss P={:.3f} MW, Q={:.3f} Mvar", std::real(SLT), std::imag(SLT)); } diff --git a/src/io/Writer.H b/src/io/Writer.H index a650f9f..875cb2e 100644 --- a/src/io/Writer.H +++ b/src/io/Writer.H @@ -33,8 +33,8 @@ #include // Forward declaration -class BusData; -class BranchData; +struct BusData; +struct BranchData; /** * @brief Displays bus data in a human-readable format. diff --git a/src/logging/Logger.C b/src/logging/Logger.C index 163f4e8..effa3a4 100644 --- a/src/logging/Logger.C +++ b/src/logging/Logger.C @@ -18,7 +18,12 @@ * . */ -#include "Banner.H" +/** + * @file + * @brief File logger implementation. + */ + +#include "Display.H" #include "Logger.H" Logger& Logger::getLogger() { @@ -34,11 +39,11 @@ Logger::Logger(const std::string& name, Level level) : m_FilePath(name), m_Level // Write banner to log file if (file.is_open()) { - file << Banner::fileBanner(); + file << Display::fileBanner(); auto now = std::time(nullptr); auto ts = fmt::format("{:%d-%b-%Y %H:%M:%S}", fmt::localtime(now)); file << fmt::format("\n Log started: {}\n", ts); - file << " " << Banner::separator('-') << "\n\n"; + file << " " << Display::separator('-') << "\n\n"; file.flush(); } } @@ -47,7 +52,7 @@ Logger::~Logger() { if (file.is_open()) { auto now = std::time(nullptr); auto ts = fmt::format("{:%d-%b-%Y %H:%M:%S}", fmt::localtime(now)); - file << "\n " << Banner::separator('-') << "\n"; + file << "\n " << Display::separator('-') << "\n"; file << fmt::format(" Log ended: {}\n", ts); file.close(); } diff --git a/src/logging/Logger.H b/src/logging/Logger.H index 4ceafd0..55d1e88 100644 --- a/src/logging/Logger.H +++ b/src/logging/Logger.H @@ -39,6 +39,12 @@ #include #include +#ifdef _WIN32 + #ifdef ERROR + #undef ERROR + #endif +#endif + /** * @enum Level * @brief Log severity levels. @@ -61,27 +67,27 @@ enum class Level : uint8_t { }; /** - * @def DEBUG(msg, ...) + * @def LOG_DEBUG(msg, ...) * @brief Macro for logging a debug-level message. - * @def INFO(msg, ...) + * @def LOG_INFO(msg, ...) * @brief Macro for logging an info-level message. - * @def WARN(msg, ...) + * @def LOG_WARN(msg, ...) * @brief Macro for logging a warning-level message. - * @def ERROR(msg, ...) + * @def LOG_ERROR(msg, ...) * @brief Macro for logging an error-level message. - * @def CRITICAL(msg, ...) + * @def LOG_CRITICAL(msg, ...) * @brief Macro for logging a critical-level message. - * @def MESSAGE(msg, ...) + * @def LOG_MESSAGE(msg, ...) * @brief Macro for printing messages to stdout. * * All macros use fmt formatting and forward the formatted message to the logger. */ -#define DEBUG(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::DEBUG) -#define INFO(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::INFO) -#define WARN(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::WARN) -#define ERROR(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::ERROR) -#define CRITICAL(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::CRITICAL) -#define MESSAGE(msg, ...) fmt::print("{}\n", fmt::format(FMT_STRING(msg), ##__VA_ARGS__)) +#define LOG_DEBUG(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::DEBUG) +#define LOG_INFO(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::INFO) +#define LOG_WARN(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::WARN) +#define LOG_ERROR(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::ERROR) +#define LOG_CRITICAL(msg, ...) Logger::getLogger().log(fmt::format(FMT_STRING(msg), ##__VA_ARGS__), Level::CRITICAL) +#define LOG_MESSAGE(msg, ...) fmt::print("{}\n", fmt::format(FMT_STRING(msg), ##__VA_ARGS__)) /** * @class Logger diff --git a/src/main.C b/src/main.C index 2cbc637..58949dd 100644 --- a/src/main.C +++ b/src/main.C @@ -32,7 +32,7 @@ #include "Admittance.H" #include "Argparse.H" -#include "Banner.H" +#include "Display.H" #include "GaussSeidel.H" #include "IEEE.H" #include "Logger.H" @@ -44,7 +44,7 @@ #include "Writer.H" int main(int argc, char* argv[]) { - Banner::printTerminalBanner(); + Display::printTerminalBanner(); auto startTime = std::chrono::high_resolution_clock::now(); @@ -60,23 +60,23 @@ int main(int argc, char* argv[]) { std::string solverName = (solver == SolverType::GaussSeidel) ? "Gauss-Seidel" : "Newton-Raphson"; std::string formatName = (format == InputFormat::IEEE) ? "IEEE Common Data Format" : "PSS/E Raw Format"; - DEBUG("Job name :: {}", jobName); - DEBUG("Input file :: {}", inputFile); - DEBUG("Input format :: {}", formatName); - DEBUG("Solver :: {}", solverName); - DEBUG("Tolerance :: {:.6e}", tolerance); - DEBUG("Max iter :: {}", maxIter); + LOG_DEBUG("Job name :: {}", jobName); + LOG_DEBUG("Input file :: {}", inputFile); + LOG_DEBUG("Input format :: {}", formatName); + LOG_DEBUG("Solver :: {}", solverName); + LOG_DEBUG("Tolerance :: {:.6e}", tolerance); + LOG_DEBUG("Max iter :: {}", maxIter); std::unique_ptr reader; switch (format) { case InputFormat::IEEE: reader = std::make_unique(); - INFO("Reading IEEE Common Data Format file: {}", inputFile); + LOG_INFO("Reading IEEE Common Data Format file: {}", inputFile); break; case InputFormat::PSSE: reader = std::make_unique(); - INFO("Reading PSS/E Raw Format file: {}", inputFile); + LOG_INFO("Reading PSS/E Raw Format file: {}", inputFile); break; default: break; @@ -88,14 +88,14 @@ int main(int argc, char* argv[]) { auto branchData = reader->getBranchData(); if (busData.ID.size() == 0 || branchData.From.size() == 0) { - ERROR("No bus or branch data found in '{}'. Check the file exists and is valid.", inputFile); + LOG_ERROR("No bus or branch data found in '{}'. Check the file exists and is valid.", inputFile); std::exit(1); } int N = busData.ID.size(); int nBranch = branchData.From.size(); - INFO("Model: {} buses, {} branches", N, nBranch); + LOG_INFO("Model: {} buses, {} branches", N, nBranch); int nSlack = 0, nPV = 0, nPQ = 0; for (int i = 0; i < N; ++i) { @@ -103,10 +103,10 @@ int main(int argc, char* argv[]) { else if (busData.Type(i) == 2) nPV++; else nPQ++; } - DEBUG("Bus types: {} Slack, {} PV, {} PQ", nSlack, nPV, nPQ); + LOG_DEBUG("Bus types: {} Slack, {} PV, {} PQ", nSlack, nPV, nPQ); auto Y = computeAdmittanceMatrix(busData, branchData); - DEBUG("Admittance matrix computed ({}x{})", N, N); + LOG_DEBUG("Admittance matrix computed ({}x{})", N, N); Eigen::MatrixXd G = Y.array().real().matrix(); Eigen::MatrixXd B = Y.array().imag().matrix(); @@ -130,7 +130,7 @@ int main(int argc, char* argv[]) { double finalError = 0.0; std::vector> iterationHistory; - INFO("Starting {} solver ...", solverName); + LOG_INFO("Starting {} solver ...", solverName); switch (solver) { case SolverType::GaussSeidel: { @@ -153,7 +153,7 @@ int main(int argc, char* argv[]) { finalConverged = converged; if (!converged) { - ERROR("Gauss-Seidel solver failed to converge."); + LOG_ERROR("Gauss-Seidel solver failed to converge."); break; } @@ -161,7 +161,7 @@ int main(int argc, char* argv[]) { busData, pv_indices, N); if (Q_lim_status) - DEBUG("Re-running Gauss-Seidel with updated bus types ..."); + LOG_DEBUG("Re-running Gauss-Seidel with updated bus types ..."); } break; } @@ -190,7 +190,7 @@ int main(int argc, char* argv[]) { finalConverged = converged; if (!converged) { - ERROR("Newton-Raphson solver failed to converge."); + LOG_ERROR("Newton-Raphson solver failed to converge."); break; } @@ -198,7 +198,7 @@ int main(int argc, char* argv[]) { busData, pv_indices, N); if (Q_lim_status) { - DEBUG("Re-running Newton-Raphson with updated bus types ..."); + LOG_DEBUG("Re-running Newton-Raphson with updated bus types ..."); } } break; @@ -252,8 +252,8 @@ int main(int argc, char* argv[]) { double PLoss = busData.Pg.sum() - busData.Pl.sum(); double QLoss = busData.Qg.sum() - busData.Ql.sum(); - DEBUG("Total real power loss: {:.6f} p.u.", PLoss); - DEBUG("Total reactive power loss: {:.6f} p.u.", QLoss); + LOG_DEBUG("Total real power loss: {:.6f} p.u.", PLoss); + LOG_DEBUG("Total reactive power loss: {:.6f} p.u.", QLoss); dispBusData(busData); dispLineFlow(busData, branchData, Y); @@ -276,7 +276,7 @@ int main(int argc, char* argv[]) { OutputFile::writeMessageFile(jobName, solverName, iterationHistory, tolerance, finalConverged); fmt::print("\n"); - fmt::print(fg(Banner::BRAND_COLOR) | fmt::emphasis::bold, + fmt::print(fg(Display::LOGO_COLOR) | fmt::emphasis::bold, " THE ANALYSIS HAS BEEN COMPLETED SUCCESSFULLY\n"); fmt::print("\n"); fmt::print(" Elapsed time : {:.3f} sec\n", elapsedSec); diff --git a/src/utils/Argparse.C b/src/utils/Argparse.C index b4df77c..e715685 100644 --- a/src/utils/Argparse.C +++ b/src/utils/Argparse.C @@ -18,12 +18,17 @@ * . */ +/** + * @file + * @brief Command-line argument parser implementation. + */ + #include #include #include #include "Argparse.H" -#include "Banner.H" +#include "Display.H" #include "Logger.H" #include "Utils.H" #include "Version.H" @@ -52,7 +57,6 @@ void ArgumentParser::parse_args(int argc, char* argv[]) { this->relaxation = std::stod(argv[++i]); } else if (arg == "--version" || arg == "-v") { - version(); std::exit(0); } else if (arg == "--help" || arg == "-h") { @@ -70,7 +74,7 @@ void ArgumentParser::parse_args(int argc, char* argv[]) { this->format = InputFormat::PSSE; } else { - MESSAGE("ERROR: Invalid format '{}'", arg); + LOG_MESSAGE("ERROR: Invalid format '{}'", arg); help(); std::exit(1); } @@ -87,26 +91,26 @@ void ArgumentParser::parse_args(int argc, char* argv[]) { methodFound = true; } else { - MESSAGE("ERROR: Invalid method '{}'", arg); + LOG_MESSAGE("ERROR: Invalid method '{}'", arg); help(); std::exit(1); } } else { - MESSAGE("ERROR: Unexpected argument '{}'", arg); + LOG_MESSAGE("ERROR: Unexpected argument '{}'", arg); help(); std::exit(1); } } if (!inputFileFound) { - MESSAGE("ERROR: Input CDF file (.txt or .cdf) is required."); + LOG_MESSAGE("ERROR: Input CDF file (.txt or .cdf) is required."); help(); std::exit(1); } if (!methodFound) { - MESSAGE("ERROR: Missing required solver argument (GAUSS or NEWTON)."); + LOG_MESSAGE("ERROR: Missing required solver argument (GAUSS or NEWTON)."); help(); std::exit(1); } @@ -116,11 +120,11 @@ void ArgumentParser::parse_args(int argc, char* argv[]) { } if (method == SolverType::NewtonRaphson && relaxation != 1.0) { - MESSAGE("Warning: Relaxation coefficient ignored for method 'NEWTON'"); + LOG_MESSAGE("Warning: Relaxation coefficient ignored for method 'NEWTON'"); } - DEBUG("deltaFlow v{}", deltaFlow_VERSION); - DEBUG("CMake v{}, GCC v{}", CMake_VERSION, gcc_VERSION); + LOG_DEBUG("deltaFlow v{}", deltaFlow_VERSION); + LOG_DEBUG("CMake v{}, GCC v{}", CMake_VERSION, gcc_VERSION); } std::string ArgumentParser::getInputFile() const noexcept { @@ -152,8 +156,7 @@ InputFormat ArgumentParser::getInputFormat() const noexcept { } void ArgumentParser::help() const noexcept { - Banner::printTerminalBanner(); - MESSAGE(R"( + LOG_MESSAGE(R"( Usage: deltaFlow [OPTIONS] @@ -175,7 +178,3 @@ Solvers: NEWTON Newton-Raphson Method )"); } - -void ArgumentParser::version() const noexcept { - Banner::printTerminalBanner(); -} diff --git a/src/utils/Argparse.H b/src/utils/Argparse.H index 87fc2fa..0e0e4f6 100644 --- a/src/utils/Argparse.H +++ b/src/utils/Argparse.H @@ -32,7 +32,7 @@ #include /** - * @enum Solver + * @enum SolverType * @brief Types of solvers supported by deltaFlow. * * - GaussSeidel: Gauss-Seidel iterative method. @@ -43,9 +43,13 @@ enum class SolverType { NewtonRaphson ///< Newton-Raphson iterative method }; +/** + * @enum InputFormat + * @brief Supported input file formats. + */ enum class InputFormat { - IEEE, - PSSE + IEEE, ///< IEEE Common Data Format (.cdf, .txt) + PSSE ///< PSS/E Raw Data Format (.raw) }; /** @@ -111,6 +115,10 @@ class ArgumentParser final { */ SolverType getSolverType() const noexcept; + /** + * @brief Get the input file format. + * @return InputFormat enum (IEEE or PSSE). + */ InputFormat getInputFormat() const noexcept; private: @@ -120,7 +128,7 @@ class ArgumentParser final { int maxIterations = 1024; ///< Maximum number of iterations ($$ N_{max} $$) double relaxation = 1.0; ///< Relaxation coefficient ($$ \omega $$) SolverType method; ///< Solver type - InputFormat format; + InputFormat format; ///< Input file format /** * @brief Parse the provided arguments. @@ -134,10 +142,6 @@ class ArgumentParser final { */ void help() const noexcept; - /** - * @brief Print version information to stdout. - */ - void version() const noexcept; }; #endif diff --git a/src/utils/Banner.H b/src/utils/Banner.H deleted file mode 100644 index 9bc3185..0000000 --- a/src/utils/Banner.H +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright (c) 2024 Saud Zahir - * - * This file is part of deltaFlow. - * - * deltaFlow is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * deltaFlow is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with deltaFlow. If not, see - * . - */ - -/** - * @file - * @brief Banner and branding utilities for deltaFlow. - * - * Provides the deltaFlow logo and formatted header/footer for - * terminal (colored) and file (plain text) output. - */ - -#ifndef BANNER_H -#define BANNER_H - -#include -#include -#include - -#ifdef _WIN32 - #define WIN32_LEAN_AND_MEAN - #define NOMINMAX - #include - #undef ERROR -#else - #include -#endif - -#include "Version.H" - -namespace Banner { - -constexpr fmt::rgb BRAND_COLOR{153, 0, 204}; - -/** - * @brief Returns the width of the output page (characters). - */ -inline int pageWidth() { return 80; } - -/** - * @brief Returns a separator line of characters. - */ -inline std::string separator(char ch = '=') { - return std::string(pageWidth(), ch); -} - -/** - * @brief Returns a centered string within the page width. - */ -inline std::string center(const std::string& text) { - int pad = (pageWidth() - static_cast(text.size())) / 2; - if (pad <= 0) return text; - return std::string(pad, ' ') + text; -} - -/** - * @brief Returns the starred license+logo box (single box with logo inside). - * - * The box is 67 columns of '*' with a leading space: - * " " + 67*'*' = 68-char lines. - * Inner content width is 65 characters. - */ -inline std::string licenseNotice() { - constexpr int W = 67; // number of '*' in the rule line - constexpr int inner = W - 2; // 65 chars of content between the two border '*' - - auto star = [&](const std::string& text) -> std::string { - int pad = inner - static_cast(text.size()); - if (pad < 0) pad = 0; - return " *" + text + std::string(pad, ' ') + "*\n"; - }; - auto rule = [&]() -> std::string { - return " " + std::string(W, '*') + "\n"; - }; - auto centerIn = [&](const std::string& text) -> std::string { - int left = (inner - static_cast(text.size())) / 2; - if (left < 0) left = 0; - return std::string(left, ' ') + text; - }; - - std::string blk; - blk += rule(); - blk += star(""); - blk += star(centerIn(R"(/\\)")); - blk += star(centerIn(R"(/ \\)")); - blk += star(centerIn(R"(/ \\)")); - blk += star(centerIn(R"(/ \\)")); - blk += star(centerIn("=========")); - blk += star(centerIn("deltaFlow v" + std::string(deltaFlow_VERSION))); - blk += star(""); - blk += star(" Copyright (c) 2024 Saud Zahir."); - blk += star(" All rights reserved."); - blk += star(""); - blk += star(" deltaFlow is free software: you can redistribute it"); - blk += star(" and/or modify it under the terms of the GNU General"); - blk += star(" Public License as published by the Free Software"); - blk += star(" Foundation, either version 3 of the License, or"); - blk += star(" (at your option) any later version."); - blk += star(""); - blk += star(" deltaFlow is distributed in the hope that it will be useful,"); - blk += star(" but WITHOUT ANY WARRANTY; without even the implied warranty"); - blk += star(" of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."); - blk += star(" See the GNU General Public License for more details."); - blk += star(""); - blk += star(" "); - blk += star(""); - blk += rule(); - - return blk; -} - -/** - * @brief Returns the boxed product info block (LS-DYNA style). - */ -inline std::string productBox() { - constexpr int W = 59; - auto line = [&](const std::string& text) -> std::string { - int pad = W - 2 - static_cast(text.size()); - if (pad < 0) pad = 0; - return " |" + text + std::string(pad, ' ') + "|\n"; - }; - auto rule = [&]() -> std::string { - return " |" + std::string(W - 2, '_') + "|\n"; - }; - - std::string box; - box += " " + std::string(W - 2, '_') + "\n"; - box += line(""); - box += line(" deltaFlow -- Power Flow Analysis Software"); - box += line(" A Program for Steady-State Power System Analysis"); - box += line(fmt::format(" Version : {}", deltaFlow_VERSION)); - box += line(fmt::format(" CMake : {}", CMake_VERSION)); - box += line(fmt::format(" GCC : {}", gcc_VERSION)); - box += line(""); - box += line(" Copyright (c) 2024 Saud Zahir"); - box += line(" Licensed under GNU GPL v3.0"); - box += line(""); - std::string host = "unknown"; -#ifdef _WIN32 - char hbuf[MAX_COMPUTERNAME_LENGTH + 1]; - DWORD hbufLen = sizeof(hbuf); - if (GetComputerNameA(hbuf, &hbufLen)) host = std::string(hbuf); -#else - char hbuf[256]; - if (gethostname(hbuf, sizeof(hbuf)) == 0) host = std::string(hbuf); -#endif - box += line(fmt::format(" Hostname : {}", host)); - box += rule(); - - return box; -} - -/** - * @brief Prints the colored product info box to terminal. - */ -inline void printProductBox() { - constexpr int W = 59; - std::string host = "unknown"; -#ifdef _WIN32 - char hbuf[MAX_COMPUTERNAME_LENGTH + 1]; - DWORD hbufLen = sizeof(hbuf); - if (GetComputerNameA(hbuf, &hbufLen)) host = std::string(hbuf); -#else - char hbuf[256]; - if (gethostname(hbuf, sizeof(hbuf)) == 0) host = std::string(hbuf); -#endif - - auto border = [&](const std::string& text) -> std::string { - int pad = W - 2 - static_cast(text.size()); - if (pad < 0) pad = 0; - return text + std::string(pad, ' '); - }; - - // Top rule - fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, - " {}\n", std::string(W - 2, '_')); - - // Helper: print a line with colored border and colored content - auto printLine = [&](const std::string& text, fmt::rgb color) { - fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, " |"); - fmt::print(fg(color), "{}", border(text)); - fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, "|\n"); - }; - auto printKV = [&](const std::string& key, const std::string& val, - fmt::rgb keyColor, fmt::rgb valColor) { - std::string full = key + val; - int pad = W - 2 - static_cast(full.size()); - if (pad < 0) pad = 0; - fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, " |"); - fmt::print(fg(keyColor), "{}", key); - fmt::print(fg(valColor) | fmt::emphasis::bold, "{}", val); - fmt::print("{}", std::string(pad, ' ')); - fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, "|\n"); - }; - - printLine("", BRAND_COLOR); - printLine(" deltaFlow -- Power Flow Analysis Software", BRAND_COLOR); - printLine(" A Program for Steady-State Power System Analysis", fmt::rgb{180, 180, 180}); - printKV(" Version : ", deltaFlow_VERSION, fmt::rgb{100, 200, 255}, fmt::rgb{255, 255, 100}); - printKV(" CMake : ", CMake_VERSION, fmt::rgb{100, 200, 255}, fmt::rgb{255, 255, 100}); - printKV(" GCC : ", gcc_VERSION, fmt::rgb{100, 200, 255}, fmt::rgb{255, 255, 100}); - printLine("", BRAND_COLOR); - printKV(" Copyright (c) 2024 ", "Saud Zahir", fmt::rgb{180, 180, 180}, fmt::rgb{255, 165, 0}); - printLine(" Licensed under GNU GPL v3.0", fmt::rgb{100, 255, 100}); - printLine("", BRAND_COLOR); - printKV(" Hostname : ", host, fmt::rgb{100, 200, 255}, fmt::rgb{255, 200, 100}); - - // Bottom rule - fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, - " |{}|\n", std::string(W - 2, '_')); -} - -/** - * @brief Prints the colored license notice box to terminal. - * - */ -inline void printLicenseNotice() { - constexpr int W = 67; - constexpr int inner = W - 2; - constexpr fmt::rgb TEXT_COLOR{0x5a, 0x71, 0x79}; - constexpr fmt::rgb BORDER_COLOR{0x5a, 0x71, 0x79}; - - auto centerIn = [&](const std::string& text) -> std::string { - int left = (inner - static_cast(text.size())) / 2; - if (left < 0) left = 0; - return std::string(left, ' ') + text; - }; - - // Print a star-bordered line with border in BORDER_COLOR and content in given color - auto starLine = [&](const std::string& text, fmt::rgb color) { - int pad = inner - static_cast(text.size()); - if (pad < 0) pad = 0; - fmt::print(fg(BORDER_COLOR), " *"); - fmt::print(fg(color) | fmt::emphasis::bold, "{}", text); - fmt::print(fg(BORDER_COLOR), "{}", std::string(pad, ' ')); - fmt::print(fg(BORDER_COLOR), "*\n"); - }; - - // Rule line - auto rule = [&]() { - fmt::print(fg(BORDER_COLOR), " {}\n", std::string(W, '*')); - }; - - rule(); - starLine("", TEXT_COLOR); - starLine(centerIn(R"(/\\)"), BRAND_COLOR); - starLine(centerIn(R"(/ \\)"), BRAND_COLOR); - starLine(centerIn(R"(/ \\)"), BRAND_COLOR); - starLine(centerIn(R"(/ \\)"), BRAND_COLOR); - starLine(centerIn("========="), BRAND_COLOR); - starLine(centerIn("deltaFlow v" + std::string(deltaFlow_VERSION)), BRAND_COLOR); - starLine("", TEXT_COLOR); - starLine(" Copyright (c) 2024 Saud Zahir.", TEXT_COLOR); - starLine(" All rights reserved.", TEXT_COLOR); - starLine("", TEXT_COLOR); - starLine(" deltaFlow is free software: you can redistribute it", TEXT_COLOR); - starLine(" and/or modify it under the terms of the GNU General", TEXT_COLOR); - starLine(" Public License as published by the Free Software", TEXT_COLOR); - starLine(" Foundation, either version 3 of the License, or", TEXT_COLOR); - starLine(" (at your option) any later version.", TEXT_COLOR); - starLine("", TEXT_COLOR); - starLine(" deltaFlow is distributed in the hope that it will be useful,", TEXT_COLOR); - starLine(" but WITHOUT ANY WARRANTY; without even the implied warranty", TEXT_COLOR); - starLine(" of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.", TEXT_COLOR); - starLine(" See the GNU General Public License for more details.", TEXT_COLOR); - starLine("", TEXT_COLOR); - starLine(" ", TEXT_COLOR); - starLine("", TEXT_COLOR); - rule(); -} - -/** - * @brief Prints the colored banner to terminal. - */ -inline void printTerminalBanner() { - fmt::print("\n"); - printLicenseNotice(); - fmt::print("\n"); - printProductBox(); - fmt::print("\n"); -} - -/** - * @brief Returns a full plain-text banner for output/log files. - */ -inline std::string fileBanner() { - std::string banner; - banner += licenseNotice(); - banner += "\n"; - banner += productBox(); - return banner; -} - -/** - * @brief Returns a section header for output files. - */ -inline std::string sectionHeader(const std::string& title) { - std::string header; - header += "\n"; - header += separator('*') + "\n"; - header += "\n"; - header += center(title) + "\n"; - header += center(std::string(title.size() + 4, '-')) + "\n"; - header += "\n"; - header += separator('*') + "\n"; - return header; -} - -/** - * @brief Returns a sub-section header for output files. - */ -inline std::string subSectionHeader(const std::string& title) { - std::string header; - header += "\n"; - header += " " + std::string(title.size() + 4, '-') + "\n"; - header += " " + title + "\n"; - header += " " + std::string(title.size() + 4, '-') + "\n"; - return header; -} - -/** - * @brief Prints a colored section header to terminal. - */ -inline void printSectionHeader(const std::string& title) { - fmt::print("\n"); - fmt::print(fg(BRAND_COLOR), "{}\n", separator('*')); - fmt::print("\n"); - fmt::print(fg(BRAND_COLOR) | fmt::emphasis::bold, "{}\n", center(title)); - fmt::print(fg(BRAND_COLOR), "{}\n", - center(std::string(title.size() + 4, '-'))); - fmt::print("\n"); - fmt::print(fg(BRAND_COLOR), "{}\n", separator('*')); - fmt::print("\n"); -} - -} - -#endif diff --git a/src/utils/Display.H b/src/utils/Display.H new file mode 100644 index 0000000..9bb9a50 --- /dev/null +++ b/src/utils/Display.H @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2024 Saud Zahir + * + * This file is part of deltaFlow. + * + * deltaFlow is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * deltaFlow is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with deltaFlow. If not, see + * . + */ + +/** + * @file + * @brief Display and formatting utilities for terminal and file output. + */ + +#ifndef DISPLAY_H +#define DISPLAY_H + +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include +#endif + +#include "Version.H" + +/** + * @namespace Display + * @brief Terminal and file output formatting for deltaFlow. + * + * Provides colored terminal banners, plain-text banners for log/output files, + * section headers, and common formatting constants. + */ +namespace Display { + constexpr fmt::rgb LOGO_COLOR{153, 0, 204}; ///< deltaFlow logo color + constexpr int pageWidth = 80; ///< Standard output page width + constexpr int licenseBoxWidth = 67; ///< License notice box width + constexpr int productBoxWidth = 59; ///< Product info box width + + /** + * @brief Right-pads a string with spaces to the given width. + * @param text Input string. + * @param width Target width. + * @return Padded string (unchanged if text is already wider). + */ + inline std::string padRight(const std::string& text, int width) { + int pad = width - static_cast(text.size()); + return (pad > 0) ? text + std::string(pad, ' ') : text; + } + + /** + * @brief Centers a string within a given width. + * @param text Text to center. + * @param width Target width. + * @return Centered string. + */ + inline std::string centerIn(const std::string& text, int width) { + int left = (width - static_cast(text.size())) / 2; + return (left > 0) ? std::string(left, ' ') + text : text; + } + + /** + * @brief Returns a separator line of the given character. + * @param ch Fill character (default: '='). + * @return String of pageWidth characters. + */ + inline std::string separator(char ch = '=') { + return std::string(pageWidth, ch); + } + + /** + * @brief Returns a centered string within the page width. + * @param text Text to center. + * @return Padded string. + */ + inline std::string center(const std::string& text) { + return centerIn(text, pageWidth); + } + + /** + * @brief Returns the machine hostname. + * @return Hostname string, or "unknown" on failure. + */ + inline std::string hostname() { + char buf[256]; + #ifdef _WIN32 + DWORD len = sizeof(buf); + if (GetComputerNameA(buf, &len)) return std::string(buf); + #else + if (gethostname(buf, sizeof(buf)) == 0) return std::string(buf); + #endif + return "unknown"; + } + + /// A single line in the license notice box. + struct LicenseLine { + std::string text; ///< Line content + bool isLogo; ///< true for logo/brand lines + }; + + /** + * @brief Returns the license notice content lines. + * + * Shared by licenseNotice() (plain-text) and printLicenseNotice() (colored). + */ + inline std::vector licenseContent() { + constexpr int inner = licenseBoxWidth - 2; + auto mid = [](const std::string& t) { return centerIn(t, inner); }; + + return { + {"", false}, + {mid(R"(/\\)"), true}, + {mid(R"(/ \\)"), true}, + {mid(R"(/ \\)"), true}, + {mid(R"(/ \\)"), true}, + {mid("========="), true}, + {mid("deltaFlow v" + std::string(deltaFlow_VERSION)), true}, + {"", false}, + {" Copyright (c) 2024 Saud Zahir.", false}, + {" All rights reserved.", false}, + {"", false}, + {" deltaFlow is free software: you can redistribute it", false}, + {" and/or modify it under the terms of the GNU General", false}, + {" Public License as published by the Free Software", false}, + {" Foundation, either version 3 of the License, or", false}, + {" (at your option) any later version.", false}, + {"", false}, + {" deltaFlow is distributed in the hope that it will be useful,", false}, + {" but WITHOUT ANY WARRANTY; without even the implied warranty", false}, + {" of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.", false}, + {" See the GNU General Public License for more details.", false}, + {"", false}, + {" ", false}, + {"", false}, + }; + } + + /// A single line in the product info box. + struct ProductLine { + std::string key; ///< Label or full text + std::string value; ///< Value (empty for non-KV lines) + fmt::rgb keyColor; ///< Key/text color (terminal only) + fmt::rgb valColor; ///< Value color (terminal only, ignored when value is empty) + }; + + /** + * @brief Returns the product info box content lines. + * + * Shared by productBox() (plain-text) and printProductBox() (colored). + */ + inline std::vector productContent() { + constexpr fmt::rgb INFO{100, 200, 255}; + constexpr fmt::rgb VAL{255, 255, 100}; + constexpr fmt::rgb DIM{180, 180, 180}; + + return { + {"", "", LOGO_COLOR, {}}, + {" deltaFlow -- Power Flow Analysis Software", "", LOGO_COLOR, {}}, + {" A Program for Steady-State Power System Analysis", "", DIM, {}}, + {" Version : ", deltaFlow_VERSION, INFO, VAL}, + {" CMake : ", CMake_VERSION, INFO, VAL}, + {" GCC : ", gcc_VERSION, INFO, VAL}, + {"", "", LOGO_COLOR, {}}, + {" Copyright (c) 2024 ", "Saud Zahir", DIM, fmt::rgb{255, 165, 0}}, + {" Licensed under GNU GPL v3.0", "", fmt::rgb{100,255,100},{}}, + {"", "", LOGO_COLOR, {}}, + {" Hostname : ", hostname(), INFO, fmt::rgb{255, 200, 100}}, + }; + } + + /** + * @brief Returns the plain-text license notice box. + */ + inline std::string licenseNotice() { + constexpr int inner = licenseBoxWidth - 2; + std::string rule = " " + std::string(licenseBoxWidth, '*') + "\n"; + + std::string s = rule; + for (const auto& line : licenseContent()) + s += " *" + padRight(line.text, inner) + "*\n"; + s += rule; + return s; + } + + /** + * @brief Returns the plain-text product info box. + */ + inline std::string productBox() { + constexpr int inner = productBoxWidth - 2; + + std::string s; + s += " " + std::string(inner, '_') + "\n"; + for (const auto& p : productContent()) + s += " |" + padRight(p.key + p.value, inner) + "|\n"; + s += " |" + std::string(inner, '_') + "|\n"; + return s; + } + + /** + * @brief Returns a full plain-text banner for output/log files. + */ + inline std::string fileBanner() { + return licenseNotice() + "\n" + productBox(); + } + + /** + * @brief Returns a section header for output files. + * @param title Section title. + * @return Formatted section header string. + */ + inline std::string sectionHeader(const std::string& title) { + std::string s; + s += "\n"; + s += separator('*') + "\n"; + s += "\n"; + s += center(title) + "\n"; + s += center(std::string(title.size() + 4, '-')) + "\n"; + s += "\n"; + s += separator('*') + "\n"; + return s; + } + + /** + * @brief Returns a sub-section header for output files. + * @param title Sub-section title. + * @return Formatted sub-section header string. + */ + inline std::string subSectionHeader(const std::string& title) { + std::string s; + s += "\n"; + s += " " + std::string(title.size() + 4, '-') + "\n"; + s += " " + title + "\n"; + s += " " + std::string(title.size() + 4, '-') + "\n"; + return s; + } + + /** + * @brief Prints the colored license notice box to terminal. + */ + inline void printLicenseNotice() { + constexpr int inner = licenseBoxWidth - 2; + constexpr fmt::rgb BORDER{0x5a, 0x71, 0x79}; + + auto rule = [&]() { + fmt::print(fg(BORDER), " {}\n", std::string(licenseBoxWidth, '*')); + }; + + rule(); + for (const auto& line : licenseContent()) { + fmt::rgb color = line.isLogo ? LOGO_COLOR : BORDER; + fmt::print(fg(BORDER), " *"); + fmt::print(fg(color) | fmt::emphasis::bold, "{}", line.text); + fmt::print(fg(BORDER), "{}*\n", padRight("", inner - static_cast(line.text.size()))); + } + rule(); + } + + /** + * @brief Prints the colored product info box to terminal. + */ + inline void printProductBox() { + constexpr int inner = productBoxWidth - 2; + + auto printLine = [&](const std::string& text, fmt::rgb color) { + fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, " |"); + fmt::print(fg(color), "{}", padRight(text, inner)); + fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, "|\n"); + }; + + auto printKV = [&](const std::string& key, const std::string& val, + fmt::rgb keyColor, fmt::rgb valColor) { + int pad = inner - static_cast(key.size()) - static_cast(val.size()); + if (pad < 0) pad = 0; + fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, " |"); + fmt::print(fg(keyColor), "{}", key); + fmt::print(fg(valColor) | fmt::emphasis::bold, "{}", val); + fmt::print("{}", std::string(pad, ' ')); + fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, "|\n"); + }; + + fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, + " {}\n", std::string(inner, '_')); + + for (const auto& p : productContent()) { + if (p.value.empty()) + printLine(p.key, p.keyColor); + else + printKV(p.key, p.value, p.keyColor, p.valColor); + } + + fmt::print(fg(fmt::color::white) | fmt::emphasis::bold, + " |{}|\n", std::string(inner, '_')); + } + + /** + * @brief Prints the full colored banner to terminal. + */ + inline void printTerminalBanner() { + fmt::print("\n"); + printLicenseNotice(); + fmt::print("\n"); + printProductBox(); + fmt::print("\n"); + } + + /** + * @brief Prints a colored section header to terminal. + * @param title Section title. + */ + inline void printSectionHeader(const std::string& title) { + fmt::print("\n"); + fmt::print(fg(LOGO_COLOR), "{}\n", separator('*')); + fmt::print("\n"); + fmt::print(fg(LOGO_COLOR) | fmt::emphasis::bold, "{}\n", center(title)); + fmt::print(fg(LOGO_COLOR), "{}\n", center(std::string(title.size() + 4, '-'))); + fmt::print("\n"); + fmt::print(fg(LOGO_COLOR), "{}\n", separator('*')); + fmt::print("\n"); + } + +} + +#endif diff --git a/src/utils/ProgressBar.H b/src/utils/Progress.H similarity index 99% rename from src/utils/ProgressBar.H rename to src/utils/Progress.H index 8d5378d..1942b11 100644 --- a/src/utils/ProgressBar.H +++ b/src/utils/Progress.H @@ -62,7 +62,6 @@ inline void printIterationProgress( if (iter > 1) fmt::print("\033[1F\033[2K"); - // Solver label fmt::print(fg(fmt::color::cyan) | fmt::emphasis::bold, "{:<16}", solver); // Progress bar @@ -113,7 +112,6 @@ inline void printConvergenceStatus( float ratio = maxIter == 0 ? 0.0f : static_cast(iter) / maxIter; int filled = static_cast(ratio * barWidth); - // Solver label fmt::print(fg(fmt::color::cyan) | fmt::emphasis::bold, "{:<16}", solver); if (converged) { diff --git a/src/utils/Utils.H b/src/utils/Utils.H index 249a345..806f51d 100644 --- a/src/utils/Utils.H +++ b/src/utils/Utils.H @@ -37,17 +37,46 @@ namespace Utilities { /** * @brief Check if filepath is IEEE Common Data Format. - * @param str The filepath. - * @return True if extension is .cdf, false otherwise. + * @param filePath The filepath. + * @return True if extension is .cdf or .txt, false otherwise. */ inline bool isCommonDataFormat(const std::string& filePath) { auto ext = std::filesystem::path(filePath).extension(); return ext == ".cdf" || ext == ".txt"; } + /** + * @brief Check if filepath is PSS/E Raw format. + * @param filePath The filepath. + * @return True if extension is .raw, false otherwise. + */ inline bool isRawFormat(const std::string& filePath) { return std::filesystem::path(filePath).extension() == ".raw"; } + + /** + * @brief Strip leading and trailing whitespace from a string. + * @param s The input string. + * @return Trimmed string. + */ + inline std::string strip(const std::string& s) { + auto start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return ""; + auto end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); + } + + /** + * @brief Strip surrounding single quotes and whitespace from a string. + * @param s The input string. + * @return String with quotes removed. + */ + inline std::string stripQuotes(const std::string& s) { + std::string t = strip(s); + if (t.size() >= 2 && t.front() == '\'' && t.back() == '\'') + return strip(t.substr(1, t.size() - 2)); + return t; + } } #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 646d934..781e61d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,7 +13,7 @@ set(TEST_LIBS Catch2::Catch2WithMain deltaFlowLib) # Helper macro to add a test executable macro(ADD_DELTAFLOW_TEST TEST_NAME) - add_executable(${TEST_NAME} ${TEST_NAME}.C ${SOURCES}) + add_executable(${TEST_NAME} ${TEST_NAME}.C) target_link_libraries(${TEST_NAME} PRIVATE ${TEST_LIBS}) target_include_directories(${TEST_NAME} PRIVATE ${EIGEN3_INCLUDE_DIRS} .) catch_discover_tests(${TEST_NAME}) diff --git a/test/TestAdmittance.C b/test/TestAdmittance.C index dcbd5ec..800ba41 100644 --- a/test/TestAdmittance.C +++ b/test/TestAdmittance.C @@ -31,7 +31,7 @@ TEST_CASE("Admittance Matrix Computation - 5 Bus System", "[Admittance][5-Bus]") { - DEBUG("Testing [Admittance][5-Bus] - Admittance Matrix Computation ..."); + LOG_DEBUG("Testing [Admittance][5-Bus] - Admittance Matrix Computation ..."); BusData busData; const int nBus = 5; @@ -46,8 +46,8 @@ TEST_CASE("Admittance Matrix Computation - 5 Bus System", "[Admittance][5-Bus]") Eigen::MatrixXcd Y = computeAdmittanceMatrix(busData, branchData); - DEBUG("Admittance Matrix"); - DEBUG("{}", std::string(80, '=')); + LOG_DEBUG("Admittance Matrix"); + LOG_DEBUG("{}", std::string(80, '=')); for (int i = 0; i < Y.rows(); ++i) { std::stringstream rowStream; for (int j = 0; j < Y.cols(); ++j) { @@ -68,7 +68,7 @@ TEST_CASE("Admittance Matrix Computation - 5 Bus System", "[Admittance][5-Bus]") if (j != Y.cols() - 1) rowStream << "\t"; } - DEBUG("{}", rowStream.str().c_str()); + LOG_DEBUG("{}", rowStream.str().c_str()); } @@ -102,7 +102,7 @@ TEST_CASE("Admittance Matrix Computation - 5 Bus System", "[Admittance][5-Bus]") for (int j = 0; j < nBus; ++j) { auto x = std::abs(Y(i, j).real() - Y_ref(i, j).real()); auto y = std::abs(Y(i, j).imag() - Y_ref(i, j).imag()); - DEBUG("Mismatch at ({}, {}): {} + {}i", i, j, x, y); + LOG_DEBUG("Mismatch at ({}, {}): {} + {}i", i, j, x, y); REQUIRE(x < tol); REQUIRE(y < tol); } diff --git a/test/TestDivergence.C b/test/TestDivergence.C index f48f739..d549464 100644 --- a/test/TestDivergence.C +++ b/test/TestDivergence.C @@ -26,9 +26,8 @@ #include "TestUtils.H" TEST_CASE("Newton-Raphson Divergence Test", "[Newton-Raphson][Divergence]") { - DEBUG("Testing [Newton-Raphson][Divergence] - Expected failure case ..."); + LOG_DEBUG("Testing [Newton-Raphson][Divergence] - Expected failure case ..."); - // Use the standard 5-bus system but with extreme load to cause divergence auto busData = create5BusBusData(); auto branchData = create5BusBranchData(); @@ -38,13 +37,12 @@ TEST_CASE("Newton-Raphson Divergence Test", "[Newton-Raphson][Divergence]") { busData.Pl(3) = 500.0; busData.Ql(3) = 200.0; - // Allow only a few iterations so the red bar is clearly visible bool converged = solvePowerFlowNR(busData, branchData, 10, 1E-8); REQUIRE_FALSE(converged); } TEST_CASE("Gauss-Seidel Divergence Test", "[Gauss-Seidel][Divergence]") { - DEBUG("Testing [Gauss-Seidel][Divergence] - Expected failure case ..."); + LOG_DEBUG("Testing [Gauss-Seidel][Divergence] - Expected failure case ..."); auto busData = create5BusBusData(); auto branchData = create5BusBranchData(); diff --git a/test/TestGaussSeidel.C b/test/TestGaussSeidel.C index b9ebae4..c826756 100644 --- a/test/TestGaussSeidel.C +++ b/test/TestGaussSeidel.C @@ -26,7 +26,7 @@ #include "TestUtils.H" TEST_CASE("Gauss-Seidel 5-Bus Power Flow Test", "[Gauss-Seidel][5-Bus]") { - DEBUG("Testing [Gauss-Seidel][5-Bus] - 5 Bus System Power Flow ..."); + LOG_DEBUG("Testing [Gauss-Seidel][5-Bus] - 5 Bus System Power Flow ..."); auto busData = create5BusBusData(); auto branchData = create5BusBranchData(); diff --git a/test/TestIEEECDF118.C b/test/TestIEEECDF118.C index e3fa52f..4dc7c0f 100644 --- a/test/TestIEEECDF118.C +++ b/test/TestIEEECDF118.C @@ -27,7 +27,7 @@ #include "TestUtils.H" TEST_CASE("IEEE CDF 118-Bus Power Flow", "[IEEE][CDF][118-Bus]") { - DEBUG("Testing [IEEE][CDF][118-Bus] - IEEE CDF 118-Bus Power Flow ..."); + LOG_DEBUG("Testing [IEEE][CDF][118-Bus] - IEEE CDF 118-Bus Power Flow ..."); IEEECommonDataFormat reader; reader.read(testDataDir("IEEE") + "IEEE118.txt"); diff --git a/test/TestIEEECDF14.C b/test/TestIEEECDF14.C index 6536061..8633a4f 100644 --- a/test/TestIEEECDF14.C +++ b/test/TestIEEECDF14.C @@ -27,7 +27,7 @@ #include "TestUtils.H" TEST_CASE("IEEE CDF 14-Bus Power Flow", "[IEEE][CDF][14-Bus]") { - DEBUG("Testing [IEEE][CDF][14-Bus] - IEEE CDF 14-Bus Power Flow ..."); + LOG_DEBUG("Testing [IEEE][CDF][14-Bus] - IEEE CDF 14-Bus Power Flow ..."); IEEECommonDataFormat reader; reader.read(testDataDir("IEEE") + "IEEE14.txt"); diff --git a/test/TestIEEECDF30.C b/test/TestIEEECDF30.C index 2f40cd7..3adda74 100644 --- a/test/TestIEEECDF30.C +++ b/test/TestIEEECDF30.C @@ -27,7 +27,7 @@ #include "TestUtils.H" TEST_CASE("IEEE CDF 30-Bus Power Flow", "[IEEE][CDF][30-Bus]") { - DEBUG("Testing [IEEE][CDF][30-Bus] - IEEE CDF 30-Bus Power Flow ..."); + LOG_DEBUG("Testing [IEEE][CDF][30-Bus] - IEEE CDF 30-Bus Power Flow ..."); IEEECommonDataFormat reader; reader.read(testDataDir("IEEE") + "IEEE30.txt"); diff --git a/test/TestIEEECDF300.C b/test/TestIEEECDF300.C index 32efb9b..1f69433 100644 --- a/test/TestIEEECDF300.C +++ b/test/TestIEEECDF300.C @@ -27,7 +27,7 @@ #include "TestUtils.H" TEST_CASE("IEEE CDF 300-Bus Power Flow", "[IEEE][CDF][300-Bus]") { - DEBUG("Testing [IEEE][CDF][300-Bus] - IEEE CDF 300-Bus Power Flow ..."); + LOG_DEBUG("Testing [IEEE][CDF][300-Bus] - IEEE CDF 300-Bus Power Flow ..."); IEEECommonDataFormat reader; reader.read(testDataDir("IEEE") + "IEEE300.txt"); diff --git a/test/TestIEEECDF57.C b/test/TestIEEECDF57.C index 0426a4c..e49371f 100644 --- a/test/TestIEEECDF57.C +++ b/test/TestIEEECDF57.C @@ -27,7 +27,7 @@ #include "TestUtils.H" TEST_CASE("IEEE CDF 57-Bus Power Flow", "[IEEE][CDF][57-Bus]") { - DEBUG("Testing [IEEE][CDF][57-Bus] - IEEE CDF 57-Bus Power Flow ..."); + LOG_DEBUG("Testing [IEEE][CDF][57-Bus] - IEEE CDF 57-Bus Power Flow ..."); IEEECommonDataFormat reader; reader.read(testDataDir("IEEE") + "IEEE57.txt"); diff --git a/test/TestNewtonRaphson.C b/test/TestNewtonRaphson.C index 736b56b..4267bd1 100644 --- a/test/TestNewtonRaphson.C +++ b/test/TestNewtonRaphson.C @@ -26,7 +26,7 @@ #include "TestUtils.H" TEST_CASE("Newton-Raphson 5-Bus Test", "[Newton-Raphson][5-Bus]") { - DEBUG("Testing [Newton-Raphson][5-Bus] - 5 Bus System Power Flow ..."); + LOG_DEBUG("Testing [Newton-Raphson][5-Bus] - 5 Bus System Power Flow ..."); auto busData = create5BusBusData(); auto branchData = create5BusBranchData(); diff --git a/test/TestPSSEIEEE14V32.C b/test/TestPSSEIEEE14V32.C index 087326d..2d54896 100644 --- a/test/TestPSSEIEEE14V32.C +++ b/test/TestPSSEIEEE14V32.C @@ -27,7 +27,7 @@ #include "TestUtils.H" TEST_CASE("PSS/E v32 IEEE 14-Bus Power Flow", "[PSSE][IEEE][14-Bus][v32]") { - DEBUG("Testing [PSSE][IEEE][14-Bus][v32] - PSS/E v32 IEEE 14-Bus Power Flow ..."); + LOG_DEBUG("Testing [PSSE][IEEE][14-Bus][v32] - PSS/E v32 IEEE 14-Bus Power Flow ..."); PSSERawFormat reader; reader.read(testDataDir("PSSE") + "IEEE14_v32.raw"); diff --git a/test/TestPSSEIEEE39V33.C b/test/TestPSSEIEEE39V33.C index 2aedc1e..f9231ac 100644 --- a/test/TestPSSEIEEE39V33.C +++ b/test/TestPSSEIEEE39V33.C @@ -27,7 +27,7 @@ #include "TestUtils.H" TEST_CASE("PSS/E v33 IEEE 39-Bus Power Flow", "[PSSE][IEEE][39-Bus][v33]") { - DEBUG("Testing [PSSE][IEEE][39-Bus][v33] - PSS/E v33 IEEE 39-Bus Power Flow ..."); + LOG_DEBUG("Testing [PSSE][IEEE][39-Bus][v33] - PSS/E v33 IEEE 39-Bus Power Flow ..."); PSSERawFormat reader; reader.read(testDataDir("PSSE") + "IEEE39_v33.raw"); diff --git a/test/TestUtils.H b/test/TestUtils.H index 57e3ed5..bbd9884 100644 --- a/test/TestUtils.H +++ b/test/TestUtils.H @@ -40,12 +40,13 @@ inline std::string testDataDir(const std::string& subdir = "IEEE") { std::string file(__FILE__); - auto pos = file.rfind('/'); + // MSVC uses backslashes in __FILE__, GCC/Clang use forward slashes + auto pos = file.find_last_of("/\\"); return (pos != std::string::npos ? file.substr(0, pos) : ".") + "/data/" + subdir + "/"; } // --------------------------------------------------------------------------- -// 5-Bus test system (used by Admittance, Newton-Raphson, Gauss-Seidel tests) +// 5-Bus test system // --------------------------------------------------------------------------- inline BusData create5BusBusData() {