OPALX (Object Oriented Parallel Accelerator Library for Exascal) master (dc2a29eed580)
OPALX
Loading...
Searching...
No Matches
FromFile.cpp
Go to the documentation of this file.
1#include "FromFile.h"
2#include <Kokkos_Core.hpp>
3#include <algorithm>
4#include <cctype>
5#include <filesystem>
6#include <fstream>
7#include <memory>
8#include <sstream>
10#include "Distribution.h"
11#include "SamplingBase.hpp"
13#include "Utility/Inform.h"
14
16 std::shared_ptr<ParticleContainer_t> pc, std::shared_ptr<FieldContainer_t> fc,
17 Distribution_t* opalDist)
18 : SamplingBase(pc, fc, opalDist), numParticles_m(0) {
19 // Get filename from distribution
20 filename_m = opalDist->getFilename();
21
22 if (filename_m.empty()) {
23 throw OpalException(
24 "FromFile::FromFile",
25 "FNAME attribute must be set for FROMFILE distribution type.");
26 }
27
28 // Resolve file path (check relative to input file directory)
29 namespace fs = std::filesystem;
30 if (!fs::exists(filename_m)) {
31 // Try relative to input file directory
32 std::string inputDir = OpalData::getInstance()->getInputFn();
33 if (!inputDir.empty()) {
34 fs::path inputPath(inputDir);
35 fs::path filePath = inputPath.parent_path() / filename_m;
36 if (fs::exists(filePath)) {
37 filename_m = filePath.string();
38 }
39 }
40 }
41
42 // Read and parse the file
44}
45
47 std::shared_ptr<ParticleContainer_t> pc, std::shared_ptr<FieldContainer_t> fc,
48 const std::string& filename)
49 : SamplingBase(pc, fc), numParticles_m(0) {
50 filename_m = filename;
51
52 if (filename_m.empty()) {
53 throw OpalException("FromFile::FromFile", "Filename must not be empty.");
54 }
55
56 // Read and parse the file
58}
59
60void FromFile::readFile(const std::string& filename) {
61 std::ifstream file(filename);
62 if (!file.is_open()) {
63 throw OpalException("FromFile::readFile", "Couldn't open file '" + filename + "'.");
64 }
65
66 // Helper to read next non-empty, non-comment line; returns false if none left
67 auto nextLine = [&file](std::string& out) -> bool {
68 std::string line;
69 while (std::getline(file, line)) {
70 auto first = line.find_first_not_of(" \t\r\n");
71 if (first == std::string::npos) continue;
72 auto last = line.find_last_not_of(" \t\r\n");
73 line = line.substr(first, last - first + 1);
74 if (line.empty() || line[0] == '#') continue;
75 out = line;
76 return true;
77 }
78 return false;
79 };
80
81 // Required format: line 1 = particle count, line 2 = column names, then data lines
82 std::string line1;
83 if (!nextLine(line1)) {
84 throw OpalException("FromFile::readFile",
85 "File '" + filename + "' is empty or has no valid lines. "
86 "Expected format: first line = number of particles, second line = column names.");
87 }
88
89 // First line must be a single integer (particle count)
90 std::istringstream iss1(line1);
91 long long npart = 0;
92 if (!(iss1 >> npart) || npart < 0 || !iss1.eof()) {
93 throw OpalException("FromFile::readFile",
94 "First non-empty line in '" + filename + "' must be the number of particles (single integer). "
95 "Got: '" + line1 + "'. Expected format: N\\n column_names\\n data...");
96 }
97 size_t expectedNumParticles = static_cast<size_t>(npart);
98
99 std::string headerLine;
100 if (!nextLine(headerLine)) {
101 throw OpalException("FromFile::readFile",
102 "Missing header line in '" + filename + "'. "
103 "Expected format: N\\n column_names\\n data... (e.g. x px y py z pz).");
104 }
105
106 columnIndices_m = parseHeader(headerLine);
107
108 // Read data lines (comments and blank lines are skipped)
109 particleData_m.clear();
110 size_t lineNumber = 3;
111
112 std::string line;
113 while (std::getline(file, line)) {
114 auto first = line.find_first_not_of(" \t\r\n");
115 if (first == std::string::npos) {
116 ++lineNumber;
117 continue;
118 }
119 auto last = line.find_last_not_of(" \t\r\n");
120 line = line.substr(first, last - first + 1);
121 if (line.empty() || line[0] == '#') {
122 ++lineNumber;
123 continue;
124 }
125
126 std::istringstream lineStream(line);
127 std::vector<double> values;
128 double value;
129 while (lineStream >> value)
130 values.push_back(value);
131
132 size_t maxColIdx = *std::max_element(columnIndices_m.begin(), columnIndices_m.end());
133 if (values.size() <= maxColIdx) {
134 throw OpalException(
135 "FromFile::readFile",
136 "Line " + std::to_string(lineNumber) + " in '" + filename
137 + "' has fewer columns (" + std::to_string(values.size())
138 + ") than required (index " + std::to_string(maxColIdx) + ").");
139 }
140
141 std::vector<double> phaseSpace(6);
142 phaseSpace[0] = values[columnIndices_m[0]];
143 phaseSpace[1] = values[columnIndices_m[1]];
144 phaseSpace[2] = values[columnIndices_m[2]];
145 phaseSpace[3] = values[columnIndices_m[3]];
146 phaseSpace[4] = values[columnIndices_m[4]];
147 phaseSpace[5] = values[columnIndices_m[5]];
148 particleData_m.push_back(phaseSpace);
149 ++lineNumber;
150 }
151
152 file.close();
154
155 if (numParticles_m == 0) {
156 throw OpalException(
157 "FromFile::readFile", "No valid particle data found in '" + filename + "'.");
158 }
159
160 if (numParticles_m != expectedNumParticles) {
161 throw OpalException(
162 "FromFile::readFile", "Number of data lines (" + std::to_string(numParticles_m)
163 + ") does not match declared count ("
164 + std::to_string(expectedNumParticles) + ") in '"
165 + filename + "'.");
166 }
167}
168
169std::vector<size_t> FromFile::parseHeader(const std::string& headerLine) {
170 std::istringstream headerStream(headerLine);
171 std::vector<std::string> columnNames;
172 std::string token;
173
174 while (headerStream >> token) {
175 columnNames.push_back(token);
176 }
177
178 // Map column names to indices
179 std::vector<size_t> indices(6, SIZE_MAX);
180
181 for (size_t i = 0; i < columnNames.size(); ++i) {
182 std::string normalized = normalizeColumnName(columnNames[i]);
183
184 if (normalized == "x" && indices[0] == SIZE_MAX) {
185 indices[0] = i;
186 } else if (normalized == "y" && indices[1] == SIZE_MAX) {
187 indices[1] = i;
188 } else if (normalized == "z" && indices[2] == SIZE_MAX) {
189 indices[2] = i;
190 } else if ((normalized == "px" || normalized == "px/momentumx") && indices[3] == SIZE_MAX) {
191 indices[3] = i;
192 } else if ((normalized == "py" || normalized == "py/momentumy") && indices[4] == SIZE_MAX) {
193 indices[4] = i;
194 } else if ((normalized == "pz" || normalized == "pz/momentumz") && indices[5] == SIZE_MAX) {
195 indices[5] = i;
196 }
197 }
198
199 // Require all six phase-space columns in the header
200 for (size_t i = 0; i < 6; ++i) {
201 if (indices[i] == SIZE_MAX) {
202 const char* names[] = {"x", "y", "z", "px", "py", "pz"};
203 throw OpalException(
204 "FromFile::parseHeader",
205 "Header must contain all column names (x, y, z, px, py, pz). "
206 "Missing or unrecognized: '"
207 + std::string(names[i]) + "'.");
208 }
209 }
210
211 return indices;
212}
213
214std::string FromFile::normalizeColumnName(const std::string& name) {
215 std::string normalized = name;
216
217 // Convert to lowercase
218 std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char c) {
219 return std::tolower(c);
220 });
221
222 // Remove whitespace
223 normalized.erase(
224 std::remove_if(
225 normalized.begin(), normalized.end(),
226 [](unsigned char c) {
227 return std::isspace(c);
228 }),
229 normalized.end());
230
231 return normalized;
232}
233
234void FromFile::generateParticles(size_t& numberOfParticles, Vector_t<double, 3> /*nr*/) {
235 if (emissionModel_m != "NONE")
236 throw OpalException(
237 "FromFile::generateParticles",
238 "EMISSIONMODEL '" + emissionModel_m
239 + "' is not supported for FROMFILE distributions");
240
241 // Only generate during initial sampling (t0 <= 0). For t0 > 0, this
242 // distribution is time-independent and should not contribute here unless
243 // explicitly triggered via emitParticles (which sets hasEmittedOnce_m).
244 if (t0_m > 0.0 && !hasEmittedOnce_m) {
245 return;
246 }
247 Inform m("FromFile::generateParticles");
248
249 // Use number of particles from file if available, otherwise use requested number
250 size_t totalParticles = (numParticles_m > 0) ? numParticles_m : numberOfParticles;
251
252 // If file has fewer particles than requested, use file count
253 if (numParticles_m > 0 && numParticles_m < numberOfParticles) {
254 m << "Warning: File contains " << numParticles_m << " particles, but " << numberOfParticles
255 << " were requested." << endl;
256 m << "Using " << numParticles_m << " particles from file." << endl;
257 totalParticles = numParticles_m;
258 }
259
260 // If file has more particles than requested, use requested count
261 if (numParticles_m > numberOfParticles) {
262 totalParticles = numberOfParticles;
263 }
264
265 numberOfParticles = totalParticles;
266
267 // Distribute particles across MPI ranks (capacity-aware)
268 const int rank = ippl::Comm->rank();
269 const int nranks = std::max(1, ippl::Comm->size());
270 const size_t nranks_u = static_cast<size_t>(nranks);
271 size_t nlocal = pc_m ? computeLocalEmitCount(totalParticles)
272 : (totalParticles / nranks_u
273 + (static_cast<size_t>(rank) < (totalParticles % nranks_u) ? 1 : 0));
274 // Use Mpi scan to get the start index for each rank (since they are all potentially different!)
275 size_t startIdx = 0;
276 if (pc_m && ippl::Comm->size() > 0) {
277 unsigned long nlocalUL = static_cast<unsigned long>(nlocal);
278 unsigned long startIdxUL = 0;
279 MPI_Exscan(
280 &nlocalUL, &startIdxUL, 1, MPI_UNSIGNED_LONG, MPI_SUM,
281 ippl::Comm->getCommunicator());
282 if (rank > 0) {
283 startIdx = static_cast<size_t>(startIdxUL);
284 }
285 }
286
287 // Allocate particles, appending after any existing ones.
288 const size_t nlocalCurrent = pc_m->getLocalNum();
289 pc_m->createParticles(nlocal);
290
291 // Use subviews over the newly created slots, matching the Gaussian pattern.
292 view_type RviewFull = pc_m->R.getView();
293 auto Rview = Kokkos::subview(RviewFull, std::make_pair(nlocalCurrent, nlocalCurrent + nlocal));
294 view_type PviewFull = pc_m->P.getView();
295 auto Pview = Kokkos::subview(PviewFull, std::make_pair(nlocalCurrent, nlocalCurrent + nlocal));
296
297 // Create Kokkos view for particle data (host accessible)
298 using HostView = Kokkos::View<double**, Kokkos::HostSpace>;
299 HostView hostParticleData("hostParticleData", totalParticles, 6);
300
301 // Copy data to host view
302 for (size_t i = 0; i < totalParticles && i < particleData_m.size(); ++i) {
303 for (int j = 0; j < 6; ++j) {
304 hostParticleData(i, j) = particleData_m[i][j];
305 }
306 }
307
308 // Create device mirror with layout compatible for Host->Device deep_copy
309 // (required when no common execution space, e.g. Host vs Cuda)
310 auto deviceParticleData =
311 Kokkos::create_mirror(Kokkos::DefaultExecutionSpace::memory_space(), hostParticleData);
312 Kokkos::deep_copy(deviceParticleData, hostParticleData);
313
314 // Copy particle data into the subview [0, nlocal).
315 Kokkos::parallel_for(
316 "FromFile::generateParticles", nlocal, KOKKOS_LAMBDA(const size_t k) {
317 const size_t dataIdx = startIdx + k;
318 Rview(k)[0] = deviceParticleData(dataIdx, 0); // x
319 Rview(k)[1] = deviceParticleData(dataIdx, 1); // y
320 Rview(k)[2] = deviceParticleData(dataIdx, 2); // z
321 Pview(k)[0] = deviceParticleData(dataIdx, 3); // px
322 Pview(k)[1] = deviceParticleData(dataIdx, 4); // py
323 Pview(k)[2] = deviceParticleData(dataIdx, 5); // pz
324 });
325 Kokkos::fence();
326
327 // Apply per-emission-source offsets after all mean-fixing/corrections.
328 // EMISSIONSOURCE offsets are expected to translate the generated bunch
329 // without being affected by the internal "fix mean" logic.
330 const Vector_t<double, 3> R0 = R0_m;
331 const Vector_t<double, 3> P0 = P0_m;
332 Kokkos::parallel_for(
333 nlocal, KOKKOS_LAMBDA(const size_t k) {
334 Rview(k) += R0;
335 Pview(k) += P0;
336 });
337
338 Inform mALL("FromFile::generateParticles", INFORM_ALL_NODES);
339 mALL << "FromFile: Loaded " << totalParticles << " particles from file '" << filename_m << "'"
340 << endl;
341 ippl::Comm->barrier();
342 mALL << "Rank " << rank << ": " << nlocal << " local particles" << endl;
343 ippl::Comm->barrier();
344
345 fillPolarization(nlocalCurrent, nlocal);
346
347 pc_m->markMomentsDirty();
348}
349
350void FromFile::emitParticles(double t, double dt) {
351 // One-shot delayed emission for FROMFILE sources.
352 const double tStart = t;
353 const double tEnd = t + dt;
354
355 if (hasEmittedOnce_m) {
356 // Don't sample again.
357 return;
358 }
359
360 if (t0_m <= 0.0) {
361 return;
362 }
363
364 if (!(tStart <= t0_m && t0_m < tEnd)) {
365 // Not time to emit yet.
366 return;
367 }
368
369 // If bound to a Distribution, respect its NPARTDIST; otherwise fall back to file count.
370 size_t requested = numParticles_m;
371 if (opalDist_m && opalDist_m->getNumParticles() > 0) {
372 requested = opalDist_m->getNumParticles();
373 }
374 if (requested == 0) {
375 // Short circuit: no particles to emit.
376 return;
377 }
378
379 const size_t nlocalBefore = pc_m->getLocalNum();
380
381 hasEmittedOnce_m = true;
382 Vector_t<double, 3> dummyNr(0.0);
383 generateParticles(requested, dummyNr);
384
385 // Set per-particle dt for newly created particles.
386 // switchToUnitlessPositions(use_dt_per_particle=true) in pushParticles scales
387 // R by 1/(c * dtview(i)). New particles come with dtview = 0 (Kokkos
388 // zero-init), so that division produces inf, and the subsequent multiply by
389 // c*0 in switchOffUnitlessPositions gives inf*0 = NaN positions.
390 const size_t nlocalAfter = pc_m->getLocalNum();
391 const size_t nNew = nlocalAfter - nlocalBefore;
392 if (nNew > 0) {
393 const double fracDt = tEnd - t0_m;
394 auto dtview = pc_m->dt.getView();
395 const size_t offset = nlocalBefore;
396 Kokkos::parallel_for(
397 "FromFile_setDt", nNew,
398 KOKKOS_LAMBDA(const size_t j) { dtview(offset + j) = fracDt; });
399 Kokkos::fence();
400 }
401}
ippl::Vector< T, Dim > Vector_t
typename ippl::detail::ViewType< ippl::Vector< double, Dim >, 1 >::view_type view_type
Defines the FromFile class used for reading particle phase space from ASCII files.
std::string getFilename() const
size_t getNumParticles() const
std::vector< size_t > parseHeader(const std::string &headerLine)
Parses the header line to get column indices. Header must contain all of x, y, z, px,...
Definition FromFile.cpp:169
size_t numParticles_m
Number of particles in the file.
Definition FromFile.h:103
void readFile(const std::string &filename)
Reads and parses the particle file. Format must be: N (particle count), then header line (column name...
Definition FromFile.cpp:60
std::string normalizeColumnName(const std::string &name)
Normalizes column name for comparison (case-insensitive, whitespace handling).
Definition FromFile.cpp:214
FromFile(std::shared_ptr< ParticleContainer_t > pc, std::shared_ptr< FieldContainer_t > fc, Distribution_t *opalDist)
Constructor for FromFile.
Definition FromFile.cpp:15
void generateParticles(size_t &numberOfParticles, Vector_t< double, 3 > nr) override
Generates particles by reading from file.
Definition FromFile.cpp:234
std::string filename_m
File path to read particle data from.
Definition FromFile.h:97
std::vector< size_t > columnIndices_m
Column indices mapping: [x_idx, y_idx, z_idx, px_idx, py_idx, pz_idx].
Definition FromFile.h:106
std::vector< std::vector< double > > particleData_m
Stored particle data: each element is a 6D phase space vector [x, y, z, px, py, pz].
Definition FromFile.h:100
void emitParticles(double t, double dt) override
Time-stepped emission hook for one-shot delayed file-based injection.
Definition FromFile.cpp:350
std::string getInputFn()
get opals input filename
Definition OpalData.cpp:569
static OpalData * getInstance()
Definition OpalData.cpp:193
Vector_t< double, 3 > P0_m
Vector_t< double, 3 > R0_m
Emission source offset: position R0, momentum P0, start time t0 (applied in sample step).
bool hasEmittedOnce_m
For one-shot emitters (e.g. Gaussian at delayed t0): guard to avoid double sampling.
size_t computeLocalEmitCount(size_t totalToSample) const
Computes the number of particles this rank should emit so that the global total equals totalToSample ...
std::string emissionModel_m
Distribution_t * opalDist_m
void fillPolarization(size_t startIdx, size_t count)
std::shared_ptr< ParticleContainer_t > pc_m