diff --git a/Sim_Engine/Engine.vcxproj b/Sim_Engine/Engine.vcxproj
index bdc17788..35777830 100644
--- a/Sim_Engine/Engine.vcxproj
+++ b/Sim_Engine/Engine.vcxproj
@@ -57,7 +57,7 @@
true
_DEBUG;_CONSOLE;%(PreprocessorDefinitions)
true
- $(SolutionDir)EngineLib;$(SolutionDir)EngineLib\include;$SolutionDir)External\GLFW;$(SolutionDir)External\GLAD;$(SolutionDir)External\GLM;$(SolutionDir)External\assimp;$(SolutionDir)External\MathLib\include;%(AdditionalIncludeDirectories)
+ $(SolutionDir)EngineLib;$(SolutionDir)EngineLib\include;$SolutionDir)External\GLFW;$(SolutionDir)External\GLAD;$(SolutionDir)External\GLM;$(SolutionDir)External\assimp;$(SolutionDir)External\MathLib\include;$(SolutionDir)Engine\include;$(SolutionDir)External\Eigen;%(AdditionalIncludeDirectories)
stdcpp20
$(IntDir)vc$(PlatformToolsetVersion).pdb
$(IntDir)%(FileName).obj
@@ -87,7 +87,7 @@ echo D | xcopy /E /Y /D "$(ProjectDir)Engine\configs\*" "$(TargetDir)configs\"
true
NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
true
- $(SolutionDir)EngineLib;$(SolutionDir)EngineLib\include;$SolutionDir)External\GLFW;$(SolutionDir)External\GLAD;$(SolutionDir)External\GLM;$(SolutionDir)External\assimp;$(SolutionDir)External\MathLib\include;%(AdditionalIncludeDirectories)
+ $(SolutionDir)EngineLib;$(SolutionDir)EngineLib\include;$SolutionDir)External\GLFW;$(SolutionDir)External\GLAD;$(SolutionDir)External\GLM;$(SolutionDir)External\assimp;$(SolutionDir)External\MathLib\include;$(SolutionDir)Engine\include;$(SolutionDir)External\Eigen;%(AdditionalIncludeDirectories)
stdcpp20
false
$(IntDir)vc$(PlatformToolsetVersion).pdb
@@ -111,6 +111,7 @@ echo D | xcopy /E /Y /D "$(ProjectDir)Engine\configs\*" "$(TargetDir)configs\"
+
@@ -138,6 +139,10 @@ echo D | xcopy /E /Y /D "$(ProjectDir)Engine\configs\*" "$(TargetDir)configs\"
+
+
+
+
diff --git a/Sim_Engine/Engine.vcxproj.filters b/Sim_Engine/Engine.vcxproj.filters
index a4e06fa2..c65e0d64 100644
--- a/Sim_Engine/Engine.vcxproj.filters
+++ b/Sim_Engine/Engine.vcxproj.filters
@@ -7,6 +7,9 @@
src
+
+ src
+
@@ -15,6 +18,9 @@
{383daa6b-14af-40d3-b6df-31732d4d7a6a}
+
+ {146d8cfd-be4d-41a3-bc59-546deb084dc0}
+
@@ -84,4 +90,12 @@
Shaders
+
+
+ include
+
+
+ include
+
+
\ No newline at end of file
diff --git a/Sim_Engine/Engine/include/BatchArgs.h b/Sim_Engine/Engine/include/BatchArgs.h
new file mode 100644
index 00000000..ec49d476
--- /dev/null
+++ b/Sim_Engine/Engine/include/BatchArgs.h
@@ -0,0 +1,19 @@
+#pragma once
+#include
+#include
+#include
+
+// Struct to hold command-line arguments for batch mode
+struct BatchArgs {
+ std::string testPath;
+ // Base configuration for batch runs (if not provided, defaults will be used)
+ std::optional baseDtRaw;
+ std::optional baseDt;
+ std::optional baseInt;
+ // Lists for sweep parameters (if not provided, defaults will be used)
+ std::vector sweepDtRaw;
+ std::vector sweepDt;
+ std::vector sweepInt;
+ // Run name for output organisation
+ std::optional runName;
+};
\ No newline at end of file
diff --git a/Sim_Engine/Engine/include/BatchEntry.h b/Sim_Engine/Engine/include/BatchEntry.h
new file mode 100644
index 00000000..4a24b60d
--- /dev/null
+++ b/Sim_Engine/Engine/include/BatchEntry.h
@@ -0,0 +1,3 @@
+#pragma once
+struct BatchArgs;
+int runBatchMode(const BatchArgs& args);
\ No newline at end of file
diff --git a/Sim_Engine/Engine/src/BatchMain.cpp b/Sim_Engine/Engine/src/BatchMain.cpp
new file mode 100644
index 00000000..4e9048f7
--- /dev/null
+++ b/Sim_Engine/Engine/src/BatchMain.cpp
@@ -0,0 +1,132 @@
+// BatchMain.cpp
+#include "pch.h"
+#include
+#include "BatchEntry.h"
+#include "BatchArgs.h"
+#include "Platform/StudyRunner.h"
+#include "Numerics/IntegrationMethods.h"
+#include "Platform/Paths.h"
+
+#include
+#include
+#include
+#include
+#include
+
+// Integration method parser from string (throws if unknown)
+static integration::eIntegrationMethod parseMethod(const std::string& name) {
+ if (name == "euler") return integration::eIntegrationMethod::Euler;
+ if (name == "midpoint") return integration::eIntegrationMethod::Midpoint;
+ if (name == "heun") return integration::eIntegrationMethod::Heun;
+ if (name == "ralston") return integration::eIntegrationMethod::Ralston;
+ if (name == "rk4") return integration::eIntegrationMethod::RK4;
+ if (name == "rk45") return integration::eIntegrationMethod::RK45;
+ throw std::runtime_error("Unknown integrator: " + name);
+}
+
+// Helper to format a timestep (dt) as a string for use in tags and filenames
+static std::string formatDtForFile(const std::string& raw) {
+ std::string out;
+ out.reserve(raw.size() + 4);
+ for (char c : raw) {
+ if (c == '.') { out += "p"; }
+ if (c == '/') { out += "over"; }
+ else { out += c; }
+ }
+ return out;
+}
+
+// Core factory function for StudyRunner
+static CorePtr makeCoreFactory() {
+ core::ISimulationCore* raw = CreateSimulationCore_v1();
+ if (!raw) { return CorePtr(nullptr, [](core::ISimulationCore*) {}); }
+ return CorePtr(raw, [](core::ISimulationCore* p) { DestroySimulationCore(p); });
+}
+
+// Batch mode entry point
+int runBatchMode(const BatchArgs& args) {
+ paths::init();
+ fprintf(stdout, "BATCH MODE ENTERED\n");
+ fflush(stdout);
+ // Basic validation and setup
+ try {
+ // Validate required baseline
+ if (!args.baseDt.has_value() || !args.baseInt.has_value()) { throw std::runtime_error("Baseline (--basedt and --baseint) required."); }
+ if (args.testPath.empty()) { throw std::runtime_error("No test script provided."); }
+
+ // Load script
+ std::ifstream file(args.testPath);
+ if (!file) { throw std::runtime_error("Failed to open test file: " + args.testPath); }
+ std::ostringstream buffer;
+ buffer << file.rdbuf();
+ std::string scriptText = buffer.str();
+
+ // Build configs
+ std::vector configs;
+
+ // Baseline (always first)
+ {
+ StudyRunner::config base;
+ base.method = parseMethod(args.baseInt.value());
+ base.dt = args.baseDt.value();
+ base.len_min = 1.0; // Fixed simulation duration (consistent across runs)
+ base.tag =
+ (args.runName.has_value() ? args.runName.value() + "_" : "") +
+ "base_dt" + formatDtForFile(args.baseDtRaw.value()) +
+ "_int_" + args.baseInt.value();
+ configs.push_back(base);
+ }
+
+ // Sweep grid
+ for (size_t i = 0; i < args.sweepDt.size(); ++i) {
+ double dt = args.sweepDt[i];
+ const std::string& dtRaw = args.sweepDtRaw[i];
+ for (const auto& intName : args.sweepInt) {
+ StudyRunner::config c;
+ c.method = parseMethod(intName);
+ c.dt = dt;
+ c.len_min = 1.0;
+ c.tag =
+ (args.runName.has_value() ? args.runName.value() + "_" : "") +
+ "dt" + formatDtForFile(dtRaw) +
+ "_int_" + intName;
+ configs.push_back(c);
+ }
+ }
+
+ // Worker configuration
+ size_t workers = 1; // stable mode (parallel logging disabled)
+ StudyRunner runner(makeCoreFactory, workers);
+
+ // Execute
+ auto T0 = std::chrono::high_resolution_clock::now();
+ auto results = runner.runStudies(configs, scriptText);
+ auto T1 = std::chrono::high_resolution_clock::now();
+
+ double total = std::chrono::duration(T1 - T0).count();
+ fprintf(stderr, "TOTAL BATCH TIME = %.3f sec\n", total);
+
+ // Sort results by tag
+ std::sort(results.begin(), results.end(),
+ [](const StudyResult& a, const StudyResult& b) {
+ return a.tag < b.tag;
+ });
+
+ // Print summary
+ for (const auto& r : results) {
+ std::cout
+ << "tag=" << r.tag
+ << " success=" << (r.success ? "yes" : "no")
+ << " int=" << r.intName
+ << " time=" << r.simTime
+ << " samples=" << r.samples
+ << "\n";
+ }
+ }
+ // Catch any exceptions that escaped from the batch processing and log them
+ catch (const std::exception& e) {
+ std::cerr << "Batch mode exception: " << e.what() << std::endl;
+ return EXIT_FAILURE;
+ }
+ return EXIT_SUCCESS;
+}
\ No newline at end of file
diff --git a/Sim_Engine/Engine/src/Main.cpp b/Sim_Engine/Engine/src/Main.cpp
index 86cd4cae..490db367 100644
--- a/Sim_Engine/Engine/src/Main.cpp
+++ b/Sim_Engine/Engine/src/Main.cpp
@@ -1,9 +1,166 @@
#include "pch.h"
+// File: Main.cpp
#include
#include
+#include "BatchEntry.h"
+#include "BatchArgs.h"
-int main() {
- Application app("DSFE");
- app.run();
- return 0;
+// Helper function to split a comma-separated string into a vector of strings, trimming whitespace
+static std::vector splitComma(const std::string& input) {
+ std::vector result;
+ std::stringstream ss(input);
+ std::string item;
+ // Split the input string by commas and trim whitespace from each item
+ while (std::getline(ss, item, ','))
+ if (!item.empty())
+ result.push_back(item);
+
+ return result;
+}
+
+// Helper function to parse a timestep (dt) from a string, supporting both plain decimal and fractional formats (e.g., "1/180")
+static double parseDt(const std::string& input) {
+ // Case: "1/180"
+ auto slashPos = input.find('/');
+ if (slashPos != std::string::npos) {
+ double num = std::stod(input.substr(0, slashPos));
+ double den = std::stod(input.substr(slashPos + 1));
+ return num / den;
+ }
+
+ // Case: plain decimal
+ return std::stod(input);
+}
+
+// Main entry point for the application, handling both GUI and batch modes based on command-line arguments
+int main(int argc, char** argv) {
+ bool batchMode = false;
+
+ // First pass to check for batch mode flag
+ for (int i = 1; i < argc; ++i) {
+ std::string arg(argv[i]);
+ if (arg == "--batch" || arg == "-b") {
+ batchMode = true;
+ break;
+ }
+ }
+
+ // Non-batch mode: simple pass to handle GUI-specific options
+ if (!batchMode) {
+ for (int i = 1; i < argc; ++i) {
+ std::string arg(argv[i]);
+ if (arg == "--about") {
+ std::cout << "DSFE - Dynamic Systems Framework Engine\n";
+ return 0;
+ }
+ else if (arg == "--help" || arg == "-h") {
+ std::cout << "Batch mode usage:\n";
+ std::cout << " --batch -t