FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
memory_projection.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
7#include "memory_projection.h"
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <cstdint>
13#include <format>
14#include <fstream>
15#include <limits>
16#include <nlohmann/json.hpp>
17#include <optional>
18#include <string>
19#include <unordered_map>
20
21#if defined(__APPLE__) && defined(__MACH__)
22#include <mach/mach.h>
23#elif defined(__linux__)
24#include <unistd.h>
25#endif
26
27#include "core/logging.h"
28#include "core/parameters.h"
29#include "core/sim_id.h"
30#include "core/world.h"
31#include "radar/receiver.h"
32#include "radar/transmitter.h"
33#include "timing/timing.h"
34
35namespace core
36{
37 namespace
38 {
40
41 constexpr std::uint64_t max_uint64 = std::numeric_limits<std::uint64_t>::max(); ///< Saturating byte-count cap.
42
43 /**
44 * @brief Checks whether a receiver emits sample-by-sample streaming output.
45 * @param receiver The receiver to inspect.
46 * @return True for CW and FMCW receivers, false otherwise.
47 */
48 [[nodiscard]] bool isStreamingReceiver(const radar::Receiver& receiver) noexcept
49 {
50 return receiver.getMode() == OperationMode::CW_MODE || receiver.getMode() == OperationMode::FMCW_MODE;
51 }
52
53 /**
54 * @brief Converts a finite floating-point count to an integer by rounding up.
55 * @param value The floating-point count to convert.
56 * @param overflowed Set to true when the input cannot be represented as `uint64_t`.
57 * @return The rounded-up count, clamped to `uint64_t` max on overflow.
58 */
59 [[nodiscard]] std::uint64_t ceilToUint64(const RealType value, bool& overflowed) noexcept
60 {
61 if (!std::isfinite(value))
62 {
63 overflowed = true;
64 return max_uint64;
65 }
66 if (value <= 0.0)
67 {
68 return 0;
69 }
70 if (value >= static_cast<RealType>(max_uint64))
71 {
72 overflowed = true;
73 return max_uint64;
74 }
75 const RealType nearest = std::round(value);
76 const RealType tolerance = 1.0e-12 * std::max<RealType>(1.0, std::abs(nearest));
77 if (std::abs(value - nearest) <= tolerance)
78 {
79 return static_cast<std::uint64_t>(nearest);
80 }
81 return static_cast<std::uint64_t>(std::ceil(value));
82 }
83
84 /**
85 * @brief Adds two byte projections while preserving overflow state.
86 * @param lhs The left-hand byte projection.
87 * @param rhs The right-hand byte projection.
88 * @return The summed projection, clamped to `uint64_t` max on overflow.
89 */
91 {
93 result.overflowed = lhs.overflowed || rhs.overflowed;
94 if (max_uint64 - lhs.bytes < rhs.bytes)
95 {
96 result.bytes = max_uint64;
97 result.overflowed = true;
98 return result;
99 }
100 result.bytes = lhs.bytes + rhs.bytes;
101 return result;
102 }
103
104 /**
105 * @brief Multiplies two byte-count factors while preserving prior overflow state.
106 * @param lhs The left-hand factor.
107 * @param rhs The right-hand factor.
108 * @param input_overflowed True if an upstream calculation has already overflowed.
109 * @return The product projection, clamped to `uint64_t` max on overflow.
110 */
111 [[nodiscard]] ByteProjection multiplyBytes(const std::uint64_t lhs, const std::uint64_t rhs,
112 const bool input_overflowed = false) noexcept
113 {
115 result.overflowed = input_overflowed;
116 if (lhs != 0 && rhs > max_uint64 / lhs)
117 {
118 result.bytes = max_uint64;
119 result.overflowed = true;
120 return result;
121 }
122 result.bytes = lhs * rhs;
123 return result;
124 }
125
126 /**
127 * @brief Converts an oversampled sample count to the rendered output sample count.
128 * @param oversampled_samples The sample count at the internal simulation rate.
129 * @return The sample count after applying the configured oversample ratio.
130 */
131 [[nodiscard]] std::uint64_t downsampledSampleCount(const std::uint64_t oversampled_samples) noexcept
132 {
133 const unsigned ratio = params::oversampleRatio();
134 if (ratio <= 1)
135 {
136 return oversampled_samples;
137 }
138 return oversampled_samples / ratio;
139 }
140
141 /**
142 * @brief Counts samples required for a duration at a given sample rate.
143 * @param duration_seconds Duration of the interval in seconds.
144 * @param sample_rate_hz Sample rate used for the interval.
145 * @param overflowed Set to true if the count cannot be represented as `uint64_t`.
146 * @return The rounded-up sample count for the interval.
147 */
148 [[nodiscard]] std::uint64_t countSamplesForDuration(const RealType duration_seconds,
149 const RealType sample_rate_hz, bool& overflowed) noexcept
150 {
151 return ceilToUint64(duration_seconds * sample_rate_hz, overflowed);
152 }
153
154 /**
155 * @brief Counts evenly spaced start times within an inclusive time range.
156 * @param first_start First candidate start time.
157 * @param last_start Last allowed start time.
158 * @param step_seconds Spacing between successive starts.
159 * @param overflowed Set to true if the count cannot be represented as `uint64_t`.
160 * @return The number of starts in range, or zero for an invalid range.
161 */
163 const RealType step_seconds, bool& overflowed) noexcept
164 {
165 if (first_start > last_start || step_seconds <= 0.0 || !std::isfinite(step_seconds))
166 {
167 return 0;
168 }
169 return ceilToUint64(std::floor((last_start - first_start) / step_seconds) + 1.0, overflowed);
170 }
171
172 /**
173 * @brief Projects the number of receive windows emitted by a pulsed receiver.
174 * @param receiver The pulsed receiver to inspect.
175 * @param overflowed Set to true if any window count arithmetic overflows.
176 * @return The projected number of receive windows during the simulation.
177 */
178 [[nodiscard]] std::uint64_t countPulsedWindows(const radar::Receiver& receiver, bool& overflowed)
179 {
180 const RealType prf = receiver.getWindowPrf();
181 if (prf <= 0.0 || !std::isfinite(prf))
182 {
183 return 0;
184 }
185
187 const RealType step_seconds = 1.0 / prf;
188 const auto& schedule = receiver.getSchedule();
189 std::uint64_t total = 0;
190
191 if (schedule.empty())
192 {
193 const RealType first_start = receiver.getWindowStart(0);
194 if (first_start >= sim_end)
195 {
196 return 0;
197 }
198 const auto count = countStartsInRange(first_start, sim_end, step_seconds, overflowed);
199 return count;
200 }
201
202 RealType next_requested = receiver.getWindowStart(0);
203 bool counted_any_window = false;
204 for (const auto& period : schedule)
205 {
206 const RealType period_end = std::min(period.end, sim_end);
208 {
209 continue;
210 }
211
212 const RealType first_start = std::max(next_requested, period.start);
214 {
215 break;
216 }
217
218 const auto count = countStartsInRange(first_start, period_end, step_seconds, overflowed);
219 if (count == 0)
220 {
221 continue;
222 }
223
224 const auto added = addBytes({.bytes = total}, {.bytes = count});
225 total = added.bytes;
226 overflowed = overflowed || added.overflowed;
227 counted_any_window = true;
228 next_requested = first_start + static_cast<RealType>(count) * step_seconds;
229 }
230
231 return total;
232 }
233
234 /**
235 * @brief Reads the process resident set size from the current platform when supported.
236 * @return The current resident set size in bytes, or `std::nullopt` when unavailable.
237 */
238 [[nodiscard]] std::optional<std::uint64_t> currentResidentSetBytes() noexcept
239 {
240#if defined(__linux__)
241 long const page_size = sysconf(_SC_PAGESIZE);
242 if (page_size <= 0)
243 {
244 return std::nullopt;
245 }
246
247 std::ifstream statm("/proc/self/statm");
248 if (!statm)
249 {
250 return std::nullopt;
251 }
252
253 std::string ignored_total_pages;
254 unsigned long resident_pages = 0;
256 if (!statm)
257 {
258 return std::nullopt;
259 }
260
261 const auto pages = static_cast<std::uint64_t>(resident_pages);
262 const auto bytes = multiplyBytes(pages, static_cast<std::uint64_t>(page_size));
263 if (bytes.overflowed)
264 {
265 return max_uint64;
266 }
267 return bytes.bytes;
268#elif defined(__APPLE__) && defined(__MACH__)
271 if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&info), &count) !=
273 {
274 return std::nullopt;
275 }
276 return static_cast<std::uint64_t>(info.resident_size);
277#else
278 return std::nullopt;
279#endif
280 }
281
282 /**
283 * @brief Converts a byte projection to the JSON shape used by memory reports.
284 * @param projection The byte projection to serialize.
285 * @return A JSON object containing raw bytes, formatted bytes, and overflow state.
286 */
288 {
289 return {{"bytes", projection.bytes},
290 {"human", formatByteSize(projection.bytes)},
291 {"overflowed", projection.overflowed}};
292 }
293
294 /**
295 * @brief Converts an optional byte projection to JSON, preserving unknown values.
296 * @param projection The optional byte projection to serialize.
297 * @return A JSON object with nullable bytes when the projection is unavailable.
298 */
299 [[nodiscard]] nlohmann::json optionalByteProjectionToJson(const std::optional<ByteProjection>& projection)
300 {
301 if (!projection.has_value())
302 {
303 return {{"bytes", nullptr}, {"human", "unknown"}, {"overflowed", false}};
304 }
306 }
307 }
308
309 std::vector<std::shared_ptr<timing::Timing>> collectCwPhaseNoiseTimings(const World& world)
310 {
311 std::unordered_map<SimId, std::shared_ptr<timing::Timing>> unique_timings;
312
313 for (const auto& transmitter_ptr : world.getTransmitters())
314 {
315 if (!transmitter_ptr->isStreamingMode())
316 {
317 continue;
318 }
319 unique_timings.try_emplace(transmitter_ptr->getTiming()->getId(), transmitter_ptr->getTiming());
320 }
321
322 for (const auto& receiver_ptr : world.getReceivers())
323 {
325 {
326 continue;
327 }
328 unique_timings.try_emplace(receiver_ptr->getTiming()->getId(), receiver_ptr->getTiming());
329 }
330
331 std::vector<std::shared_ptr<timing::Timing>> timings;
332 timings.reserve(unique_timings.size());
333 for (const auto& entry : unique_timings)
334 {
335 timings.push_back(entry.second);
336 }
337 return timings;
338 }
339
341 const bool sample_count_overflowed)
342 {
343 ++projection.streaming_receiver_count;
344 bool if_count_overflowed = false;
345 const auto if_sample_rate = receiver.getIfSampleRate();
346 const bool if_rate_dechirped = receiver.isDechirpEnabled() && if_sample_rate.has_value();
348 ? countSamplesForDuration(projection.duration_seconds, if_sample_rate.value_or(0.0), if_count_overflowed)
349 : std::uint64_t{0};
352 : (receiver.isDechirpEnabled() ? projection.streaming_sample_count
353 : downsampledSampleCount(projection.streaming_sample_count));
354 projection.rendered_hdf5_sample_count =
355 addBytes({.bytes = projection.rendered_hdf5_sample_count},
357 .bytes;
358 const auto resident_samples = if_rate_dechirped ? if_sample_count : projection.streaming_sample_count;
359 projection.streaming_iq_buffers =
360 addBytes(projection.streaming_iq_buffers,
361 multiplyBytes(resident_samples, static_cast<std::uint64_t>(sizeof(ComplexType)),
363 }
364
366 {
367 ++projection.pulsed_receiver_count;
368 bool window_count_overflowed = false;
370 projection.pulsed_window_count = addBytes({.bytes = projection.pulsed_window_count},
371 {.bytes = windows, .overflowed = window_count_overflowed})
372 .bytes;
373
376 receiver.getWindowLength(), projection.simulation_sample_rate_hz, pulsed_sample_count_overflowed);
378 const auto rendered_samples =
380 projection.rendered_hdf5_sample_count =
381 addBytes({.bytes = projection.rendered_hdf5_sample_count}, rendered_samples).bytes;
382 }
383
385 {
387 projection.duration_seconds = std::max<RealType>(0.0, params::endTime() - params::startTime());
388 projection.oversample_ratio = params::oversampleRatio();
389 projection.simulation_sample_rate_hz = params::rate() * static_cast<RealType>(projection.oversample_ratio);
390
391 bool sample_count_overflowed = false;
392 projection.streaming_sample_count = countSamplesForDuration(
393 projection.duration_seconds, projection.simulation_sample_rate_hz, sample_count_overflowed);
394
396 bool phase_noise_count_overflowed = false;
397 projection.phase_noise_sample_count =
399 projection.simulation_sample_rate_hz, phase_noise_count_overflowed);
400 if (projection.phase_noise_sample_count != max_uint64)
401 {
402 ++projection.phase_noise_sample_count;
403 }
405 {
406 projection.phase_noise_sample_count = max_uint64;
407 }
408
409 const auto timings = collectCwPhaseNoiseTimings(world);
410 projection.phase_noise_timing_count = static_cast<std::uint64_t>(timings.size());
411 for (const auto& timing : timings)
412 {
413 if (timing && timing->isEnabled())
414 {
415 ++projection.enabled_phase_noise_timing_count;
416 }
417 }
418
419 projection.phase_noise_lookup =
420 multiplyBytes(projection.phase_noise_sample_count,
421 projection.enabled_phase_noise_timing_count * static_cast<std::uint64_t>(sizeof(RealType)),
423
424 for (const auto& receiver_ptr : world.getReceivers())
425 {
426 const auto& receiver = *receiver_ptr;
428 {
430 continue;
431 }
432
433 if (receiver.getMode() == OperationMode::PULSED_MODE)
434 {
436 }
437 }
438
439 projection.rendered_hdf5_payload =
440 multiplyBytes(projection.rendered_hdf5_sample_count, 2ULL * static_cast<std::uint64_t>(sizeof(RealType)));
441
442 projection.current_resident_set = currentResidentSetBytes();
443 if (projection.current_resident_set.has_value())
444 {
445 projection.resident_baseline = ByteProjection{.bytes = *projection.current_resident_set};
446 projection.projected_total_footprint =
447 addBytes(addBytes(addBytes(projection.phase_noise_lookup, projection.streaming_iq_buffers),
448 projection.rendered_hdf5_payload),
449 *projection.resident_baseline);
450 }
451
452 return projection;
453 }
454
455 std::string formatByteSize(const std::uint64_t bytes)
456 {
457 constexpr std::array units = {"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"};
458 auto value = static_cast<long double>(bytes);
459 std::size_t unit_index = 0;
460 while (value >= 1024.0L && unit_index + 1 < units.size())
461 {
462 value /= 1024.0L;
463 ++unit_index;
464 }
465 if (unit_index == 0)
466 {
467 return std::format("{} {}", bytes, units.at(unit_index));
468 }
469 return std::format("{:.2f} {}", static_cast<double>(value), units.at(unit_index));
470 }
471
473 {
474 const nlohmann::json result = {
475 {"duration_seconds", projection.duration_seconds},
476 {"simulation_sample_rate_hz", projection.simulation_sample_rate_hz},
477 {"oversample_ratio", projection.oversample_ratio},
478 {"sample_counts",
479 {{"streaming_samples", projection.streaming_sample_count},
480 {"phase_noise_samples_per_enabled_timing", projection.phase_noise_sample_count},
481 {"rendered_hdf5_samples", projection.rendered_hdf5_sample_count},
482 {"pulsed_windows", projection.pulsed_window_count}}},
483 {"object_counts",
484 {{"phase_noise_timing_sources", projection.phase_noise_timing_count},
485 {"enabled_phase_noise_timing_sources", projection.enabled_phase_noise_timing_count},
486 {"streaming_receivers", projection.streaming_receiver_count},
487 {"pulsed_receivers", projection.pulsed_receiver_count}}},
488 {"phase_noise_lookups", byteProjectionToJson(projection.phase_noise_lookup)},
489 {"streaming_iq_buffers", byteProjectionToJson(projection.streaming_iq_buffers)},
490 {"rendered_hdf5_dataset_payload", byteProjectionToJson(projection.rendered_hdf5_payload)},
491 {"current_resident_set",
492 projection.current_resident_set.has_value()
493 ? nlohmann::json{{"bytes", *projection.current_resident_set},
494 {"human", formatByteSize(*projection.current_resident_set)}}
495 : nlohmann::json{{"bytes", nullptr}, {"human", "unknown"}}},
496 {"resident_baseline", optionalByteProjectionToJson(projection.resident_baseline)},
497 {"projected_total_footprint", optionalByteProjectionToJson(projection.projected_total_footprint)}};
498 return result.dump(2);
499 }
500
502 {
503 const auto projection = projectSimulationMemory(world);
504 const std::string resident_baseline =
505 projection.resident_baseline.has_value() ? formatByteSize(projection.resident_baseline->bytes) : "unknown";
506 const std::string total = projection.projected_total_footprint.has_value()
507 ? formatByteSize(projection.projected_total_footprint->bytes)
508 : "unknown";
509
511 "Projected simulation footprint: phase_noise_lookup_memory={} ({} enabled timing sources x {} samples), "
512 "streaming_output_buffer_memory={} ({} streaming receivers, IF-rate receivers use IF sample counts), "
513 "rendered_hdf5_dataset_payload={} "
514 "({} output samples), resident_baseline={} (current RSS before projected run allocations), "
515 "projected_total_footprint={}.",
516 formatByteSize(projection.phase_noise_lookup.bytes), projection.enabled_phase_noise_timing_count,
517 projection.phase_noise_sample_count, formatByteSize(projection.streaming_iq_buffers.bytes),
518 projection.streaming_receiver_count, formatByteSize(projection.rendered_hdf5_payload.bytes),
519 projection.rendered_hdf5_sample_count, resident_baseline, total);
520
521 constexpr std::uint64_t one_gib = 1024ULL * 1024ULL * 1024ULL;
522 for (const auto& receiver_ptr : world.getReceivers())
523 {
524 const auto& receiver = *receiver_ptr;
525 if (!receiver.isDechirpEnabled())
526 {
527 continue;
528 }
529 if (receiver.hasFmcwIfSampleRate())
530 {
531 continue;
532 }
533 const auto projected_payload =
534 multiplyBytes(projection.streaming_sample_count, 2ULL * static_cast<std::uint64_t>(sizeof(RealType)));
535 if (projected_payload.bytes > one_gib || projected_payload.overflowed)
536 {
537 const auto gib = static_cast<long double>(projected_payload.bytes) / static_cast<long double>(one_gib);
538 // TODO: UI workflows should have disk+memory usage stats on the SimulationView page (shows before user
539 // runs the sim)
541 "Receiver {} is outputting RF-rate IF data. Projected file size is {:.2f} GiB. This is expected "
542 "for V1 native dechirping, but ensure you have sufficient disk space. Future versions will support "
543 "IF-rate decimation.",
544 receiver.getName(), static_cast<double>(gib));
545 }
546 }
547 }
548}
const Receiver & receiver
The World class manages the simulator environment.
Definition world.h:39
const std::vector< std::unique_ptr< radar::Transmitter > > & getTransmitters() const noexcept
Retrieves the list of radar transmitters.
Definition world.h:246
const std::vector< std::unique_ptr< radar::Receiver > > & getReceivers() const noexcept
Retrieves the list of radar receivers.
Definition world.h:236
RealType earliestPhaseNoiseLookupStart() const
Finds the earliest simulation time that can require CW phase-noise samples.
Definition world.cpp:209
Manages radar signal reception and response processing.
Definition receiver.h:47
double RealType
Type for real numbers.
Definition config.h:27
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
Startup memory and output-size projection helpers for simulations.
void logSimulationMemoryProjection(const World &world)
Logs the projected simulation memory footprint for the provided world.
void addPulsedReceiverProjection(SimulationMemoryProjection &projection, const radar::Receiver &receiver)
std::string memoryProjectionToJsonString(const SimulationMemoryProjection &projection)
Serializes a simulation memory projection as JSON.
void addStreamingReceiverProjection(SimulationMemoryProjection &projection, const radar::Receiver &receiver, const bool sample_count_overflowed)
std::vector< std::shared_ptr< timing::Timing > > collectCwPhaseNoiseTimings(const World &world)
Collects unique timing sources used by CW/FMCW transmitters and receivers.
SimulationMemoryProjection projectSimulationMemory(const World &world)
Projects startup memory and rendered-output sizes for a simulation world.
std::string formatByteSize(const std::uint64_t bytes)
Formats a byte count using binary units.
@ WARNING
Warning level for potentially harmful situations.
@ DEBUG
Debug level for general debugging information.
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:109
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:121
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:103
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
OperationMode
Defines the operational mode of a radar component.
Definition radar_obj.h:39
Defines the Parameters struct and provides methods for managing simulation parameters.
Radar Receiver class for managing signal reception and response handling.
math::Vec3 max
Describes a projected byte count and whether it saturated during arithmetic.
std::uint64_t bytes
Projected byte count, clamped to uint64_t max on overflow.
Captures startup memory and rendered-output projections for a simulation.
Timing source for simulation objects.
Header file for the Transmitter class in the radar namespace.
Header file for the World class in the simulator.