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  FileSignal
 File-backed sampled waveform with explicit radar-mode identity. 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...
 
class  SteppedFrequencySignal
 Stepped-frequency continuous-wave signal implementation. 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...
 
enum class  FileWaveformKind : std::uint8_t { Pulsed , Cw , Fmcw }
 Simulation mode assigned to samples loaded from a waveform file. 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

◆ FileWaveformKind

enum class fers_signal::FileWaveformKind : std::uint8_t
strong

Simulation mode assigned to samples loaded from a waveform file.

Enumerator
Pulsed 
Cw 
Fmcw 

Definition at line 43 of file radar_signal.h.

◆ 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 695 of file json_serializer.cpp.

696 {
697 const auto name = j.at("name").get<std::string>();
698 const auto id = parse_json_id(j, "id", "waveform");
699 const auto power = j.at("power").get<RealType>();
700 const auto carrier = j.at("carrier_frequency").get<RealType>();
701
702 if (j.contains("cw"))
703 {
704 auto cw_signal = std::make_unique<CwSignal>();
705 rs = std::make_unique<RadarSignal>(name, power, carrier, params::endTime() - params::startTime(),
706 std::move(cw_signal), id);
707 }
708 else if (j.contains("stepped_frequency"))
709 {
710 const auto& sfcw_json = j.at("stepped_frequency");
711 const auto step_count = sfcw_json.at("step_count").get<long long>();
712 if (step_count <= 0)
713 {
714 throw std::runtime_error("Waveform '" + name + "' has an invalid step_count.");
715 }
716 std::optional<std::size_t> sweep_count;
717 if (sfcw_json.contains("sweep_count"))
718 {
719 const auto parsed_count = sfcw_json.at("sweep_count").get<long long>();
720 if (parsed_count <= 0)
721 {
722 throw std::runtime_error("Waveform '" + name + "' has an invalid sweep_count.");
723 }
724 sweep_count = static_cast<std::size_t>(parsed_count);
725 }
726 auto sfcw_signal = std::make_unique<SteppedFrequencySignal>(
727 sfcw_json.at("start_frequency_offset").get<RealType>(), sfcw_json.at("step_size").get<RealType>(),
728 static_cast<std::size_t>(step_count), sfcw_json.at("dwell_time").get<RealType>(),
729 sfcw_json.at("step_period").get<RealType>(), sweep_count);
730 rs = std::make_unique<RadarSignal>(name, power, carrier, sfcw_signal->getDwellTime(),
731 std::move(sfcw_signal), id);
732 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
733 }
734 else if (j.contains("fmcw_linear_chirp"))
735 {
736 const auto& fmcw_json = j.at("fmcw_linear_chirp");
737 const auto direction = parseFmcwChirpDirection(fmcw_json.at("direction").get<std::string>());
738 std::optional<std::size_t> chirp_count;
739 if (fmcw_json.contains("chirp_count"))
740 {
741 const auto parsed_count = fmcw_json.at("chirp_count").get<long long>();
742 if (parsed_count <= 0)
743 {
744 throw std::runtime_error("Waveform '" + name + "' has an invalid chirp_count.");
745 }
746 chirp_count = static_cast<std::size_t>(parsed_count);
747 }
748
749 auto fmcw_signal = std::make_unique<FmcwChirpSignal>(
750 fmcw_json.at("chirp_bandwidth").get<RealType>(), fmcw_json.at("chirp_duration").get<RealType>(),
751 fmcw_json.at("chirp_period").get<RealType>(), fmcw_json.value("start_frequency_offset", 0.0),
752 chirp_count, direction);
753 rs = std::make_unique<RadarSignal>(name, power, carrier, fmcw_signal->getChirpDuration(),
754 std::move(fmcw_signal), id);
755 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
756 }
757 else if (j.contains("fmcw_triangle"))
758 {
759 const auto& fmcw_json = j.at("fmcw_triangle");
760 std::optional<std::size_t> triangle_count;
761 if (fmcw_json.contains("triangle_count"))
762 {
763 const auto& count_json = fmcw_json.at("triangle_count");
764 if (!count_json.is_number_integer() && !count_json.is_number_unsigned())
765 {
766 throw std::runtime_error("Waveform '" + name + "' has an invalid triangle_count.");
767 }
768 const auto parsed_count = count_json.get<long long>();
769 if (parsed_count <= 0)
770 {
771 throw std::runtime_error("Waveform '" + name + "' has an invalid triangle_count.");
772 }
773 triangle_count = static_cast<std::size_t>(parsed_count);
774 }
775
776 auto fmcw_signal = std::make_unique<FmcwTriangleSignal>(
777 fmcw_json.at("chirp_bandwidth").get<RealType>(), fmcw_json.at("chirp_duration").get<RealType>(),
778 fmcw_json.value("start_frequency_offset", 0.0), triangle_count);
779 rs = std::make_unique<RadarSignal>(name, power, carrier, fmcw_signal->getTrianglePeriod(),
780 std::move(fmcw_signal), id);
781 validate_fmcw_waveform(*rs, "Waveform '" + name + "'");
782 }
783 else if (j.contains("pulsed_from_file"))
784 {
785 const auto& pulsed_file = j.at("pulsed_from_file");
786 const auto filename = pulsed_file.value("filename", "");
787 if (filename.empty())
788 {
789 LOG(logging::Level::WARNING, "Skipping load of file-based waveform '{}': filename is empty.", name);
790 return; // rs remains nullptr
791 }
792 rs = serial::loadWaveformFromFile(name, filename, power, carrier, id);
793 }
794 else if (j.contains("cw_from_file"))
795 {
796 const auto filename = j.at("cw_from_file").value("filename", "");
797 if (filename.empty())
798 {
799 LOG(logging::Level::WARNING, "Skipping load of file-based waveform '{}': filename is empty.", name);
800 return;
801 }
802 rs = serial::loadWaveformFromFile(name, filename, power, carrier, id, FileWaveformKind::Cw);
803 }
804 else if (j.contains("fmcw_from_file"))
805 {
806 const auto filename = j.at("fmcw_from_file").value("filename", "");
807 if (filename.empty())
808 {
809 LOG(logging::Level::WARNING, "Skipping load of file-based waveform '{}': filename is empty.", name);
810 return;
811 }
812 rs = serial::loadWaveformFromFile(name, filename, power, carrier, id, FileWaveformKind::Fmcw);
813 }
814 else
815 {
816 throw std::runtime_error("Unsupported waveform type in from_json for '" + name + "'");
817 }
818 }
#define LOG(level,...)
Definition logging.h:19
FmcwChirpDirection parseFmcwChirpDirection(const std::string_view direction)
Parses a schema chirp direction token.
@ 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, const FileWaveformKind kind)

References Cw, params::endTime(), Fmcw, 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 610 of file json_serializer.cpp.

611 {
612 j = nlohmann::json{{"id", sim_id_to_json(rs.getId())},
613 {"name", rs.getName()},
614 {"power", rs.getPower()},
615 {"carrier_frequency", rs.getCarrier()}};
616 if (const auto* file = rs.getFileSignal(); file != nullptr)
617 {
618 std::string_view key = "pulsed_from_file";
619 if (file->getKind() == FileWaveformKind::Cw)
620 {
621 key = "cw_from_file";
622 }
623 else if (file->getKind() == FileWaveformKind::Fmcw)
624 {
625 key = "fmcw_from_file";
626 }
627 if (const auto& filename = rs.getFilename(); filename.has_value())
628 {
629 j[key] = {{"filename", *filename}};
630 }
631 else
632 {
633 throw std::logic_error("Attempted to serialize a file-based waveform named '" + rs.getName() +
634 "' without a source filename.");
635 }
636 }
637 else if (dynamic_cast<const CwSignal*>(rs.getSignal()) != nullptr)
638 {
639 j["cw"] = nlohmann::json::object();
640 }
641 else if (const auto* sfcw = rs.getSteppedFrequencySignal(); sfcw != nullptr)
642 {
643 j["stepped_frequency"] = {{"start_frequency_offset", sfcw->getStartFrequencyOffset()},
644 {"step_size", sfcw->getStepSize()},
645 {"step_count", sfcw->getStepCount()},
646 {"dwell_time", sfcw->getDwellTime()},
647 {"step_period", sfcw->getStepPeriod()}};
648 if (sfcw->getSweepCount().has_value())
649 {
650 j["stepped_frequency"]["sweep_count"] = *sfcw->getSweepCount();
651 }
652 }
653 else if (const auto* fmcw = rs.getFmcwChirpSignal(); fmcw != nullptr)
654 {
655 j["fmcw_linear_chirp"] = {{"direction", std::string(fmcwChirpDirectionToken(fmcw->getDirection()))},
656 {"chirp_bandwidth", fmcw->getChirpBandwidth()},
657 {"chirp_duration", fmcw->getChirpDuration()},
658 {"chirp_period", fmcw->getChirpPeriod()}};
659 if (std::abs(fmcw->getStartFrequencyOffset()) > EPSILON)
660 {
661 j["fmcw_linear_chirp"]["start_frequency_offset"] = fmcw->getStartFrequencyOffset();
662 }
663 if (fmcw->getChirpCount().has_value())
664 {
665 j["fmcw_linear_chirp"]["chirp_count"] = *fmcw->getChirpCount();
666 }
667 }
668 else if (const auto* triangle = rs.getFmcwTriangleSignal(); triangle != nullptr)
669 {
670 j["fmcw_triangle"] = {{"chirp_bandwidth", triangle->getChirpBandwidth()},
671 {"chirp_duration", triangle->getChirpDuration()}};
672 if (std::abs(triangle->getStartFrequencyOffset()) > EPSILON)
673 {
674 j["fmcw_triangle"]["start_frequency_offset"] = triangle->getStartFrequencyOffset();
675 }
676 if (triangle->getTriangleCount().has_value())
677 {
678 j["fmcw_triangle"]["triangle_count"] = *triangle->getTriangleCount();
679 }
680 }
681 else
682 {
683 if (const auto& filename = rs.getFilename(); filename.has_value())
684 {
685 j["pulsed_from_file"] = {{"filename", *filename}};
686 }
687 else
688 {
689 throw std::logic_error("Attempted to serialize a file-based waveform named '" + rs.getName() +
690 "' without a source filename.");
691 }
692 }
693 }
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 Cw, EPSILON, Fmcw, 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: