Skip to content

Commit

Permalink
Merge branch '184-fix-fmcw-parameterization' into 'master'
Browse files Browse the repository at this point in the history
Resolve "FMCW allows higher bandwidth than sampling rate"

Closes #184

See merge request barkhauseninstitut/wicon/hermespy!173
  • Loading branch information
adlerjan committed Apr 5, 2024
2 parents 19aa981 + acb9e18 commit 027953c
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 21 deletions.
56 changes: 41 additions & 15 deletions hermespy/radar/fmcw.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class FMCW(RadarWaveform, Serializable):

yaml_tag = "Radar-FMCW"

__DERIVE_SAMPLING_RATE: float = 0.0
"""Magic number at which FMCW waveforms will automatically derive the sampling rates."""

__num_chirps: int # Number of chirps per radar frame
__bandwidth: float # Sweep bandwidth of the chirp in Hz
__sampling_rate: float # simulation sampling rate of the baseband signal in Hz
Expand All @@ -53,8 +56,8 @@ def __init__(
bandwidth: float = 0.1e9,
chirp_duration: float = 1.5e-6,
pulse_rep_interval: float = 1.5e-6,
sampling_rate: float = None,
adc_sampling_rate: float = None,
sampling_rate: float | None = None,
adc_sampling_rate: float | None = None,
) -> None:
"""
Args:
Expand All @@ -77,9 +80,11 @@ def __init__(
sampling_rate (float, optional):
Sampling rate of the baseband signal in Hz.
If not specified, the sampling rate will be equal to the bandwidth.
adc_sampling_rate (float, optional):
Sampling rate of the analog-digital conversion in Hz.
If not specified, the adc sampling rate will be equal to the bandwidth.
"""

# Initialize base class
Expand All @@ -90,9 +95,9 @@ def __init__(
self.bandwidth = bandwidth
self.chirp_duration = chirp_duration
self.pulse_rep_interval = pulse_rep_interval
self.sampling_rate = bandwidth if sampling_rate is None else sampling_rate
self.sampling_rate = FMCW.__DERIVE_SAMPLING_RATE if sampling_rate is None else sampling_rate
self.adc_sampling_rate = (
self.sampling_rate if adc_sampling_rate is None else adc_sampling_rate
FMCW.__DERIVE_SAMPLING_RATE if adc_sampling_rate is None else adc_sampling_rate
)

def ping(self) -> Signal:
Expand Down Expand Up @@ -235,31 +240,37 @@ def chirp_duration(self, value: float) -> None:

@property
def adc_sampling_rate(self) -> float:
"""Sampling rate at ADC
Returns:
float: sampling rate in Hz.
"""Sampling rate at the ADC in Hz.
Raises:
ValueError: If sampling rate is smaller or equal to zero.
"""

# If the sampling rate is not specified, it will be derived from the bandwidth
if self.__adc_sampling_rate == FMCW.__DERIVE_SAMPLING_RATE:
return self.bandwidth

return self.__adc_sampling_rate

@adc_sampling_rate.setter
def adc_sampling_rate(self, value: float) -> None:
if value <= 0.0:
raise ValueError("ADC Sampling rate must be greater than zero")
if value < 0.0:
raise ValueError("ADC Sampling rate must be non-negative")

self.__adc_sampling_rate = value

@property
def sampling_rate(self) -> float:
# If the sampling rate is not specified, it will be derived from the bandwidth
if self.__sampling_rate == FMCW.__DERIVE_SAMPLING_RATE:
return self.__bandwidth

return self.__sampling_rate

@sampling_rate.setter
def sampling_rate(self, value: float) -> None:
if value <= 0.0:
raise ValueError("Sampling rate must be greater than zero")
if value < 0.0:
raise ValueError("Sampling rate must be non-negative")

self.__sampling_rate = value

Expand Down Expand Up @@ -312,18 +323,33 @@ def power(self) -> float:
return 1.0

def __chirp_prototype(self) -> np.ndarray:
"""Prototype function to generate a single chirp."""
"""Prototype function to generate a single chirp.
num_samples = int(self.chirp_duration * self.sampling_rate)
Raises:
RuntimeError: If the sampling rate is smaller than the chirp bandwidth.
"""

# Validate that there's no aliasing during chirp generation
if self.sampling_rate < self.bandwidth:
raise RuntimeError(
f"Sampling rate may not be smaller than chirp bandwidth ({self.sampling_rate} < {self.bandwidth})"
)

num_samples = int(self.chirp_duration * self.sampling_rate)
timestamps = np.arange(num_samples) / self.sampling_rate

# baseband chirp between -B/2 and B/2
chirp = np.exp(1j * pi * (-self.bandwidth * timestamps + self.slope * timestamps**2))
return chirp

def __pulse_prototype(self) -> np.ndarray:
"""Prototype function to generate a single pulse including chirp and guard interval."""
"""Prototype function to generate a single pulse including chirp and guard interval.
Raises:
RuntimeError: If the pulse repetition interval is smaller than the chirp duration.
"""

num_zero_samples = int((self.pulse_rep_interval - self.chirp_duration) * self.sampling_rate)

Expand Down
19 changes: 13 additions & 6 deletions tests/unit_tests/radar/test_fmcw.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ def test_init(self) -> None:
self.assertEqual(self.chirp_duration, self.fmcw.chirp_duration)
self.assertEqual(self.pulse_rep_interval, self.fmcw.pulse_rep_interval)
self.assertEqual(self.sampling_rate, self.fmcw.sampling_rate)

def test_ping_validation(self) -> None:
"""Creating a ping should raise a RuntimeError on invalid parameterizations"""

self.fmcw.sampling_rate = 0.5 * self.fmcw.bandwidth
with self.assertRaises(RuntimeError):
_ = self.fmcw.ping()

def test_ping_estimate(self) -> None:
"""Pinging and estimating should result in a valid velocity-range profile"""
Expand Down Expand Up @@ -115,9 +122,6 @@ def test_chirp_duration_setget(self) -> None:
def test_adc_sampling_rate_validation(self) -> None:
"""ADC sampling rate property setter should raise ValueError on arguments smaller or equal to zero"""

with self.assertRaises(ValueError):
self.fmcw.adc_sampling_rate = 0.0

with self.assertRaises(ValueError):
self.fmcw.adc_sampling_rate = -1.0

Expand All @@ -129,6 +133,9 @@ def test_adc_sampling_rate_setget(self) -> None:

self.assertEqual(adc_sampling_rate, self.fmcw.adc_sampling_rate)

self.fmcw.adc_sampling_rate = 0.0
self.assertEqual(self.fmcw.bandwidth, self.fmcw.adc_sampling_rate)

def test_sampling_rate_setget(self) -> None:
"""Sampling rate property getter should return setter argument"""

Expand All @@ -137,12 +144,12 @@ def test_sampling_rate_setget(self) -> None:

self.assertEqual(sampling_rate, self.fmcw.sampling_rate)

self.fmcw.sampling_rate = 0.0
self.assertEqual(self.fmcw.bandwidth, self.fmcw.sampling_rate)

def test_sampling_rate_validation(self) -> None:
"""Sampling rate property setter should raise ValueError on arguments smaller or equal to zero"""

with self.assertRaises(ValueError):
self.fmcw.sampling_rate = 0.0

with self.assertRaises(ValueError):
self.fmcw.sampling_rate = -1.0

Expand Down

0 comments on commit 027953c

Please sign in to comment.