Molecular Dynamics Simulation  1.0
XMLFileReader.cpp
Go to the documentation of this file.
1 #include "XMLFileReader.h"
2 
3 #include <spdlog/fmt/bundled/core.h>
4 
5 #include <filesystem>
6 #include <optional>
7 
9 #include "io/logger/Logger.h"
13 #include "simulation/Simulation.h"
14 #include "utils/Enums.h"
15 
16 std::string trim(const std::string& str) {
17  // skip whitespace and newlines
18  auto start = str.find_first_not_of(" \t\n\r\f\v");
19  auto end = str.find_last_not_of(" \t\n\r\f\v");
20 
21  if (start == std::string::npos) {
22  return "";
23  } else {
24  return str.substr(start, end - start + 1);
25  }
26 }
27 
28 std::filesystem::path sanitizePath(const std::string& text) {
29  std::string sanitized = trim(text);
30 
31  // Remove trailing slashes
32  if (sanitized.back() == '/') {
33  sanitized.pop_back();
34  }
35 
36  // replace spaces with underscores
37  std::replace(sanitized.begin(), sanitized.end(), ' ', '_');
38 
39  // to lower case
40  std::transform(sanitized.begin(), sanitized.end(), sanitized.begin(), [](unsigned char c) { return std::tolower(c); });
41 
42  return sanitized;
43 }
44 
45 std::filesystem::path convertToPath(const std::filesystem::path& base_path, const std::filesystem::path& path) {
46  auto is_relative_path = path.is_relative();
47 
48  if (is_relative_path) {
49  return base_path / path;
50  } else {
51  return path;
52  }
53 }
54 
55 int loadCheckpointFile(std::vector<Particle>& particles, const std::filesystem::path& path) {
56  std::string file_extension = path.extension().string();
57  if (file_extension != ".chkpt") {
58  Logger::logger->error("Error: file extension '{}' is not supported. Only .chkpt files can be used as checkpoints.", file_extension);
59  throw FileReader::FileFormatException("File extension is not supported");
60  }
61 
62  ChkptPointFileReader reader;
63  auto [loaded_particles, iteration] = reader.readFile(path);
64  particles.insert(particles.end(), loaded_particles.begin(), loaded_particles.end());
65 
66  Logger::logger->info("Loaded {} particles from checkpoint file {}", loaded_particles.size(), path.string());
67 
68  return iteration;
69 }
70 
71 std::optional<std::filesystem::path> loadLatestValidCheckpointFromFolder(const std::filesystem::path& base_path) {
72  if (!std::filesystem::exists(base_path)) {
73  return std::nullopt;
74  }
75 
76  std::optional<std::filesystem::path> check_point_path = std::nullopt;
77  size_t best_iteration = 0;
78  auto directory_iterator = std::filesystem::directory_iterator(base_path);
79 
80  std::set<std::filesystem::path> entries;
81  for (auto& entry : directory_iterator) {
82  if (entry.path().extension() == ".chkpt") {
83  entries.insert(entry.path());
84  }
85  }
86 
87  for (auto it = entries.rbegin(); it != entries.rend(); ++it) {
88  auto& entry = *it;
89 
90  std::string filename = entry.filename().string();
91  std::string iteration_str =
92  filename.substr(filename.find_last_of("_") + 1, filename.find_last_of(".") - filename.find_last_of("_") - 1);
93 
94  size_t current_file_number = std::stoul(iteration_str);
95 
96  if (current_file_number > best_iteration) {
97  try {
98  auto hash_valid = ChkptPointFileReader::detectSourceFileChanges(entry.string());
99 
100  if (!hash_valid) {
101  Logger::logger->warn(
102  "The input file for the checkpoint file {} has changed since the checkpoint file was created. Skipping.",
103  entry.string());
104  continue;
105  }
106 
107  best_iteration = current_file_number;
108  check_point_path = entry;
109  } catch (const FileReader::FileFormatException& e) {
110  Logger::logger->warn("Error: Could not read checkpoint file {}. Skipping.", entry.string());
111  }
112  }
113  }
114 
115  return check_point_path;
116 }
117 
118 auto loadConfig(const SubSimulationType& sub_simulation, const std::filesystem::path& curr_file_path,
119  const std::filesystem::path& base_path) {
120  // Configuration is in a separate file
121  auto other_file_name = convertToPath(base_path, std::filesystem::path(std::string(sub_simulation.path())));
122 
123  std::string file_extension = other_file_name.extension().string();
124  if (file_extension != ".xml") {
125  Logger::logger->error("Error: file extension '{}' is not supported. Only .xml files can be used as sub simulations.",
126  file_extension);
127  throw FileReader::FileFormatException("File extension is not supported");
128  }
129 
130  auto config = configuration(other_file_name);
131  return std::make_pair(*config, other_file_name);
132 }
133 
134 std::tuple<std::vector<Particle>, SimulationParams> prepareParticles(std::filesystem::path curr_file_path, ConfigurationType& config,
135  bool fresh, bool allow_recursion,
136  std::filesystem::path output_base_path = "", int depth = 0) {
137  Logger::logger->info("Constructing configuration for file {} at depth {}", curr_file_path.string(), depth);
138 
139  auto settings = config.settings();
140  auto particle_sources = config.particle_source();
141 
142  ThirdDimension third_dimension = settings.third_dimension() ? ThirdDimension::ENABLED : ThirdDimension::DISABLED;
143 
144  std::vector<Particle> particles;
145 
146  auto container_type = XSDToInternalTypeAdapter::convertToParticleContainer(settings.particle_container());
147 
148  auto interceptors = XSDToInternalTypeAdapter::convertToSimulationInterceptors(settings.interceptors(), third_dimension, container_type);
149 
150  auto forces = XSDToInternalTypeAdapter::convertToForces(settings.forces(), container_type);
151 
152  auto params = SimulationParams{curr_file_path, settings.delta_t(), settings.end_time(), container_type, interceptors,
153  std::get<0>(forces), std::get<1>(forces), std::get<2>(forces), fresh, output_base_path};
154 
155  if (output_base_path.empty()) {
156  output_base_path = params.output_dir_path;
157  }
158 
159  auto curr_folder = std::filesystem::path(curr_file_path).parent_path().string();
160 
161  bool load_in_spawners = true;
162 
163  // try to load latest checkpoint file and continue from there
164  auto latest_checkpoint_path = loadLatestValidCheckpointFromFolder(params.output_dir_path);
165  if (latest_checkpoint_path.has_value()) {
166  Logger::logger->warn("Found checkpoint file {}", latest_checkpoint_path.value().string());
167  Logger::logger->warn("Continue simulation from checkpoint?");
168  Logger::logger->warn(" [y] Continue from checkpoint [n] Start from scratch");
169 
170  char answer;
171  while (true) {
172  std::cin >> answer;
173  if (std::cin.fail() || std::cin.eof()) {
174  std::cin.clear();
175  continue;
176  }
177 
178  if (answer != 'y' && answer != 'n') {
179  Logger::logger->warn("Invalid input. Please enter 'y' or 'n'");
180  continue;
181  } else {
182  break;
183  }
184  }
185 
186  if (answer == 'y') {
187  int end_iteration = loadCheckpointFile(particles, *latest_checkpoint_path);
188 
189  params.start_iteration = end_iteration;
190  load_in_spawners = false;
191 
192  Logger::logger->warn("Continuing from checkpoint file {} with iteration {}", latest_checkpoint_path.value().string(),
193  params.start_iteration);
194  } else {
195  Logger::logger->warn("Starting simulation from scratch");
196  }
197  } else {
198  Logger::logger->warn("Error: No valid checkpoint file found in output directory {}", params.output_dir_path.string());
199  }
200 
201  if (load_in_spawners) {
202  // Spawn particles specified in the XML file
203  for (auto cuboid_spawner : particle_sources.cuboid_spawner()) {
204  auto spawner = XSDToInternalTypeAdapter::convertToCuboidSpawner(cuboid_spawner, third_dimension);
205  int num_spawned = spawner.spawnParticles(particles);
206  Logger::logger->info("Spawned {} particles from cuboid spawner", num_spawned);
207  }
208 
209  for (auto soft_body_cuboid_spawner : particle_sources.soft_body_cuboid_spawner()) {
210  // if container has outflow boundaries
211  if (std::holds_alternative<SimulationParams::LinkedCellsType>(container_type)) {
212  auto container = std::get<SimulationParams::LinkedCellsType>(container_type);
213  if (std::find(container.boundary_conditions.begin(), container.boundary_conditions.end(),
214  LinkedCellsContainer::BoundaryCondition::OUTFLOW) != container.boundary_conditions.end()) {
215  throw FileReader::FileFormatException("Soft body cuboid spawner is not supported with outflow boundary conditions");
216  }
217  }
218 
219  auto spawner = XSDToInternalTypeAdapter::convertToSoftBodyCuboidSpawner(soft_body_cuboid_spawner, third_dimension);
220  int num_spawned = spawner.spawnParticles(particles);
221  Logger::logger->info("Spawned {} particles from soft body cuboid spawner", num_spawned);
222  }
223 
224  for (auto sphere_spawner : particle_sources.sphere_spawner()) {
225  auto spawner = XSDToInternalTypeAdapter::convertToSphereSpawner(sphere_spawner, third_dimension);
226  int num_spawned = spawner.spawnParticles(particles);
227  Logger::logger->info("Spawned {} particles from sphere spawner", num_spawned);
228  }
229 
230  for (auto single_particle_spawner : particle_sources.single_particle_spawner()) {
231  auto spawner = XSDToInternalTypeAdapter::convertToSingleParticleSpawner(single_particle_spawner, third_dimension);
232  int num_spawned = spawner.spawnParticles(particles);
233  Logger::logger->info("Spawned {} particles from single particle spawner", num_spawned);
234  }
235 
236  for (auto check_point_loader : particle_sources.check_point_loader()) {
237  auto path = convertToPath(curr_folder, std::filesystem::path(std::string(check_point_loader.path())));
238  loadCheckpointFile(particles, path);
239  }
240 
241  for (auto sub_simulation : particle_sources.sub_simulation()) {
242  if (!allow_recursion) {
243  Logger::logger->warn("Error: Recursion is disabled. Skipping sub simulation at depth {}", depth);
244  continue;
245  }
246 
247  auto name = std::filesystem::path(std::string(sub_simulation.path())).stem().string();
248 
249  Logger::logger->info("Found sub simulation {} at depth {}", name, depth);
250 
251  std::filesystem::path new_output_base_path = output_base_path / sanitizePath(name);
252 
253  // Try to find a checkpoint file in the base directory
254  auto checkpoint_path = fresh ? std::nullopt : loadLatestValidCheckpointFromFolder(new_output_base_path);
255 
256  // If no checkpoint file was found, run the sub simulation
257  if (checkpoint_path.has_value()) {
258  Logger::logger->warn(
259  "Using cached result for sub simulation {} at depth {}. To force a rerun, delete the checkpoint file at {}", name,
260  depth, checkpoint_path.value().string());
261  }
262 
263  if (!checkpoint_path.has_value()) {
264  Logger::logger->info("Starting sub simulation {} at depth {}", name, depth);
265 
266  // Load the configuration from the sub simulation
267  auto [loaded_config, file_name] = loadConfig(sub_simulation, curr_file_path, curr_folder);
268 
269  // Create the initial conditions for the sub simulation
270  auto [sub_particles, sub_config] =
271  prepareParticles(file_name, loaded_config, fresh, allow_recursion, new_output_base_path, depth + 1);
272  sub_config.output_dir_path = new_output_base_path;
273 
274  // Run the sub simulation
275  Simulation simulation{sub_particles, sub_config};
276 
277  sub_config.logSummary(depth);
278  auto result = simulation.runSimulation();
279  result.logSummary(depth);
280 
281  // Write the checkpoint file
282  FileOutputHandler file_output_handler{OutputFormat::CHKPT, sub_config};
283 
284  checkpoint_path = file_output_handler.writeFile(result.total_iterations, result.resulting_particles);
285 
286  Logger::logger->info("Wrote {} particles to checkpoint file in: {}", result.resulting_particles.size(),
287  (*checkpoint_path).string());
288  }
289 
290  // Load the checkpoint file
291  loadCheckpointFile(particles, *checkpoint_path);
292  }
293  }
294 
295  if (settings.log_level()) {
296  Logger::update_level(settings.log_level().get());
297  }
298 
299  params.num_particles = particles.size();
300 
301  return std::make_tuple(particles, std::move(params));
302 }
303 
304 std::tuple<std::vector<Particle>, std::optional<SimulationParams>> XMLFileReader::readFile(const std::filesystem::path& filepath) const {
305  try {
306  auto config = configuration(filepath);
307 
308  return prepareParticles(filepath, *config, fresh, allow_recursion);
309  } catch (const xml_schema::exception& e) {
310  std::stringstream error_message;
311  error_message << "Error: could not parse file '" << filepath << "'.\n";
312  error_message << e << std::endl;
313  throw FileFormatException(error_message.str());
314  }
315 }
ThirdDimension
Enum class to define the dimension count of the simulation (2D or 3D). Affects primarily the dimensio...
Definition: Enums.h:7
std::filesystem::path convertToPath(const std::filesystem::path &base_path, const std::filesystem::path &path)
auto loadConfig(const SubSimulationType &sub_simulation, const std::filesystem::path &curr_file_path, const std::filesystem::path &base_path)
int loadCheckpointFile(std::vector< Particle > &particles, const std::filesystem::path &path)
std::string trim(const std::string &str)
std::tuple< std::vector< Particle >, SimulationParams > prepareParticles(std::filesystem::path curr_file_path, ConfigurationType &config, bool fresh, bool allow_recursion, std::filesystem::path output_base_path="", int depth=0)
std::filesystem::path sanitizePath(const std::string &text)
std::optional< std::filesystem::path > loadLatestValidCheckpointFromFolder(const std::filesystem::path &base_path)
Class to read particle and simulation data from a '.xml' file.
static bool detectSourceFileChanges(const std::string &filepath)
Checks if the given file contains a valid hash generated from the original input file.
std::tuple< std::vector< Particle >, int > readFile(const std::filesystem::path &filepath) const
Reads particle data from a '.xml' file and returns a vector of particles.
Wrapper class to abstract the writing of output files.
Exception to be thrown when the file format is invalid.
Definition: FileReader.h:31
static void update_level(std::string &log_level)
Sets the log level of the logger.
Definition: Logger.cpp:48
static std::shared_ptr< spdlog::logger > logger
Publically accessible shared pointer to the logger.
Definition: Logger.h:35
void logSummary(int depth=0) const
Prints a summary of the simulation overview to the logger.
Contains all parameters needed to run a simulation.
double delta_t
Time step of a single simulation iteration.
Class to run a simulation.
Definition: Simulation.h:20
SimulationOverview runSimulation()
Runs the simulation, using the parameters given at construction and returns a SimulationOverview obje...
Definition: Simulation.cpp:43
bool allow_recursion
Definition: XMLFileReader.h:13
std::tuple< std::vector< Particle >, std::optional< SimulationParams > > readFile(const std::filesystem::path &filepath) const override
Reads particle data from a '.xml' file and returns a vector of particles Other simulation parameters ...
static SoftBodyCuboidSpawner convertToSoftBodyCuboidSpawner(const SoftBodySpawnerType &soft_body_cuboid, ThirdDimension third_dimension)
Converts a soft body cuboid from the XSD format to the internal format.
static SphereSpawner convertToSphereSpawner(const SphereSpawnerType &sphere, ThirdDimension third_dimension)
Converts a sphere from the XSD format to the internal format.
static std::tuple< std::vector< std::shared_ptr< SimpleForceSource > >, std::vector< std::shared_ptr< PairwiseForceSource > >, std::vector< std::shared_ptr< TargettedForceSource > > > convertToForces(const ForcesType &forces, const std::variant< SimulationParams::DirectSumType, SimulationParams::LinkedCellsType > &container_data)
Converts a force type from the XSD format to the internal format.
static std::variant< SimulationParams::DirectSumType, SimulationParams::LinkedCellsType > convertToParticleContainer(const ParticleContainerType &container_type)
Converts a container type from the XSD format to the internal format.
static CuboidSpawner convertToSingleParticleSpawner(const SingleParticleSpawnerType &particle, ThirdDimension third_dimension)
Converts a particle from the XSD format to the internal format.
static std::vector< std::shared_ptr< SimulationInterceptor > > convertToSimulationInterceptors(const SimulationInterceptorsType &interceptors, ThirdDimension third_dimension, std::variant< SimulationParams::DirectSumType, SimulationParams::LinkedCellsType > container_type)
Converts the simulation interceptors from the XSD format to the internal format.
static CuboidSpawner convertToCuboidSpawner(const CuboidSpawnerType &cuboid, ThirdDimension third_dimension)
Converts a cuboid from the XSD format to the internal format.