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 receiver.getMode() == OperationMode::SFCW_MODE;
52 }
53
54 /**
55 * @brief Converts a finite floating-point count to an integer by rounding up.
56 * @param value The floating-point count to convert.
57 * @param overflowed Set to true when the input cannot be represented as `uint64_t`.
58 * @return The rounded-up count, clamped to `uint64_t` max on overflow.
59 */
60 [[nodiscard]] std::uint64_t ceilToUint64(const RealType value, bool& overflowed) noexcept
61 {
62 if (!std::isfinite(value))
63 {
64 overflowed = true;
65 return max_uint64;
66 }
67 if (value <= 0.0)
68 {
69 return 0;
70 }
71 if (value >= static_cast<RealType>(max_uint64))
72 {
73 overflowed = true;
74 return max_uint64;
75 }
76 const RealType nearest = std::round(value);
77 const RealType tolerance = 1.0e-12 * std::max<RealType>(1.0, std::abs(nearest));
78 if (std::abs(value - nearest) <= tolerance)
79 {
80 return static_cast<std::uint64_t>(nearest);
81 }
82 return static_cast<std::uint64_t>(std::ceil(value));
83 }
84
85 /**
86 * @brief Adds two byte projections while preserving overflow state.
87 * @param lhs The left-hand byte projection.
88 * @param rhs The right-hand byte projection.
89 * @return The summed projection, clamped to `uint64_t` max on overflow.
90 */
92 {
94 result.overflowed = lhs.overflowed || rhs.overflowed;
95 if (max_uint64 - lhs.bytes < rhs.bytes)
96 {
97 result.bytes = max_uint64;
98 result.overflowed = true;
99 return result;
100 }
101 result.bytes = lhs.bytes + rhs.bytes;
102 return result;
103 }
104
105 /**
106 * @brief Multiplies two byte-count factors while preserving prior overflow state.
107 * @param lhs The left-hand factor.
108 * @param rhs The right-hand factor.
109 * @param input_overflowed True if an upstream calculation has already overflowed.
110 * @return The product projection, clamped to `uint64_t` max on overflow.
111 */
112 [[nodiscard]] ByteProjection multiplyBytes(const std::uint64_t lhs, const std::uint64_t rhs,
113 const bool input_overflowed = false) noexcept
114 {
116 result.overflowed = input_overflowed;
117 if (lhs != 0 && rhs > max_uint64 / lhs)
118 {
119 result.bytes = max_uint64;
120 result.overflowed = true;
121 return result;
122 }
123 result.bytes = lhs * rhs;
124 return result;
125 }
126
127 /**
128 * @brief Converts an oversampled sample count to the rendered output sample count.
129 * @param oversampled_samples The sample count at the internal simulation rate.
130 * @return The sample count after applying the configured oversample ratio.
131 */
132 [[nodiscard]] std::uint64_t downsampledSampleCount(const std::uint64_t oversampled_samples) noexcept
133 {
134 const unsigned ratio = params::oversampleRatio();
135 if (ratio <= 1)
136 {
137 return oversampled_samples;
138 }
139 return oversampled_samples / ratio;
140 }
141
142 /**
143 * @brief Counts samples required for a duration at a given sample rate.
144 * @param duration_seconds Duration of the interval in seconds.
145 * @param sample_rate_hz Sample rate used for the interval.
146 * @param overflowed Set to true if the count cannot be represented as `uint64_t`.
147 * @return The rounded-up sample count for the interval.
148 */
149 [[nodiscard]] std::uint64_t countSamplesForDuration(const RealType duration_seconds,
150 const RealType sample_rate_hz, bool& overflowed) noexcept
151 {
152 return ceilToUint64(duration_seconds * sample_rate_hz, overflowed);
153 }
154
155 /**
156 * @brief Counts evenly spaced start times within an inclusive time range.
157 * @param first_start First candidate start time.
158 * @param last_start Last allowed start time.
159 * @param step_seconds Spacing between successive starts.
160 * @param overflowed Set to true if the count cannot be represented as `uint64_t`.
161 * @return The number of starts in range, or zero for an invalid range.
162 */
164 const RealType step_seconds, bool& overflowed) noexcept
165 {
166 if (first_start > last_start || step_seconds <= 0.0 || !std::isfinite(step_seconds))
167 {
168 return 0;
169 }
170 return ceilToUint64(std::floor((last_start - first_start) / step_seconds) + 1.0, overflowed);
171 }
172
173 /**
174 * @brief Projects the number of receive windows emitted by a pulsed receiver.
175 * @param receiver The pulsed receiver to inspect.
176 * @param overflowed Set to true if any window count arithmetic overflows.
177 * @return The projected number of receive windows during the simulation.
178 */
179 [[nodiscard]] std::uint64_t countPulsedWindows(const radar::Receiver& receiver, bool& overflowed)
180 {
181 const RealType prf = receiver.getWindowPrf();
182 if (prf <= 0.0 || !std::isfinite(prf))
183 {
184 return 0;
185 }
186
188 const RealType step_seconds = 1.0 / prf;
189 const auto& schedule = receiver.getSchedule();
190 std::uint64_t total = 0;
191
192 if (schedule.empty())
193 {
194 const RealType first_start = receiver.getWindowStart(0);
195 if (first_start >= sim_end)
196 {
197 return 0;
198 }
199 const auto count = countStartsInRange(first_start, sim_end, step_seconds, overflowed);
200 return count;
201 }
202
203 RealType next_requested = receiver.getWindowStart(0);
204 bool counted_any_window = false;
205 for (const auto& period : schedule)
206 {
207 const RealType period_end = std::min(period.end, sim_end);
209 {
210 continue;
211 }
212
213 const RealType first_start = std::max(next_requested, period.start);
215 {
216 break;
217 }
218
219 const auto count = countStartsInRange(first_start, period_end, step_seconds, overflowed);
220 if (count == 0)
221 {
222 continue;
223 }
224
225 const auto added = addBytes({.bytes = total}, {.bytes = count});
226 total = added.bytes;
227 overflowed = overflowed || added.overflowed;
228 counted_any_window = true;
229 next_requested = first_start + static_cast<RealType>(count) * step_seconds;
230 }
231
232 return total;
233 }
234
235 /**
236 * @brief Reads the process resident set size from the current platform when supported.
237 * @return The current resident set size in bytes, or `std::nullopt` when unavailable.
238 */
239 [[nodiscard]] std::optional<std::uint64_t> currentResidentSetBytes() noexcept
240 {
241#if defined(__linux__)
242 long const page_size = sysconf(_SC_PAGESIZE);
243 if (page_size <= 0)
244 {
245 return std::nullopt;
246 }
247
248 std::ifstream statm("/proc/self/statm");
249 if (!statm)
250 {
251 return std::nullopt;
252 }
253
254 std::string ignored_total_pages;
255 unsigned long resident_pages = 0;
257 if (!statm)
258 {
259 return std::nullopt;
260 }
261
262 const auto pages = static_cast<std::uint64_t>(resident_pages);
263 const auto bytes = multiplyBytes(pages, static_cast<std::uint64_t>(page_size));
264 if (bytes.overflowed)
265 {
266 return max_uint64;
267 }
268 return bytes.bytes;
269#elif defined(__APPLE__) && defined(__MACH__)
272 if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&info), &count) !=
274 {
275 return std::nullopt;
276 }
277 return static_cast<std::uint64_t>(info.resident_size);
278#else
279 return std::nullopt;
280#endif
281 }
282
283 /**
284 * @brief Converts a byte projection to the JSON shape used by memory reports.
285 * @param projection The byte projection to serialize.
286 * @return A JSON object containing raw bytes, formatted bytes, and overflow state.
287 */
289 {
290 return {{"bytes", projection.bytes},
291 {"human", formatByteSize(projection.bytes)},
292 {"overflowed", projection.overflowed}};
293 }
294
295 /**
296 * @brief Converts an optional byte projection to JSON, preserving unknown values.
297 * @param projection The optional byte projection to serialize.
298 * @return A JSON object with nullable bytes when the projection is unavailable.
299 */
300 [[nodiscard]] nlohmann::json optionalByteProjectionToJson(const std::optional<ByteProjection>& projection)
301 {
302 if (!projection.has_value())
303 {
304 return {{"bytes", nullptr}, {"human", "unknown"}, {"overflowed", false}};
305 }
307 }
308 }
309
310 std::vector<std::shared_ptr<timing::Timing>> collectCwPhaseNoiseTimings(const World& world)
311 {
312 std::unordered_map<SimId, std::shared_ptr<timing::Timing>> unique_timings;
313
314 for (const auto& transmitter_ptr : world.getTransmitters())
315 {
316 if (!transmitter_ptr->isStreamingMode())
317 {
318 continue;
319 }
320 unique_timings.try_emplace(transmitter_ptr->getTiming()->getId(), transmitter_ptr->getTiming());
321 }
322
323 for (const auto& receiver_ptr : world.getReceivers())
324 {
326 {
327 continue;
328 }
329 unique_timings.try_emplace(receiver_ptr->getTiming()->getId(), receiver_ptr->getTiming());
330 }
331
332 std::vector<std::shared_ptr<timing::Timing>> timings;
333 timings.reserve(unique_timings.size());
334 for (const auto& entry : unique_timings)
335 {
336 timings.push_back(entry.second);
337 }
338 return timings;
339 }
340
342 const bool sample_count_overflowed)
343 {
344 ++projection.streaming_receiver_count;
345 bool if_count_overflowed = false;
346 const auto if_sample_rate = receiver.getIfSampleRate();
347 const bool if_rate_dechirped = receiver.isDechirpEnabled() && if_sample_rate.has_value();
349 ? countSamplesForDuration(projection.duration_seconds, if_sample_rate.value_or(0.0), if_count_overflowed)
350 : std::uint64_t{0};
353 : (receiver.isDechirpEnabled() ? projection.streaming_sample_count
354 : downsampledSampleCount(projection.streaming_sample_count));
355 projection.rendered_hdf5_sample_count =
356 addBytes({.bytes = projection.rendered_hdf5_sample_count},
358 .bytes;
359 const auto resident_samples = if_rate_dechirped ? if_sample_count : projection.streaming_sample_count;
360 projection.streaming_iq_buffers =
361 addBytes(projection.streaming_iq_buffers,
362 multiplyBytes(resident_samples, static_cast<std::uint64_t>(sizeof(ComplexType)),
364 }
365
367 {
368 ++projection.pulsed_receiver_count;
369 bool window_count_overflowed = false;
371 projection.pulsed_window_count = addBytes({.bytes = projection.pulsed_window_count},
372 {.bytes = windows, .overflowed = window_count_overflowed})
373 .bytes;
374
377 receiver.getWindowLength(), projection.simulation_sample_rate_hz, pulsed_sample_count_overflowed);
379 const auto rendered_samples =
381 projection.rendered_hdf5_sample_count =
382 addBytes({.bytes = projection.rendered_hdf5_sample_count}, rendered_samples).bytes;
383 }
384
386 {
388 projection.duration_seconds = std::max<RealType>(0.0, params::endTime() - params::startTime());
389 projection.oversample_ratio = params::oversampleRatio();
390 projection.simulation_sample_rate_hz = params::rate() * static_cast<RealType>(projection.oversample_ratio);
391
392 bool sample_count_overflowed = false;
393 projection.streaming_sample_count = countSamplesForDuration(
394 projection.duration_seconds, projection.simulation_sample_rate_hz, sample_count_overflowed);
395
397 bool phase_noise_count_overflowed = false;
398 projection.phase_noise_sample_count =
400 projection.simulation_sample_rate_hz, phase_noise_count_overflowed);
401 if (projection.phase_noise_sample_count != max_uint64)
402 {
403 ++projection.phase_noise_sample_count;
404 }
406 {
407 projection.phase_noise_sample_count = max_uint64;
408 }
409
410 const auto timings = collectCwPhaseNoiseTimings(world);
411 projection.phase_noise_timing_count = static_cast<std::uint64_t>(timings.size());
412 for (const auto& timing : timings)
413 {
414 if (timing && timing->isEnabled())
415 {
416 ++projection.enabled_phase_noise_timing_count;
417 }
418 }
419
420 projection.phase_noise_lookup =
421 multiplyBytes(projection.phase_noise_sample_count,
422 projection.enabled_phase_noise_timing_count * static_cast<std::uint64_t>(sizeof(RealType)),
424
425 for (const auto& receiver_ptr : world.getReceivers())
426 {
427 const auto& receiver = *receiver_ptr;
429 {
431 continue;
432 }
433
434 if (receiver.getMode() == OperationMode::PULSED_MODE)
435 {
437 }
438 }
439
440 projection.rendered_hdf5_payload =
441 multiplyBytes(projection.rendered_hdf5_sample_count, 2ULL * static_cast<std::uint64_t>(sizeof(RealType)));
442
443 projection.current_resident_set = currentResidentSetBytes();
444 if (projection.current_resident_set.has_value())
445 {
446 projection.resident_baseline = ByteProjection{.bytes = *projection.current_resident_set};
447 projection.projected_total_footprint =
448 addBytes(addBytes(addBytes(projection.phase_noise_lookup, projection.streaming_iq_buffers),
449 projection.rendered_hdf5_payload),
450 *projection.resident_baseline);
451 }
452
453 return projection;
454 }
455
456 std::string formatByteSize(const std::uint64_t bytes)
457 {
458 constexpr std::array units = {"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"};
459 auto value = static_cast<long double>(bytes);
460 std::size_t unit_index = 0;
461 while (value >= 1024.0L && unit_index + 1 < units.size())
462 {
463 value /= 1024.0L;
464 ++unit_index;
465 }
466 if (unit_index == 0)
467 {
468 return std::format("{} {}", bytes, units.at(unit_index));
469 }
470 return std::format("{:.2f} {}", static_cast<double>(value), units.at(unit_index));
471 }
472
474 {
475 const nlohmann::json result = {
476 {"duration_seconds", projection.duration_seconds},
477 {"simulation_sample_rate_hz", projection.simulation_sample_rate_hz},
478 {"oversample_ratio", projection.oversample_ratio},
479 {"sample_counts",
480 {{"streaming_samples", projection.streaming_sample_count},
481 {"phase_noise_samples_per_enabled_timing", projection.phase_noise_sample_count},
482 {"rendered_hdf5_samples", projection.rendered_hdf5_sample_count},
483 {"pulsed_windows", projection.pulsed_window_count}}},
484 {"object_counts",
485 {{"phase_noise_timing_sources", projection.phase_noise_timing_count},
486 {"enabled_phase_noise_timing_sources", projection.enabled_phase_noise_timing_count},
487 {"streaming_receivers", projection.streaming_receiver_count},
488 {"pulsed_receivers", projection.pulsed_receiver_count}}},
489 {"phase_noise_lookups", byteProjectionToJson(projection.phase_noise_lookup)},
490 {"streaming_iq_buffers", byteProjectionToJson(projection.streaming_iq_buffers)},
491 {"rendered_hdf5_dataset_payload", byteProjectionToJson(projection.rendered_hdf5_payload)},
492 {"current_resident_set",
493 projection.current_resident_set.has_value()
494 ? nlohmann::json{{"bytes", *projection.current_resident_set},
495 {"human", formatByteSize(*projection.current_resident_set)}}
496 : nlohmann::json{{"bytes", nullptr}, {"human", "unknown"}}},
497 {"resident_baseline", optionalByteProjectionToJson(projection.resident_baseline)},
498 {"projected_total_footprint", optionalByteProjectionToJson(projection.projected_total_footprint)}};
499 return result.dump(2);
500 }
501
503 {
504 const auto projection = projectSimulationMemory(world);
505 const std::string resident_baseline =
506 projection.resident_baseline.has_value() ? formatByteSize(projection.resident_baseline->bytes) : "unknown";
507 const std::string total = projection.projected_total_footprint.has_value()
508 ? formatByteSize(projection.projected_total_footprint->bytes)
509 : "unknown";
510
512 "Projected simulation footprint: phase_noise_lookup_memory={} ({} enabled timing sources x {} samples), "
513 "streaming_output_buffer_memory={} ({} streaming receivers, IF-rate receivers use IF sample counts), "
514 "rendered_hdf5_dataset_payload={} "
515 "({} output samples), resident_baseline={} (current RSS before projected run allocations), "
516 "projected_total_footprint={}.",
517 formatByteSize(projection.phase_noise_lookup.bytes), projection.enabled_phase_noise_timing_count,
518 projection.phase_noise_sample_count, formatByteSize(projection.streaming_iq_buffers.bytes),
519 projection.streaming_receiver_count, formatByteSize(projection.rendered_hdf5_payload.bytes),
520 projection.rendered_hdf5_sample_count, resident_baseline, total);
521
522 constexpr std::uint64_t one_gib = 1024ULL * 1024ULL * 1024ULL;
523 for (const auto& receiver_ptr : world.getReceivers())
524 {
525 const auto& receiver = *receiver_ptr;
526 if (!receiver.isDechirpEnabled())
527 {
528 continue;
529 }
530 if (receiver.hasFmcwIfSampleRate())
531 {
532 continue;
533 }
534 const auto projected_payload =
535 multiplyBytes(projection.streaming_sample_count, 2ULL * static_cast<std::uint64_t>(sizeof(RealType)));
536 if (projected_payload.bytes > one_gib || projected_payload.overflowed)
537 {
538 const auto gib = static_cast<long double>(projected_payload.bytes) / static_cast<long double>(one_gib);
539 // TODO: UI workflows should have disk+memory usage stats on the SimulationView page (shows before user
540 // runs the sim)
542 "Receiver {} is outputting RF-rate IF data. Projected file size is {:.2f} GiB. This is expected "
543 "for V1 native dechirping, but ensure you have sufficient disk space. Future versions will support "
544 "IF-rate decimation.",
545 receiver.getName(), static_cast<double>(gib));
546 }
547 }
548 }
549}
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.