OPALX (Object Oriented Parallel Accelerator Library for Exascal) master (dc2a29eed580)
OPALX
Loading...
Searching...
No Matches
EmittedFromFile.cpp
Go to the documentation of this file.
1#include "EmittedFromFile.h"
2
3#include <mpi.h>
4
5#include <algorithm>
6#include <array>
7#include <cctype>
8#include <cmath>
9#include <filesystem>
10#include <fstream>
11#include <sstream>
12
13#include <Kokkos_Core.hpp>
14
16#include "Physics/Physics.h"
18
19namespace {
20 std::string trim(const std::string& input) {
21 const auto first = input.find_first_not_of(" \t\r\n");
22 if (first == std::string::npos) {
23 return "";
24 }
25 const auto last = input.find_last_not_of(" \t\r\n");
26 return input.substr(first, last - first + 1);
27 }
28
29 std::string lowercase(std::string value) {
30 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
31 return static_cast<char>(std::tolower(c));
32 });
33 return value;
34 }
35
36 bool parseSingleInteger(const std::string& line, long long& value) {
37 std::istringstream stream(line);
38 stream >> value;
39 return stream && stream.eof();
40 }
41
42 bool looksLikeHeader(const std::string& line) {
43 const std::string lower = lowercase(line);
44 return lower.find("x") != std::string::npos && lower.find("px") != std::string::npos
45 && lower.find("y") != std::string::npos && lower.find("py") != std::string::npos
46 && lower.find("t") != std::string::npos && lower.find("pz") != std::string::npos;
47 }
48
49 bool parseRecordValues(const std::string& line, std::array<double, 6>& record) {
50 std::istringstream stream(line);
51 std::vector<double> values;
52 double value = 0.0;
53 while (stream >> value) {
54 values.push_back(value);
55 }
56 if (values.size() < 6) {
57 return false;
58 }
59
60 for (size_t i = 0; i < record.size(); ++i) {
61 record[i] = values[i];
62 }
63 return true;
64 }
65} // namespace
66
68 std::shared_ptr<ParticleContainer_t> pc, std::shared_ptr<FieldContainer_t> fc,
69 Distribution_t* opalDist)
70 : SamplingBase(pc, fc, opalDist) {
71 filename_m = opalDist->getFilename();
73 if (filename_m.empty()) {
74 throw OpalException(
75 "EmittedFromFile::EmittedFromFile",
76 "FNAME attribute must be set for EMITTEDFROMFILE distribution type.");
77 }
78
81}
82
84 std::shared_ptr<ParticleContainer_t> pc, std::shared_ptr<FieldContainer_t> fc,
85 const std::string& filename)
86 : SamplingBase(pc, fc), filename_m(filename) {
87 if (filename_m.empty()) {
88 throw OpalException("EmittedFromFile::EmittedFromFile", "Filename must not be empty.");
89 }
90
92}
93
95 namespace fs = std::filesystem;
96 if (fs::exists(filename_m)) {
97 return;
98 }
99
100 const std::string inputFile = OpalData::getInstance()->getInputFn();
101 if (!inputFile.empty()) {
102 const fs::path filePath = fs::path(inputFile).parent_path() / filename_m;
103 if (fs::exists(filePath)) {
104 filename_m = filePath.string();
105 }
106 }
107}
108
109void EmittedFromFile::readFile(const std::string& filename) {
110 std::ifstream file(filename);
111 if (!file.is_open()) {
112 throw OpalException("EmittedFromFile::readFile", "Couldn't open file '" + filename + "'.");
113 }
114
115 rawRecords_m.clear();
116 bool sawFirstPayload = false;
117 bool expectHeaderAfterN = false;
118 bool hasDeclaredCount = false;
119 size_t declaredCount = 0;
120 size_t lineNumber = 0;
121
122 std::string line;
123 while (std::getline(file, line)) {
124 ++lineNumber;
125 std::string stripped = trim(line);
126 if (stripped.empty()) {
127 continue;
128 }
129
130 if (stripped[0] == '#') {
131 // Old OPAL emitted dumps use a comment header. The data order is
132 // positional, so the header is accepted but not needed for mapping.
133 continue;
134 }
135
136 if (!sawFirstPayload) {
137 long long count = 0;
138 if (parseSingleInteger(stripped, count)) {
139 if (count < 0) {
140 throw OpalException(
141 "EmittedFromFile::readFile",
142 "Declared particle count in '" + filename + "' must be non-negative.");
143 }
144 declaredCount = static_cast<size_t>(count);
145 hasDeclaredCount = true;
146 expectHeaderAfterN = true;
147 sawFirstPayload = true;
148 continue;
149 }
150 sawFirstPayload = true;
151 }
152
153 if (expectHeaderAfterN) {
154 if (!looksLikeHeader(stripped)) {
155 throw OpalException(
156 "EmittedFromFile::readFile",
157 "Expected a header containing x px y py t pz after the count line in '"
158 + filename + "'.");
159 }
160 expectHeaderAfterN = false;
161 continue;
162 }
163
164 if (looksLikeHeader(stripped)) {
165 continue;
166 }
167
168 std::array<double, 6> values;
169 if (!parseRecordValues(stripped, values)) {
170 throw OpalException(
171 "EmittedFromFile::readFile", "Line " + std::to_string(lineNumber) + " in '"
172 + filename
173 + "' has fewer than six numeric columns.");
174 }
175 RawRecord record;
176 record.x = values[0];
177 record.px = values[1];
178 record.y = values[2];
179 record.py = values[3];
180 record.fileTime = values[4];
181 record.pz = values[5];
182 rawRecords_m.push_back(record);
183 }
184
185 if (expectHeaderAfterN) {
186 throw OpalException(
187 "EmittedFromFile::readFile",
188 "Missing header line after declared particle count in '" + filename + "'.");
189 }
190
191 if (rawRecords_m.empty()) {
192 throw OpalException(
193 "EmittedFromFile::readFile",
194 "No valid emitted particle data found in '" + filename + "'.");
195 }
196
197 if (hasDeclaredCount && rawRecords_m.size() != declaredCount) {
198 throw OpalException(
199 "EmittedFromFile::readFile",
200 "Number of data lines (" + std::to_string(rawRecords_m.size())
201 + ") does not match declared count (" + std::to_string(declaredCount)
202 + ") in '" + filename + "'.");
203 }
204}
205
206void EmittedFromFile::generateParticles(size_t& numberOfParticles, Vector_t<double, 3> /*nr*/) {
207 if (emissionModel_m != "NONE") {
208 throw OpalException(
209 "EmittedFromFile::generateParticles",
210 "EMISSIONMODEL '" + emissionModel_m
211 + "' is not supported for EMITTEDFROMFILE distributions");
212 }
213
214 size_t requested = numberOfParticles;
215 if (opalDist_m && opalDist_m->getNumParticles() > 0) {
216 requested = opalDist_m->getNumParticles();
217 }
218
219 buildInventory(requested);
220 numberOfParticles = records_m.size();
221}
222
223void EmittedFromFile::buildInventory(size_t requested) {
224 const size_t selected =
225 requested > 0 ? std::min(requested, rawRecords_m.size()) : rawRecords_m.size();
226
227 records_m.clear();
228 records_m.reserve(selected);
230 inventoryBuilt_m = false;
232 emissionTime_m = 0.0;
233
234 if (selected == 0) {
235 if (opalDist_m) {
237 }
238 inventoryBuilt_m = true;
239 return;
240 }
241
242 double minPulseTime = -rawRecords_m[0].fileTime;
243 double maxPulseTime = minPulseTime;
244 for (size_t i = 1; i < selected; ++i) {
245 const double pulseTime = -rawRecords_m[i].fileTime;
246 minPulseTime = std::min(minPulseTime, pulseTime);
247 maxPulseTime = std::max(maxPulseTime, pulseTime);
248 }
249 emissionTime_m = std::max(0.0, maxPulseTime - minPulseTime);
250 const double pulseCenter = 0.5 * (minPulseTime + maxPulseTime);
251 if (opalDist_m) {
253 }
254
255 double pxSum = 0.0;
256 double pySum = 0.0;
257 double pzSum = 0.0;
258 for (size_t i = 0; i < selected; ++i) {
259 Record record;
260 record.raw = rawRecords_m[i];
261 record.birthTime = t0_m - record.raw.fileTime - pulseCenter;
262 records_m.push_back(record);
263 pxSum += record.raw.px + P0_m[0];
264 pySum += record.raw.py + P0_m[1];
265 pzSum += record.raw.pz + P0_m[2];
266 }
267
268 std::stable_sort(records_m.begin(), records_m.end(), [](const Record& lhs, const Record& rhs) {
269 return lhs.birthTime < rhs.birthTime;
270 });
271
272 const double invSelected = 1.0 / static_cast<double>(selected);
273 initialRefP_m[0] = pxSum * invSelected;
274 initialRefP_m[1] = pySum * invSelected;
275 initialRefP_m[2] = pzSum * invSelected;
276 inventoryBuilt_m = true;
277}
278
279std::pair<size_t, size_t> EmittedFromFile::computeLocalEmitRange(size_t totalToEmit) const {
280 if (!pc_m || totalToEmit == 0) {
281 return {0, 0};
282 }
283
284 const int nranks = ippl::Comm->size();
285 const int rank = ippl::Comm->rank();
286 if (nranks <= 0) {
287 return {0, totalToEmit};
288 }
289
290 const size_t capacity = pc_m->R.size();
291 const size_t localNum = pc_m->getLocalNum();
292 const size_t spaceLeft = (localNum <= capacity) ? (capacity - localNum) : size_t(0);
293
294 std::vector<unsigned long> spaceLeftAll(static_cast<size_t>(nranks), 0);
295 unsigned long mySpace = static_cast<unsigned long>(spaceLeft);
296 MPI_Allgather(
297 &mySpace, 1, MPI_UNSIGNED_LONG, spaceLeftAll.data(), 1, MPI_UNSIGNED_LONG,
298 ippl::Comm->getCommunicator());
299
300 const size_t nranksU = static_cast<size_t>(nranks);
301 const size_t base = totalToEmit / nranksU;
302 const size_t rem = totalToEmit % nranksU;
303
304 std::vector<size_t> nlocal(nranksU, 0);
305 size_t assigned = 0;
306 for (int r = 0; r < nranks; ++r) {
307 const size_t ideal = base + (static_cast<size_t>(r) < rem ? 1 : 0);
308 const size_t cap = static_cast<size_t>(spaceLeftAll[static_cast<size_t>(r)]);
309 nlocal[static_cast<size_t>(r)] = std::min(ideal, cap);
310 assigned += nlocal[static_cast<size_t>(r)];
311 }
312
313 size_t deficit = totalToEmit - assigned;
314 while (deficit > 0) {
315 int chosen = -1;
316 for (int r = 0; r < nranks; ++r) {
317 const size_t cap = static_cast<size_t>(spaceLeftAll[static_cast<size_t>(r)]);
318 if (nlocal[static_cast<size_t>(r)] < cap) {
319 chosen = r;
320 break;
321 }
322 }
323 if (chosen < 0) {
324 throw OpalException(
325 "EmittedFromFile::computeLocalEmitRange",
326 "Particle container capacity is insufficient for EMITTEDFROMFILE emission. "
327 "Increase BEAM::NALLOC or reduce NPARTDIST.");
328 }
329 nlocal[static_cast<size_t>(chosen)] += 1;
330 --deficit;
331 }
332
333 size_t offset = 0;
334 for (int r = 0; r < rank; ++r) {
335 offset += nlocal[static_cast<size_t>(r)];
336 }
337 return {offset, nlocal[static_cast<size_t>(rank)]};
338}
339
340void EmittedFromFile::emitParticles(double t, double dt) {
341 if (!inventoryBuilt_m || records_m.empty()) {
342 return;
343 }
344 if (nextGlobalIndex_m >= records_m.size()) {
345 return;
346 }
347
348 const double tEnd = t + dt;
349 auto emitEndIt = std::upper_bound(
350 records_m.begin() + nextGlobalIndex_m, records_m.end(), tEnd,
351 [](double value, const Record& record) {
352 return value < record.birthTime;
353 });
354 const size_t globalEnd = static_cast<size_t>(emitEndIt - records_m.begin());
355 const size_t totalNew = globalEnd - nextGlobalIndex_m;
356 if (totalNew == 0) {
357 return;
358 }
359
360 const double overdueTolerance = std::max(1.0e-18, std::abs(dt) * 1.0e-12);
361 if (records_m[nextGlobalIndex_m].birthTime < t - overdueTolerance) {
362 throw OpalException(
363 "EmittedFromFile::emitParticles",
364 "EMITTEDFROMFILE found an overdue birth time. This means the tracker "
365 "started too late or skipped a timestep containing file-emitted particles.");
366 }
367
368 const auto [localOffset, nNew] = computeLocalEmitRange(totalNew);
369 const size_type nlocalBefore = pc_m->getLocalNum();
370 pc_m->createParticles(static_cast<size_type>(nNew));
371
372 generateLocalParticles(nlocalBefore, nextGlobalIndex_m + localOffset, nNew, t, dt);
373 nextGlobalIndex_m = globalEnd;
374}
375
377 size_type nlocalBefore, size_t globalBegin, size_t nNew, double tStart, double dt) {
378 if (nNew == 0) {
379 return;
380 }
381
382 using HostView = Kokkos::View<double**, Kokkos::HostSpace>;
383 HostView hostData("EmittedFromFile_hostData", nNew, 6);
384 const double tEnd = tStart + dt;
385 const double overdueTolerance = std::max(1.0e-18, std::abs(dt) * 1.0e-12);
386
387 for (size_t i = 0; i < nNew; ++i) {
388 const Record& record = records_m[globalBegin + i];
389 if (record.birthTime < tStart - overdueTolerance) {
390 throw OpalException(
391 "EmittedFromFile::generateLocalParticles",
392 "EMITTEDFROMFILE would need to pre-age a particle born before "
393 "the current step.");
394 }
395 const double effectiveBirthTime = record.birthTime < tStart ? tStart : record.birthTime;
396 const double stepDt = tEnd - effectiveBirthTime;
397
398 hostData(i, 0) = record.raw.x;
399 hostData(i, 1) = record.raw.y;
400 hostData(i, 2) = record.raw.px;
401 hostData(i, 3) = record.raw.py;
402 hostData(i, 4) = record.raw.pz;
403 hostData(i, 5) = stepDt;
404 }
405
406 auto deviceData =
407 Kokkos::create_mirror(Kokkos::DefaultExecutionSpace::memory_space(), hostData);
408 Kokkos::deep_copy(deviceData, hostData);
409
410 auto Rview = pc_m->R.getView();
411 auto Pview = pc_m->P.getView();
412 auto dtView = pc_m->dt.getView();
413 const Vector_t<double, 3> R0 = R0_m;
414 const Vector_t<double, 3> P0 = P0_m;
415 const size_type offset = nlocalBefore;
416 const double c = Physics::c;
417
418 Kokkos::parallel_for(
419 "EmittedFromFile_generateLocalParticles", nNew, KOKKOS_LAMBDA(const size_t i) {
420 const size_t j = offset + i;
422 deviceData(i, 2) + P0[0], deviceData(i, 3) + P0[1],
423 deviceData(i, 4) + P0[2]);
424 const double stepDt = deviceData(i, 5);
425
426 Rview(j)[0] = deviceData(i, 0) + R0[0];
427 Rview(j)[1] = deviceData(i, 1) + R0[1];
428 Rview(j)[2] = R0[2];
429 Pview(j) = p;
430 dtView(j) = stepDt;
431
432 const double gamma = Kokkos::sqrt(1.0 + p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
433 const double drift = 0.5 * c * stepDt / gamma;
434 Rview(j)[0] += p[0] * drift;
435 Rview(j)[1] += p[1] * drift;
436 Rview(j)[2] += p[2] * drift;
437 });
438 Kokkos::fence();
439
440 pc_m->markMomentsDirty();
441}
ippl::Vector< T, Dim > Vector_t
Defines an emitted file distribution with old-OPAL time-column semantics.
std::string getFilename() const
size_t getNumParticles() const
void setTEmission(double tEmission)
size_t getEmissionSteps() const
double py
Vertical momentum offset from the file.
RawRecord raw
Original parsed file row.
EmittedFromFile(std::shared_ptr< ParticleContainer_t > pc, std::shared_ptr< FieldContainer_t > fc, Distribution_t *opalDist)
Constructor for EmittedFromFile.
double birthTime
Birth time in the OPALX tracker convention.
double fileTime
Old-OPAL pre-emission time column.
bool inventoryBuilt_m
True once records_m is ready for emission.
double x
Horizontal position from the file.
void emitParticles(double t, double dt) override
Emits all file records whose birth times fall into the current step.
double emissionTime_m
Total emission time spanned by records_m.
void generateLocalParticles(size_type nlocalBefore, size_t globalBegin, size_t nNew, double tStart, double dt)
Generates the local particles assigned to this rank for one emission step.
std::pair< size_t, size_t > computeLocalEmitRange(size_t totalToEmit) const
Computes the local contiguous subrange of a global emission batch.
ippl::detail::size_type size_type
size_t nextGlobalIndex_m
First global record index not emitted yet.
void readFile(const std::string &filename)
Reads and parses an old-OPAL emitted particle file.
std::string filename_m
File path to read emitted particle data from.
Vector_t< double, 3 > initialRefP_m
Average initial reference momentum.
void resolveFilenameFromInput()
Resolves relative file paths against the active input file directory.
double pz
Longitudinal momentum offset from the file.
void buildInventory(size_t requested)
Selects records, converts file times to birth times, and sorts them.
std::vector< RawRecord > rawRecords_m
Parsed file rows before selection and sorting.
size_t emissionSteps_m
Number of steps used to derive emission dt.
double px
Horizontal momentum offset from the file.
std::vector< Record > records_m
Selected records sorted by tracker birth time.
void generateParticles(size_t &numberOfParticles, Vector_t< double, 3 > nr) override
Reads the selected file records and builds the birth-time inventory.
double y
Vertical position from the file.
Raw row parsed from an old-OPAL emitted distribution dump.
Selected file row plus its converted tracker birth time.
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).
std::string emissionModel_m
Distribution_t * opalDist_m
std::shared_ptr< ParticleContainer_t > pc_m
constexpr double c
The velocity of light in m/s.
Definition Physics.h:60