FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
fers_signal Namespace Reference

Classes

class  CwSignal
 Continuous-wave signal implementation. More...
 
class  DecadeUpsampler
 Implements a specialized upsampler with a fixed upsampling factor of 10. More...
 
class  DownsamplingSink
 Stateful FIR decimator for chunked streaming output. More...
 
class  DspFilter
 Abstract base class for digital filters. More...
 
class  FirFilter
 Implements a Finite Impulse Response (FIR) filter. More...
 
class  FmcwChirpSignal
 FMCW linear chirp signal implementation. More...
 
struct  FmcwIfRateRatio
 
struct  FmcwIfResamplerLimits
 
struct  FmcwIfResamplerPlan
 
struct  FmcwIfResamplerRequest
 
struct  FmcwIfResamplerStagePlan
 
class  FmcwIfResamplingSink
 
struct  FmcwIfZeroInputResult
 
class  FmcwTriangleSignal
 FMCW symmetric triangular modulation signal implementation. More...
 
class  IirFilter
 Implements an Infinite Impulse Response (IIR) filter. More...
 
class  RadarSignal
 Class representing a radar signal with associated properties. More...
 
class  Signal
 Class for handling radar waveform signal data. More...
 

Enumerations

enum class  FmcwIfResamplerStageKind : std::uint8_t { HalfBandDecimateBy2 , RationalPolyphase }
 
enum class  FmcwChirpDirection : std::uint8_t { Up , Down }
 Sweep direction for a linear FMCW chirp. More...
 

Functions

void to_json (nlohmann::json &j, const RadarSignal &rs)
 
void from_json (const nlohmann::json &j, std::unique_ptr< RadarSignal > &rs)
 
void upsample (const std::span< const ComplexType > in, const unsigned size, std::span< ComplexType > out)
 Upsamples a complex waveform with zero-stuffing followed by Blackman FIR filtering.
 
std::vector< ComplexTypedownsample (std::span< const ComplexType > in)
 Low-pass filters and decimates an oversampled complex waveform back to base rate.
 
FmcwIfRateRatio approximateFmcwIfRateRatio (const RealType output_sample_rate_hz, const RealType input_sample_rate_hz, const FmcwIfResamplerLimits &limits)
 
FmcwIfResamplerPlan planFmcwIfResampler (const FmcwIfResamplerRequest &request)
 
std::string_view fmcwChirpDirectionToken (FmcwChirpDirection direction) noexcept
 Converts a chirp direction to the schema token.
 
FmcwChirpDirection parseFmcwChirpDirection (std::string_view direction)
 Parses a schema chirp direction token.
 

Enumeration Type Documentation

◆ FmcwChirpDirection

Sweep direction for a linear FMCW chirp.

Enumerator
Up 

Instantaneous baseband frequency increases over the chirp.

Down 

Instantaneous baseband frequency decreases over the chirp.

Definition at line 36 of file radar_signal.h.

37 {
38 Up, ///< Instantaneous baseband frequency increases over the chirp.
39 Down ///< Instantaneous baseband frequency decreases over the chirp.
40 };
@ Down
Instantaneous baseband frequency decreases over the chirp.
@ Up
Instantaneous baseband frequency increases over the chirp.

◆ FmcwIfResamplerStageKind

Enumerator
HalfBandDecimateBy2 
RationalPolyphase 

Definition at line 25 of file if_resampler.h.

Function Documentation

◆ approximateFmcwIfRateRatio()

FmcwIfRateRatio fers_signal::approximateFmcwIfRateRatio ( const RealType  output_sample_rate_hz,
const RealType  input_sample_rate_hz,
const FmcwIfResamplerLimits limits 
)

Definition at line 295 of file if_resampler.cpp.

297 {
298 const RealType target = checkedRatio(output_sample_rate_hz, input_sample_rate_hz);
299 if (std::abs(target - 1.0) <= limits.ratio_relative_tolerance)
300 {
301 return {.numerator = 1,
302 .denominator = 1,
303 .requested_ratio = target,
304 .actual_ratio = 1.0,
305 .relative_error = std::abs(target - 1.0)};
306 }
307
308 std::uint64_t prev_num = 0;
309 std::uint64_t num = 1;
310 std::uint64_t prev_den = 1;
311 std::uint64_t den = 0;
312 RealType x = target;
313 FmcwIfRateRatio best{.requested_ratio = target};
314
315 for (std::size_t iter = 0; iter < 128; ++iter)
316 {
317 const auto a = static_cast<std::uint64_t>(std::floor(x));
318 const auto next_num = a * num + prev_num;
319 const auto next_den = a * den + prev_den;
320 if (next_den == 0 || next_den > limits.max_ratio_denominator)
321 {
322 break;
323 }
324
325 const RealType actual = static_cast<RealType>(next_num) / static_cast<RealType>(next_den);
326 const RealType relative_error = std::abs(actual - target) / target;
327 best = {.numerator = next_num,
328 .denominator = next_den,
329 .requested_ratio = target,
330 .actual_ratio = actual,
331 .relative_error = relative_error};
332 if (relative_error <= limits.ratio_relative_tolerance)
333 {
334 return best;
335 }
336
337 const RealType remainder = x - static_cast<RealType>(a);
338 if (std::abs(remainder) <= std::numeric_limits<RealType>::epsilon())
339 {
340 break;
341 }
342 x = 1.0 / remainder;
343 prev_num = num;
344 num = next_num;
345 prev_den = den;
346 den = next_den;
347 }
348
349 throw std::runtime_error("IF resampler cannot approximate output/input sample-rate ratio within tolerance; "
350 "increase max denominator or choose a representable IF sample rate");
351 }
double RealType
Type for real numbers.
Definition config.h:27
math::Vec3 max
RealType a

References a, max, fers_signal::FmcwIfResamplerLimits::max_ratio_denominator, and fers_signal::FmcwIfResamplerLimits::ratio_relative_tolerance.

Referenced by planFmcwIfResampler().

+ Here is the caller graph for this function:

◆ downsample()

std::vector< ComplexType > fers_signal::downsample ( std::span< const ComplexType in)

Low-pass filters and decimates an oversampled complex waveform back to base rate.

Downsamples a signal by a given ratio.

The same fixed-length FIR design is reused for anti-alias filtering, and unsupported ratios fail fast before filtering begins.

Parameters
inInput span of oversampled complex samples.
Returns
Base-rate complex samples truncated to floor(in.size() / oversampleRatio()).
Exceptions
std::invalid_argumentif in is empty.
std::runtime_errorif the configured oversampling ratio is unsupported.
Parameters
inInput span of complex samples.
Exceptions
std::invalid_argumentif the input or output spans are empty or the ratio is zero.

Definition at line 183 of file dsp_filters.cpp.

184 {
185 if (in.empty())
186 {
187 throw std::invalid_argument("Input span is empty in Downsample");
188 }
189
190 const unsigned ratio = params::oversampleRatio();
191 // TODO: Replace with a more efficient multirate downsampling implementation.
193 const unsigned filter_length = params::renderFilterLength();
194 const auto design = blackmanFir(1 / static_cast<RealType>(ratio), ratio, filter_length);
195 const auto filt_length = design.coeffs.size();
196
197 std::vector tmp(in.size() + filt_length, ComplexType{0, 0});
198
199 std::ranges::copy(in, tmp.begin());
200
201 const FirFilter filt(design.coeffs);
202 filt.filter(tmp);
203
204 const auto downsampled_size = in.size() / ratio;
205 std::vector<ComplexType> out(downsampled_size);
206 const auto filter_delay = filt_length / 2;
207 for (std::size_t i = 0; i < downsampled_size; ++i)
208 {
209 const auto source_index = i * static_cast<std::size_t>(ratio) + filter_delay;
210 out[i] = tmp[source_index] / static_cast<RealType>(ratio);
211 }
212
213 return out;
214 }
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
unsigned renderFilterLength() noexcept
Get the render filter length.
Definition parameters.h:139

References max, params::oversampleRatio(), and params::renderFilterLength().

Referenced by processing::pipeline::applyDownsampling().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ fmcwChirpDirectionToken()

std::string_view fers_signal::fmcwChirpDirectionToken ( const FmcwChirpDirection  direction)
noexcept

Converts a chirp direction to the schema token.

Definition at line 30 of file radar_signal.cpp.

31 {
32 return direction == FmcwChirpDirection::Down ? "down" : "up";
33 }

References Down, and max.

Referenced by serial::xml_serializer_utils::serializeWaveform(), and to_json().

+ Here is the caller graph for this function:

◆ from_json()

void fers_signal::from_json ( const nlohmann::json &  j,
std::unique_ptr< RadarSignal > &  rs 
)

Definition at line 653 of file json_serializer.cpp.

654 {
655 const auto name = j.at("name").get<std::string>();
656 const auto id = parse_json_id(j, "id", "waveform");
657 const auto power = j.at("power").get<RealType>();
658 const auto carrier = j.at("carrier_frequency").get<RealType>();
659
660 if (j.contains("cw"))
661 {
662 auto cw_signal = std::make_unique<CwSignal>();
663 rs = std::make_unique<RadarSignal>(name, power, carrier, params::endTime() - params::startTime(),
664 std::move(cw_signal), id);
665 }
666 else if (j.contains("fmcw_linear_chirp"))
667 {
668 const auto& fmcw_json = j.at("fmcw_linear_chirp");
669 const auto direction = parseFmcwChirpDirection(fmcw_json.at("direction").get<std::string>());
670 std::optional<std::size_t> chirp_count;
671 if (fmcw_json.contains("chirp_count"))
672 {
673 const auto parsed_count = fmcw_json.at("chirp_count").get<long long>();
674 if (parsed_count <= 0)
675 {
676 throw std::runtime_error("Waveform '" + name + "' has an invalid chirp_count.");
677 }
678 chirp_count = static_cast<std::size_t>(parsed_count);
679 }
680
681 auto fmcw_signal = std::make_unique<FmcwChirpSignal>(
682 fmcw_json.at("chirp_bandwidth").get<RealType>(), fmcw_json.at("chirp_duration").get<RealType>(),
683 fmcw_json.at("chirp_period").get<RealType>(), fmcw_json.value("start_frequency_offset", 0.0),
684 chirp_count, direction);
685 rs = std::make_unique<RadarSignal>(name, power, carrier, fmcw_signal->getChirpDuration(),
686 std::move(fmcw_signal), id);
687 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
688 }
689 else if (j.contains("fmcw_triangle"))
690 {
691 const auto& fmcw_json = j.at("fmcw_triangle");
692 std::optional<std::size_t> triangle_count;
693 if (fmcw_json.contains("triangle_count"))
694 {
695 const auto& count_json = fmcw_json.at("triangle_count");
696 if (!count_json.is_number_integer() && !count_json.is_number_unsigned())
697 {
698 throw std::runtime_error("Waveform '" + name + "' has an invalid triangle_count.");
699 }
700 const auto parsed_count = count_json.get<long long>();
701 if (parsed_count <= 0)
702 {
703 throw std::runtime_error("Waveform '" + name + "' has an invalid triangle_count.");
704 }
705 triangle_count = static_cast<std::size_t>(parsed_count);
706 }
707
708 auto fmcw_signal = std::make_unique<FmcwTriangleSignal>(
709 fmcw_json.at("chirp_bandwidth").get<RealType>(), fmcw_json.at("chirp_duration").get<RealType>(),
710 fmcw_json.value("start_frequency_offset", 0.0), triangle_count);
711 rs = std::make_unique<RadarSignal>(name, power, carrier, fmcw_signal->getTrianglePeriod(),
712 std::move(fmcw_signal), id);
713 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
714 }
715 else if (j.contains("pulsed_from_file"))
716 {
717 const auto& pulsed_file = j.at("pulsed_from_file");
718 const auto filename = pulsed_file.value("filename", "");
719 if (filename.empty())
720 {
721 LOG(logging::Level::WARNING, "Skipping load of file-based waveform '{}': filename is empty.", name);
722 return; // rs remains nullptr
723 }
724 rs = serial::loadWaveformFromFile(name, filename, power, carrier, id);
725 }
726 else
727 {
728 throw std::runtime_error("Unsupported waveform type in from_json for '" + name + "'");
729 }
730 }
#define LOG(level,...)
Definition logging.h:19
@ WARNING
Warning level for potentially harmful situations.
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:109
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:103
std::unique_ptr< RadarSignal > loadWaveformFromFile(const std::string &name, const std::string &filename, const RealType power, const RealType carrierFreq, const SimId id)
Loads a radar waveform from a file and returns a RadarSignal object.

References params::endTime(), serial::loadWaveformFromFile(), LOG, max, parseFmcwChirpDirection(), params::startTime(), and logging::WARNING.

Referenced by serial::parse_waveform_from_json().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ parseFmcwChirpDirection()

FmcwChirpDirection fers_signal::parseFmcwChirpDirection ( const std::string_view  direction)

Parses a schema chirp direction token.

Definition at line 35 of file radar_signal.cpp.

36 {
37 if (direction == "up")
38 {
39 return FmcwChirpDirection::Up;
40 }
41 if (direction == "down")
42 {
43 return FmcwChirpDirection::Down;
44 }
45 throw std::runtime_error("Unsupported FMCW chirp direction '" + std::string(direction) + "'.");
46 }

References Down, max, and Up.

Referenced by from_json(), and serial::xml_parser_utils::parseWaveform().

+ Here is the caller graph for this function:

◆ planFmcwIfResampler()

FmcwIfResamplerPlan fers_signal::planFmcwIfResampler ( const FmcwIfResamplerRequest request)

Definition at line 353 of file if_resampler.cpp.

354 {
355 const auto ratio =
356 approximateFmcwIfRateRatio(request.output_sample_rate_hz, request.input_sample_rate_hz, request.limits);
357 validateLimit(std::isfinite(request.filter_bandwidth_hz) && request.filter_bandwidth_hz > 0.0,
358 "IF resampler filter bandwidth must be positive");
359 validateLimit(request.filter_bandwidth_hz < request.output_sample_rate_hz * 0.5,
360 "IF resampler filter bandwidth must be below output Nyquist");
361 validateLimit(request.limits.max_taps_per_stage > 0, "IF resampler max taps per stage must be positive");
362 validateLimit(request.limits.max_macs_per_output_sample > 0.0, "IF resampler max MAC budget must be positive");
363 validateLimit(request.limits.max_phase_refinement > 0, "IF resampler phase refinement limit must be positive");
364
366
368 plan.input_sample_rate_hz = request.input_sample_rate_hz;
369 plan.requested_output_sample_rate_hz = request.output_sample_rate_hz;
370 plan.actual_output_sample_rate_hz = request.input_sample_rate_hz * ratio.actual_ratio;
371 plan.filter_bandwidth_hz = request.filter_bandwidth_hz;
373 plan.stopband_attenuation_db = request.stopband_attenuation_db;
374 plan.overall_ratio = ratio;
375 plan.limits = request.limits;
376
377 auto remaining_up = ratio.numerator;
378 auto remaining_down = ratio.denominator;
379 RealType current_rate_hz = request.input_sample_rate_hz;
380
381 while (remaining_down % 2 == 0 && remaining_down > 1)
382 {
383 if (request.filter_bandwidth_hz > current_rate_hz * kHalfBandPassbandFraction)
384 {
385 break;
386 }
387
388 const RealType available_transition = current_rate_hz * 0.25 - request.filter_bandwidth_hz;
389 if (available_transition <= 0.0)
390 {
391 break;
392 }
393
396 plan.stages.push_back(makeStage(FmcwIfResamplerStageKind::HalfBandDecimateBy2, current_rate_hz, 1, 2,
397 request.filter_bandwidth_hz, halfband_transition,
398 request.stopband_attenuation_db, request.limits));
399 current_rate_hz *= 0.5;
400 remaining_down /= 2;
401 }
402
404 {
405 plan.stages.push_back(makeStage(FmcwIfResamplerStageKind::RationalPolyphase, current_rate_hz, remaining_up,
406 remaining_down, request.filter_bandwidth_hz, transition_hz,
407 request.stopband_attenuation_db, request.limits));
408 }
409
410 for (const auto& stage : plan.stages)
411 {
412 plan.group_delay_seconds += stage.group_delay_seconds;
413 }
414 recomputePlanCost(plan);
415
416 if (plan.estimated_macs_per_output_sample > request.limits.max_macs_per_output_sample)
417 {
418 throw std::runtime_error(
419 "IF resampler estimated MAC cost " + std::to_string(plan.estimated_macs_per_output_sample) +
420 " exceeds maximum " + std::to_string(request.limits.max_macs_per_output_sample) +
421 "; increase if_sample_rate, reduce if_filter_bandwidth, or increase transition width");
422 }
423
425 plan.warmup_discard_samples = static_cast<std::uint64_t>(std::floor(plan.group_delay_output_samples));
428 plan.fractional_phase_offset = plan.fractional_output_delay_samples * static_cast<RealType>(ratio.denominator);
430 plan.fractional_phase_offset * static_cast<RealType>(plan.phase_refinement) -
431 std::floor(plan.fractional_phase_offset * static_cast<RealType>(plan.phase_refinement));
433 0.5 / (plan.actual_output_sample_rate_hz * static_cast<RealType>(plan.phase_refinement));
435 2.0 * PI * request.filter_bandwidth_hz * plan.estimated_timing_error_seconds;
437
438 return plan;
439 }
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
FmcwIfRateRatio approximateFmcwIfRateRatio(const RealType output_sample_rate_hz, const RealType input_sample_rate_hz, const FmcwIfResamplerLimits &limits)
std::vector< FmcwIfResamplerStagePlan > stages
FmcwIfResamplerLimits limits

References fers_signal::FmcwIfResamplerPlan::actual_output_sample_rate_hz, approximateFmcwIfRateRatio(), fers_signal::FmcwIfResamplerPlan::branch_interpolation_fraction, fers_signal::FmcwIfResamplerPlan::estimated_macs_per_output_sample, fers_signal::FmcwIfResamplerPlan::estimated_phase_error_radians, fers_signal::FmcwIfResamplerPlan::estimated_timing_error_seconds, fers_signal::FmcwIfResamplerPlan::filter_bandwidth_hz, fers_signal::FmcwIfResamplerPlan::filter_transition_width_hz, fers_signal::FmcwIfResamplerPlan::fractional_output_delay_samples, fers_signal::FmcwIfResamplerPlan::fractional_phase_offset, fers_signal::FmcwIfResamplerPlan::group_delay_output_samples, fers_signal::FmcwIfResamplerPlan::group_delay_seconds, HalfBandDecimateBy2, fers_signal::FmcwIfResamplerPlan::input_sample_rate_hz, fers_signal::FmcwIfResamplerPlan::limits, max, fers_signal::FmcwIfResamplerPlan::overall_ratio, fers_signal::FmcwIfResamplerPlan::phase_refinement, PI, RationalPolyphase, fers_signal::FmcwIfResamplerPlan::requested_output_sample_rate_hz, fers_signal::FmcwIfResamplerPlan::stages, fers_signal::FmcwIfResamplerPlan::stopband_attenuation_db, and fers_signal::FmcwIfResamplerPlan::warmup_discard_samples.

+ Here is the call graph for this function:

◆ to_json()

void fers_signal::to_json ( nlohmann::json &  j,
const RadarSignal rs 
)

Definition at line 601 of file json_serializer.cpp.

602 {
603 j = nlohmann::json{{"id", sim_id_to_json(rs.getId())},
604 {"name", rs.getName()},
605 {"power", rs.getPower()},
606 {"carrier_frequency", rs.getCarrier()}};
607 if (dynamic_cast<const CwSignal*>(rs.getSignal()) != nullptr)
608 {
609 j["cw"] = nlohmann::json::object();
610 }
611 else if (const auto* fmcw = rs.getFmcwChirpSignal(); fmcw != nullptr)
612 {
613 j["fmcw_linear_chirp"] = {{"direction", std::string(fmcwChirpDirectionToken(fmcw->getDirection()))},
614 {"chirp_bandwidth", fmcw->getChirpBandwidth()},
615 {"chirp_duration", fmcw->getChirpDuration()},
616 {"chirp_period", fmcw->getChirpPeriod()}};
617 if (std::abs(fmcw->getStartFrequencyOffset()) > EPSILON)
618 {
619 j["fmcw_linear_chirp"]["start_frequency_offset"] = fmcw->getStartFrequencyOffset();
620 }
621 if (fmcw->getChirpCount().has_value())
622 {
623 j["fmcw_linear_chirp"]["chirp_count"] = *fmcw->getChirpCount();
624 }
625 }
626 else if (const auto* triangle = rs.getFmcwTriangleSignal(); triangle != nullptr)
627 {
628 j["fmcw_triangle"] = {{"chirp_bandwidth", triangle->getChirpBandwidth()},
629 {"chirp_duration", triangle->getChirpDuration()}};
630 if (std::abs(triangle->getStartFrequencyOffset()) > EPSILON)
631 {
632 j["fmcw_triangle"]["start_frequency_offset"] = triangle->getStartFrequencyOffset();
633 }
634 if (triangle->getTriangleCount().has_value())
635 {
636 j["fmcw_triangle"]["triangle_count"] = *triangle->getTriangleCount();
637 }
638 }
639 else
640 {
641 if (const auto& filename = rs.getFilename(); filename.has_value())
642 {
643 j["pulsed_from_file"] = {{"filename", *filename}};
644 }
645 else
646 {
647 throw std::logic_error("Attempted to serialize a file-based waveform named '" + rs.getName() +
648 "' without a source filename.");
649 }
650 }
651 }
constexpr RealType EPSILON
Machine epsilon for real numbers.
Definition config.h:51
std::string_view fmcwChirpDirectionToken(const FmcwChirpDirection direction) noexcept
Converts a chirp direction to the schema token.

References EPSILON, fmcwChirpDirectionToken(), and max.

+ Here is the call graph for this function:

◆ upsample()

void fers_signal::upsample ( const std::span< const ComplexType in,
const unsigned  size,
std::span< ComplexType out 
)

Upsamples a complex waveform with zero-stuffing followed by Blackman FIR filtering.

Upsamples a signal by a given ratio.

The production path uses a fixed-length single-stage FIR, so unsupported ratios fail fast before filtering begins.

Parameters
inInput span of base-rate complex samples.
sizeNumber of input samples to process from in.
outOutput span receiving size * oversampleRatio() filtered samples.
Exceptions
std::runtime_errorif the configured oversampling ratio is unsupported.
Parameters
inInput span of complex samples.
sizeSize of the input signal.
outOutput span for upsampled complex samples.
Exceptions
std::invalid_argumentif the input or output spans are empty or the ratio is zero.

Definition at line 145 of file dsp_filters.cpp.

146 {
147 const unsigned ratio = params::oversampleRatio();
148 // TODO: this would be better as a multirate upsampler
149 // This implementation is functional but suboptimal.
150 // Users requiring higher accuracy should oversample outside FERS until this is addressed.
152 const unsigned filter_length = params::renderFilterLength();
153 const auto design = blackmanFir(1 / static_cast<RealType>(ratio), ratio, filter_length);
154 const auto filt_length = static_cast<unsigned>(design.coeffs.size());
155
156 const auto oversampled_size = static_cast<std::size_t>(size) * static_cast<std::size_t>(ratio);
157 std::vector tmp(oversampled_size + static_cast<std::size_t>(filt_length), ComplexType{0.0, 0.0});
158
159 for (unsigned i = 0; i < size; ++i)
160 {
161 tmp[static_cast<std::size_t>(i) * static_cast<std::size_t>(ratio)] = in[i];
162 }
163
164 const FirFilter filt(design.coeffs);
165 filt.filter(tmp);
166
167 const auto delay = static_cast<std::size_t>(filt_length / 2);
168 const std::span<const ComplexType> filtered_output(tmp.data() + delay, oversampled_size);
169 std::ranges::copy(filtered_output, out.begin());
170 }

References max, params::oversampleRatio(), and params::renderFilterLength().

Referenced by fers_signal::Signal::load(), and fers_signal::DecadeUpsampler::~DecadeUpsampler().

+ Here is the call graph for this function:
+ Here is the caller graph for this function: