FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
radar_signal.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2006-2008 Marc Brooker and Michael Inggs
4// Copyright (c) 2008-present FERS Contributors (see AUTHORS.md).
5//
6// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
7
8/**
9 * @file radar_signal.cpp
10 * @brief Classes for handling radar waveforms and signals.
11 */
12
13#include "radar_signal.h"
14
15#include <algorithm>
16#include <cmath>
17#include <complex>
18#include <iterator>
19#include <limits>
20#include <stdexcept>
21#include <utility>
22
23#include "core/parameters.h"
24#include "dsp_filters.h"
27
28namespace fers_signal
29{
30 std::string_view fmcwChirpDirectionToken(const FmcwChirpDirection direction) noexcept
31 {
32 return direction == FmcwChirpDirection::Down ? "down" : "up";
33 }
34
36 {
37 if (direction == "up")
38 {
40 }
41 if (direction == "down")
42 {
44 }
45 throw std::runtime_error("Unsupported FMCW chirp direction '" + std::string(direction) + "'.");
46 }
47
48 std::vector<ComplexType> CwSignal::render(const std::vector<interp::InterpPoint>& /*points*/, unsigned& size,
49 const RealType /*fracWinDelay*/) const
50 {
51 size = 0;
52 return {};
53 }
54
55 SteppedFrequencySignal::SteppedFrequencySignal(const RealType start_frequency_offset, const RealType step_size,
56 const std::size_t step_count, const RealType dwell_time,
57 const RealType step_period, std::optional<std::size_t> sweep_count) :
58 _start_frequency_offset(start_frequency_offset), _step_size(step_size), _step_count(step_count),
59 _dwell_time(dwell_time), _step_period(step_period), _sweep_count(sweep_count)
60 {
61 }
62
63 RealType SteppedFrequencySignal::firstFrequency(const RealType carrier_frequency) const noexcept
64 {
65 return carrier_frequency + _start_frequency_offset;
66 }
67
68 RealType SteppedFrequencySignal::lastFrequency(const RealType carrier_frequency) const noexcept
69 {
70 if (_step_count == 0)
71 {
72 return firstFrequency(carrier_frequency);
73 }
74 return firstFrequency(carrier_frequency) + static_cast<RealType>(_step_count - 1U) * _step_size;
75 }
76
78 {
79 if (_step_count == 0)
80 {
81 return 0.0;
82 }
83 return std::abs(static_cast<RealType>(_step_count - 1U) * _step_size);
84 }
85
87 {
88 return static_cast<RealType>(_step_count) * std::abs(_step_size);
89 }
90
92 {
93 if (!_sweep_count.has_value())
94 {
95 return std::nullopt;
96 }
97 return static_cast<RealType>(*_sweep_count) * getSweepPeriod();
98 }
99
100 std::optional<SteppedFrequencySignal::StepState>
102 const RealType carrier_frequency) const noexcept
103 {
104 if (time_since_segment_start < 0.0 || _step_count == 0 || _step_period <= 0.0 || _dwell_time <= 0.0)
105 {
106 return std::nullopt;
107 }
108
109 const RealType sweep_period = getSweepPeriod();
110 const auto sweep_index = static_cast<std::size_t>(std::floor(time_since_segment_start / sweep_period));
111 if (_sweep_count.has_value() && sweep_index >= *_sweep_count)
112 {
113 return std::nullopt;
114 }
115
116 const RealType local_sweep_time = time_since_segment_start - static_cast<RealType>(sweep_index) * sweep_period;
117 const auto step_index = static_cast<std::size_t>(std::floor(local_sweep_time / _step_period));
118 if (step_index >= _step_count)
119 {
120 return std::nullopt;
121 }
122
123 const RealType step_start_time =
124 static_cast<RealType>(sweep_index) * sweep_period + static_cast<RealType>(step_index) * _step_period;
125 const RealType local_step_time = time_since_segment_start - step_start_time;
127 {
128 return std::nullopt;
129 }
130
131 return StepState{.step_index = step_index,
132 .sweep_index = sweep_index,
133 .step_start_time = step_start_time,
134 .dwell_end_time = step_start_time + _dwell_time,
135 .step_end_time = step_start_time + _step_period,
136 .rf_frequency =
137 firstFrequency(carrier_frequency) + static_cast<RealType>(step_index) * _step_size};
138 }
139
140 std::vector<ComplexType> SteppedFrequencySignal::render(const std::vector<interp::InterpPoint>& /*points*/,
141 unsigned& size, const RealType /*fracWinDelay*/) const
142 {
143 size = 0;
144 return {};
145 }
146
147 FmcwChirpSignal::FmcwChirpSignal(const RealType chirp_bandwidth, const RealType chirp_duration,
148 const RealType chirp_period, const RealType start_frequency_offset,
149 std::optional<std::size_t> chirp_count, const FmcwChirpDirection direction) :
150 _chirp_bandwidth(chirp_bandwidth), _chirp_duration(chirp_duration), _chirp_period(chirp_period),
151 _start_frequency_offset(start_frequency_offset), _chirp_count(chirp_count),
152 _chirp_rate(chirp_bandwidth / chirp_duration), _direction(direction)
153 {
154 }
155
156 std::optional<std::size_t>
158 {
159 if (time_since_segment_start < 0.0)
160 {
161 return std::nullopt;
162 }
163
164 const auto chirp_index = static_cast<std::size_t>(std::floor(time_since_segment_start / _chirp_period));
165 if (_chirp_count.has_value() && chirp_index >= *_chirp_count)
166 {
167 return std::nullopt;
168 }
169
170 const RealType chirp_time = time_since_segment_start - static_cast<RealType>(chirp_index) * _chirp_period;
171 // Exact arithmetic cannot make this negative, but floating-point boundary rounding can.
172 if (chirp_time < 0.0 || chirp_time >= _chirp_duration)
173 {
174 return std::nullopt;
175 }
176
177 return chirp_index;
178 }
179
180 std::optional<RealType>
182 {
183 const auto chirp_index = activeChirpIndexAt(time_since_segment_start);
184 if (!chirp_index.has_value())
185 {
186 return std::nullopt;
187 }
188
189 const RealType chirp_time = time_since_segment_start - static_cast<RealType>(*chirp_index) * _chirp_period;
190 return basebandPhaseForChirpTime(chirp_time);
191 }
192
194 {
195 return 2.0 * PI * _start_frequency_offset * chirp_time + PI * getSignedChirpRate() * chirp_time * chirp_time;
196 }
197
198 std::vector<ComplexType> FmcwChirpSignal::render(const std::vector<interp::InterpPoint>& /*points*/, unsigned& size,
199 const RealType /*fracWinDelay*/) const
200 {
201 size = 0;
202 return {};
203 }
204
205 FmcwTriangleSignal::FmcwTriangleSignal(const RealType chirp_bandwidth, const RealType chirp_duration,
206 const RealType start_frequency_offset,
207 std::optional<std::size_t> triangle_count) :
208 _chirp_bandwidth(chirp_bandwidth), _chirp_duration(chirp_duration),
209 _start_frequency_offset(start_frequency_offset), _triangle_count(triangle_count),
210 _chirp_rate(chirp_bandwidth / chirp_duration), _triangle_period(2.0 * chirp_duration),
211 _delta_phi_up(2.0 * PI * start_frequency_offset * chirp_duration +
212 PI * _chirp_rate * chirp_duration * chirp_duration)
213 {
214 }
215
217 {
218 if (triangle_time <= 0.0)
219 {
220 return 0.0;
221 }
222
223 const auto triangle_index = static_cast<std::size_t>(std::floor(triangle_time / _triangle_period));
224 const RealType local_triangle_time = triangle_time - static_cast<RealType>(triangle_index) * _triangle_period;
225 const bool down_leg = local_triangle_time >= _chirp_duration;
226 const RealType u = down_leg ? local_triangle_time - _chirp_duration : local_triangle_time;
227 const RealType phi_base =
228 static_cast<RealType>(triangle_index) * 2.0 * _delta_phi_up + (down_leg ? _delta_phi_up : 0.0);
229 if (!down_leg)
230 {
231 return phi_base + 2.0 * PI * _start_frequency_offset * u + PI * _chirp_rate * u * u;
232 }
233 return phi_base + 2.0 * PI * (_start_frequency_offset + _chirp_bandwidth) * u - PI * _chirp_rate * u * u;
234 }
235
236 std::optional<RealType>
238 {
239 if (time_since_segment_start < 0.0)
240 {
241 return std::nullopt;
242 }
243
244 const auto triangle_index = static_cast<std::size_t>(std::floor(time_since_segment_start / _triangle_period));
245 if (_triangle_count.has_value() && triangle_index >= *_triangle_count)
246 {
247 return std::nullopt;
248 }
249
251 time_since_segment_start - static_cast<RealType>(triangle_index) * _triangle_period;
253 {
254 return std::nullopt;
255 }
256 return basebandPhaseForTriangleTime(time_since_segment_start);
257 }
258
259 std::vector<ComplexType> FmcwTriangleSignal::render(const std::vector<interp::InterpPoint>& /*points*/,
260 unsigned& size, const RealType /*fracWinDelay*/) const
261 {
262 size = 0;
263 return {};
264 }
265
266 RadarSignal::RadarSignal(std::string name, const RealType power, const RealType carrierfreq, const RealType length,
267 std::unique_ptr<Signal> signal, const SimId id) :
268 _name(std::move(name)), _id(id == 0 ? SimIdGenerator::instance().generateId(ObjectType::Waveform) : id),
269 _power(power), _carrierfreq(carrierfreq), _length(length), _signal(std::move(signal))
270 {
271 if (!_signal)
272 {
273 throw std::runtime_error("Signal is empty");
274 }
275 }
276
277 std::vector<ComplexType> RadarSignal::render(const std::vector<interp::InterpPoint>& points, unsigned& size,
278 const RealType fracWinDelay) const
279 {
280 auto data = _signal->render(points, size, fracWinDelay);
281 const RealType scale = std::sqrt(_power);
282
283 std::ranges::for_each(data, [scale](auto& value) { value *= scale; });
284
285 return data;
286 }
287
288 std::vector<ComplexType> RadarSignal::renderSlice(const std::vector<interp::InterpPoint>& points,
290 const std::size_t sampleCount, const RealType fracWinDelay) const
291 {
292 auto data = _signal->renderSlice(points, outputStartTime, outputSampleRate, sampleCount, fracWinDelay);
293 const RealType scale = std::sqrt(_power);
294
295 std::ranges::for_each(data, [scale](auto& value) { value *= scale; });
296
297 return data;
298 }
299
301 {
302 if (dynamic_cast<const CwSignal*>(_signal.get()) != nullptr)
303 {
304 return true;
305 }
306 const auto* file = getFileSignal();
307 return file != nullptr && file->getKind() == FileWaveformKind::Cw;
308 }
309
311 {
312 return dynamic_cast<const FmcwChirpSignal*>(_signal.get()) != nullptr;
313 }
314
316 {
317 return dynamic_cast<const FmcwTriangleSignal*>(_signal.get()) != nullptr;
318 }
319
320 bool RadarSignal::isFmcwFamily() const noexcept { return _signal->isFmcwFamily(); }
321
323 {
324 return dynamic_cast<const SteppedFrequencySignal*>(_signal.get()) != nullptr;
325 }
326
328 {
329 return dynamic_cast<const FmcwChirpSignal*>(_signal.get());
330 }
331
333 {
334 return dynamic_cast<const FmcwTriangleSignal*>(_signal.get());
335 }
336
338 {
339 return dynamic_cast<const SteppedFrequencySignal*>(_signal.get());
340 }
341
343 {
344 return dynamic_cast<const FileSignal*>(_signal.get());
345 }
346
348 {
349 _size = 0;
350 _rate = 0;
351 }
352
353 void Signal::load(std::span<const ComplexType> inData, const unsigned samples, const RealType sampleRate)
354 {
355 clear();
356 const unsigned ratio = params::oversampleRatio();
357 const auto oversampled_samples = static_cast<std::size_t>(samples) * static_cast<std::size_t>(ratio);
358 if (oversampled_samples > std::numeric_limits<unsigned>::max())
359 {
360 throw std::overflow_error("Oversampled signal sample count exceeds unsigned range");
361 }
362 _data.resize(oversampled_samples);
363 _size = static_cast<unsigned>(oversampled_samples);
364 _rate = sampleRate * static_cast<RealType>(ratio);
365
366 if (ratio == 1)
367 {
368 std::ranges::copy(inData, _data.begin());
369 }
370 else
371 {
372 upsample(inData, samples, _data);
373 }
374 }
375
377 {
378 if (_data.empty() || _rate <= 0.0 || !std::isfinite(time_since_start) || time_since_start < 0.0 ||
379 time_since_start >= static_cast<RealType>(_size) / _rate)
380 {
381 return {0.0, 0.0};
382 }
383
384 const RealType position = time_since_start * _rate;
385 const auto center = static_cast<long long>(std::floor(position));
386 const RealType fraction = position - std::floor(position);
388 const auto filter_length = static_cast<long long>(filter.size());
389 const auto sample_count = static_cast<long long>(_data.size());
390 ComplexType value{0.0, 0.0};
391 for (long long tap = 0; tap < filter_length; ++tap)
392 {
393 const long long sample_index = center + tap - filter_length / 2;
394 if (sample_index >= 0 && sample_index < sample_count)
395 {
396 value += _data[static_cast<std::size_t>(sample_index)] * filter[static_cast<std::size_t>(tap)];
397 }
398 }
399 return value;
400 }
401
402 std::vector<ComplexType> Signal::render(const std::vector<interp::InterpPoint>& points, unsigned& size,
403 const double fracWinDelay) const
404 {
405 size = _size;
406 if (points.empty())
407 {
408 return std::vector<ComplexType>(_size);
409 }
410 return renderSlice(points, points.front().time, _rate, _size, fracWinDelay);
411 }
412
413 std::vector<ComplexType> Signal::renderSlice(const std::vector<interp::InterpPoint>& points,
415 const std::size_t sampleCount, const RealType fracWinDelay) const
416 {
417 auto out = std::vector<ComplexType>(sampleCount);
418 if (_size == 0 || _rate <= 0.0 || outputSampleRate <= 0.0 || points.empty())
419 {
420 return out;
421 }
422
423 const RealType timestep = 1.0 / outputSampleRate;
424 const int filt_length = static_cast<int>(params::renderFilterLength());
426
427 auto iter = points.begin();
428 auto next = points.size() > 1 ? std::next(iter) : iter;
429 const RealType idelay = std::round(_rate * iter->delay);
431
432 for (std::size_t i = 0; i < sampleCount; ++i)
433 {
434 while (sample_time > next->time && next != iter)
435 {
436 iter = next;
437 if (std::next(next) != points.end())
438 {
439 ++next;
440 }
441 else
442 {
443 break;
444 }
445 }
446
447 auto [amplitude, phase, fdelay, i_sample_unwrap] =
448 calculateWeightsAndDelays(iter, next, sample_time, idelay, fracWinDelay);
449 const RealType native_position = (sample_time - points.front().time) * _rate;
450 const auto source_index = static_cast<int>(std::floor(native_position));
453 {
454 source_fraction = 0.0;
455 }
456
458 const auto delay_unwrap = static_cast<int>(std::floor(combined_delay));
459 fdelay = combined_delay - static_cast<RealType>(delay_unwrap);
461
462 const auto& filt = interp.getFilter(fdelay);
463 const ComplexType accum =
464 performConvolution(source_index, filt.data(), filt_length, amplitude, i_sample_unwrap);
465 out[i] = std::exp(ComplexType(0.0, 1.0) * phase) * accum;
466
468 }
469
470 return out;
471 }
472
473 std::tuple<RealType, RealType, RealType, int>
474 Signal::calculateWeightsAndDelays(const std::vector<interp::InterpPoint>::const_iterator iter,
475 const std::vector<interp::InterpPoint>::const_iterator next,
476 const RealType sampleTime, const RealType idelay,
477 const RealType fracWinDelay) const noexcept
478 {
479 const RealType bw = iter < next ? (sampleTime - iter->time) / (next->time - iter->time) : 0.0;
480
481 const RealType amplitude = std::lerp(std::sqrt(iter->power), std::sqrt(next->power), bw);
482 const RealType phase = std::lerp(iter->phase, next->phase, bw);
483 RealType fdelay = -(std::lerp(iter->delay, next->delay, bw) * _rate - idelay + fracWinDelay);
484
485 const int i_sample_unwrap = static_cast<int>(std::floor(fdelay));
487
488 return {amplitude, phase, fdelay, i_sample_unwrap};
489 }
490
491 ComplexType Signal::performConvolution(const int i, const RealType* filt, const int filtLength,
492 const RealType amplitude, const int iSampleUnwrap) const noexcept
493 {
494 const int start = std::max(-filtLength / 2, -i);
495 const int end = std::min(filtLength / 2, static_cast<int>(_size) - i);
496
497 ComplexType accum(0.0, 0.0);
498
499 for (int j = start; j < end; ++j)
500 {
501 const int sample_idx = i + j + iSampleUnwrap;
502 const int filt_idx = j + filtLength / 2;
503 if (sample_idx >= 0 && sample_idx < static_cast<int>(_size) && filt_idx >= 0 && filt_idx < filtLength)
504 {
505 accum += amplitude * _data[static_cast<std::size_t>(sample_idx)] * filt[filt_idx];
506 }
507 }
508
509 return accum;
510 }
511}
Vec3 position
Thread-safe Meyers singleton for generating unique object IDs.
Definition sim_id.h:42
Continuous-wave signal implementation.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const override
Renders the signal data.
File-backed sampled waveform with explicit radar-mode identity.
FMCW linear chirp signal implementation.
std::optional< RealType > instantaneousBasebandPhase(RealType time_since_segment_start) const noexcept
Computes instantaneous baseband phase at a time since segment start.
RealType basebandPhaseForChirpTime(RealType chirp_time) const noexcept
Computes baseband phase for a time inside a chirp.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const override
Renders an FMCW waveform from interpolation points.
FmcwChirpSignal(RealType chirp_bandwidth, RealType chirp_duration, RealType chirp_period, RealType start_frequency_offset=0.0, std::optional< std::size_t > chirp_count=std::nullopt, FmcwChirpDirection direction=FmcwChirpDirection::Up)
Constructs an FMCW chirp signal with timing and sweep parameters.
std::optional< std::size_t > activeChirpIndexAt(RealType time_since_segment_start) const noexcept
Returns the active chirp index for a time since the segment start.
FMCW symmetric triangular modulation signal implementation.
RealType basebandPhaseForTriangleTime(RealType triangle_time) const noexcept
Computes baseband phase at a time since the triangle train start.
std::optional< RealType > instantaneousBasebandPhase(RealType time_since_segment_start) const noexcept
Computes instantaneous baseband phase at a time since segment start.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const override
Renders an FMCW waveform from interpolation points.
FmcwTriangleSignal(RealType chirp_bandwidth, RealType chirp_duration, RealType start_frequency_offset=0.0, std::optional< std::size_t > triangle_count=std::nullopt)
Constructs an FMCW triangular modulation signal.
const class SteppedFrequencySignal * getSteppedFrequencySignal() const noexcept
Gets the stepped-frequency implementation, if this signal owns one.
RadarSignal(std::string name, RealType power, RealType carrierfreq, RealType length, std::unique_ptr< Signal > signal, const SimId id=0)
Constructs a RadarSignal object.
std::vector< ComplexType > renderSlice(const std::vector< interp::InterpPoint > &points, RealType outputStartTime, RealType outputSampleRate, std::size_t sampleCount, RealType fracWinDelay) const
Renders a bounded absolute-time slice on the requested output grid.
const class FileSignal * getFileSignal() const noexcept
Gets the file-backed signal implementation, if this signal owns one.
const class FmcwTriangleSignal * getFmcwTriangleSignal() const noexcept
Gets the FMCW triangle implementation, if this signal owns one.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const
Renders the radar signal.
bool isFmcwTriangle() const noexcept
Returns true when this signal is an FMCW triangular modulation signal.
bool isFmcwFamily() const noexcept
Returns true when this signal belongs to the FMCW waveform family.
bool isSteppedFrequency() const noexcept
Returns true when this signal is a stepped-frequency CW waveform.
bool isFmcwChirp() const noexcept
Returns true when this signal is an FMCW linear chirp signal.
bool isCw() const noexcept
Returns true when this signal is a continuous-wave signal.
const class FmcwChirpSignal * getFmcwChirpSignal() const noexcept
Gets the FMCW chirp implementation, if this signal owns one.
virtual std::vector< ComplexType > renderSlice(const std::vector< interp::InterpPoint > &points, RealType outputStartTime, RealType outputSampleRate, std::size_t sampleCount, RealType fracWinDelay) const
Renders a bounded absolute-time slice on the requested output grid.
void clear() noexcept
Clears the internal signal data.
void load(std::span< const ComplexType > inData, unsigned samples, RealType sampleRate)
Loads complex radar waveform data.
ComplexType sampleAt(RealType time_since_start) const noexcept
Samples the finite stored complex envelope using the render interpolation filter.
virtual std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, double fracWinDelay) const
Renders the signal data based on interpolation points.
Stepped-frequency continuous-wave signal implementation.
RealType effectiveBandwidth() const noexcept
Gets DFT-convention effective bandwidth in hertz.
std::optional< StepState > activeStepAt(RealType time_since_segment_start, RealType carrier_frequency) const noexcept
Returns the active step for a time since the segment start.
std::optional< RealType > totalDuration() const noexcept
Gets finite waveform duration, if sweep_count is configured.
RealType getSweepPeriod() const noexcept
Gets full sweep period in seconds.
RealType lastFrequency(RealType carrier_frequency) const noexcept
Gets final-step RF frequency in hertz.
RealType frequencySpan() const noexcept
Gets first-to-last absolute span in hertz.
RealType firstFrequency(RealType carrier_frequency) const noexcept
Gets first-step RF frequency in hertz.
SteppedFrequencySignal(RealType start_frequency_offset, RealType step_size, std::size_t step_count, RealType dwell_time, RealType step_period, std::optional< std::size_t > sweep_count=std::nullopt)
Constructs a uniform stepped-frequency CW signal.
std::vector< ComplexType > render(const std::vector< interp::InterpPoint > &points, unsigned &size, RealType fracWinDelay) const override
Renders the signal data. For SFCW signals, this is a no-op.
static InterpFilter & getInstance() noexcept
Retrieves the singleton instance of the InterpFilter class.
std::span< const RealType > getFilter(RealType delay) const
Retrieves a span of precomputed filter values for a given delay.
double RealType
Type for real numbers.
Definition config.h:27
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
Header file for Digital Signal Processing (DSP) filters and upsampling/downsampling functionality.
Interpolation filter implementation using Kaiser windowing.
Defines a structure to store interpolation point data for signal processing.
FmcwChirpDirection parseFmcwChirpDirection(const std::string_view direction)
Parses a schema chirp direction token.
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::string_view fmcwChirpDirectionToken(const FmcwChirpDirection direction) noexcept
Converts a chirp direction to the schema token.
FmcwChirpDirection
Sweep direction for a linear FMCW chirp.
@ Down
Instantaneous baseband frequency decreases over the chirp.
@ Up
Instantaneous baseband frequency increases over the chirp.
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
unsigned renderFilterLength() noexcept
Get the render filter length.
Definition parameters.h:139
Defines the Parameters struct and provides methods for managing simulation parameters.
Classes for handling radar waveforms and signals.
uint64_t SimId
64-bit Unique Simulation ID.
Definition sim_id.h:18
ObjectType
Categorizes objects for ID generation.
Definition sim_id.h:25
math::Vec3 max
Active SFCW dwell selected for one local waveform time.
std::size_t step_index
Zero-based step index inside a sweep.