Lab061: integrate T2 into the strict receiver
This commit is contained in:
@@ -25,6 +25,7 @@ from scipy.signal import oaconvolve
|
||||
|
||||
from experiments import lab052_candidate_detection as detection
|
||||
from experiments.lab053_viterbi import viterbi_decode_soft, warmup
|
||||
from protocol import bpsk_carrier_tracking as carrier_tracking
|
||||
|
||||
previous = detection.previous
|
||||
physical = previous.lab045
|
||||
@@ -83,9 +84,11 @@ def measured(name):
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def rotation(length: int) -> np.ndarray:
|
||||
def rotation(length: int, sample_rate_hz: int = SAMPLE_RATE) -> np.ndarray:
|
||||
indexes = np.arange(length)
|
||||
values = np.exp(-2j * np.pi * lab043.CARRIER_OFFSET_HZ * indexes / SAMPLE_RATE).astype(np.complex64)
|
||||
values = np.exp(
|
||||
-2j * np.pi * lab043.CARRIER_OFFSET_HZ * indexes / sample_rate_hz
|
||||
).astype(np.complex64)
|
||||
values.flags.writeable = False
|
||||
return values
|
||||
|
||||
@@ -97,24 +100,46 @@ class PacketProcessor:
|
||||
Полное тело по-прежнему запрашивается только после валидного PHY header.
|
||||
"""
|
||||
|
||||
def __init__(self, *, viterbi=viterbi_decode_soft):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
viterbi=viterbi_decode_soft,
|
||||
carrier_tracking_mode: str = "legacy",
|
||||
sample_rate_hz: int = SAMPLE_RATE,
|
||||
symbol_rate: int = SYMBOL_RATE,
|
||||
):
|
||||
if carrier_tracking_mode not in ("legacy", "t2"):
|
||||
raise ValueError("carrier_tracking_mode должен быть legacy или t2")
|
||||
if type(sample_rate_hz) is not int or type(symbol_rate) is not int:
|
||||
raise TypeError("Fs и Rs должны быть целыми")
|
||||
if sample_rate_hz <= 0 or symbol_rate <= 0 or sample_rate_hz % symbol_rate:
|
||||
raise ValueError("Fs должен быть положительным целым кратным Rs")
|
||||
self.carrier_tracking_mode = carrier_tracking_mode
|
||||
self.sample_rate_hz = sample_rate_hz
|
||||
self.symbol_rate = symbol_rate
|
||||
self.samples_per_symbol = sample_rate_hz // symbol_rate
|
||||
self.startup = warmup() if viterbi is viterbi_decode_soft else {"backend": "CUSTOM", "warmup_seconds": 0}
|
||||
constants = {"SAMPLES_PER_SYMBOL": SAMPLES_PER_SYMBOL, "SYMBOL_RATE": SYMBOL_RATE}
|
||||
constants = {
|
||||
"SAMPLES_PER_SYMBOL": self.samples_per_symbol,
|
||||
"SYMBOL_RATE": self.symbol_rate,
|
||||
}
|
||||
scale = bind(physical.lab044._scale_and_carrier, **constants)
|
||||
self.bits = physical.lab044.preamble_bits().copy()
|
||||
waveform = scale(previous.iq.radio.bpsk_modulate(self.bits))
|
||||
self.reference = physical.lab044.signed_iq_bytes_to_complex64(lab043.to_cs8_bytes(waveform))
|
||||
self.taps = bind(lab043._matched_filter_taps, **constants)()
|
||||
self.expected = previous.iq.radio.bpsk_modulate(self.bits)
|
||||
self.kernel = np.zeros((len(self.bits) - 1) * SAMPLES_PER_SYMBOL + 1, np.complex64)
|
||||
self.kernel[::SAMPLES_PER_SYMBOL] = np.conj(self.expected)
|
||||
self.kernel = np.zeros(
|
||||
(len(self.bits) - 1) * self.samples_per_symbol + 1, np.complex64
|
||||
)
|
||||
self.kernel[::self.samples_per_symbol] = np.conj(self.expected)
|
||||
self.kernel = self.kernel[::-1].copy()
|
||||
for values in (self.bits, self.reference, self.taps, self.expected, self.kernel):
|
||||
values.flags.writeable = False
|
||||
slow_offsets = bind(lab043._timing_offsets, **constants)
|
||||
slow_timing = bind(previous.ORIGINAL_TIMING, _timing_offsets=slow_offsets, **constants)
|
||||
self.timing = measured("timing")(bind(previous.fast_preamble_timing,
|
||||
lab045=SimpleNamespace(SAMPLES_PER_SYMBOL=SAMPLES_PER_SYMBOL),
|
||||
lab045=SimpleNamespace(SAMPLES_PER_SYMBOL=self.samples_per_symbol),
|
||||
timing_kernel=lambda unused: self.kernel, ORIGINAL_TIMING=slow_timing))
|
||||
estimate = measured("cfo_estimation")(bind(lab043._estimate_cfo_gain, **constants))
|
||||
self.carrier = measured("phase_polarity")(bind(lab043._evaluate_symbols,
|
||||
@@ -142,7 +167,20 @@ class PacketProcessor:
|
||||
|
||||
@measured("frequency_shift")
|
||||
def shift(self, segment):
|
||||
return (segment - np.mean(segment)) * rotation(len(segment))
|
||||
if self.carrier_tracking_mode == "legacy":
|
||||
dc_reference = segment
|
||||
else:
|
||||
# Последний raw-отсчёт, способный попасть через причинный RRC в
|
||||
# последнюю P3-точку преамбулы. Header в оценку DC не попадает.
|
||||
fixed_prefix = (
|
||||
16 * self.samples_per_symbol
|
||||
+ self.taps.size
|
||||
+ (carrier_tracking.PREAMBLE_SYMBOLS - 1) * self.samples_per_symbol
|
||||
)
|
||||
dc_reference = segment[:min(len(segment), fixed_prefix)]
|
||||
return (segment - np.mean(dc_reference)) * rotation(
|
||||
len(segment), self.sample_rate_hz
|
||||
)
|
||||
|
||||
@measured("matched_rrc_filtering")
|
||||
def filter(self, values):
|
||||
@@ -150,22 +188,55 @@ class PacketProcessor:
|
||||
|
||||
@measured("soft_symbol_extraction")
|
||||
def normalize(self, matched, nominal, timing, count, carrier):
|
||||
symbols = matched[nominal + timing:nominal + timing + count * SAMPLES_PER_SYMBOL:SAMPLES_PER_SYMBOL].astype(np.complex128)
|
||||
symbols = matched[
|
||||
nominal + timing:
|
||||
nominal + timing + count * self.samples_per_symbol:
|
||||
self.samples_per_symbol
|
||||
].astype(np.complex128)
|
||||
if symbols.size != count:
|
||||
return None
|
||||
symbol_time = (np.arange(count, dtype=np.float64) + .5) / SYMBOL_RATE
|
||||
normalized = symbols * np.exp(-2j * math.pi * carrier["cfo_hz"] * symbol_time)
|
||||
normalized /= carrier["abs_h"] * np.exp(1j * carrier["phase_rad"])
|
||||
return normalized.real
|
||||
if self.carrier_tracking_mode == "legacy":
|
||||
symbol_time = (
|
||||
np.arange(count, dtype=np.float64) + 0.5
|
||||
) / self.symbol_rate
|
||||
normalized = symbols * np.exp(
|
||||
-2j * math.pi * carrier["cfo_hz"] * symbol_time
|
||||
)
|
||||
normalized /= carrier["abs_h"] * np.exp(1j * carrier["phase_rad"])
|
||||
return normalized.real
|
||||
|
||||
if count < carrier_tracking.PREAMBLE_SYMBOLS:
|
||||
return None
|
||||
estimate = carrier_tracking.estimate_p3(
|
||||
symbols[:carrier_tracking.PREAMBLE_SYMBOLS],
|
||||
self.expected,
|
||||
self.symbol_rate,
|
||||
)
|
||||
carrier.update({
|
||||
"cfo_hz": estimate.frequency_hz,
|
||||
"abs_h": float(abs(estimate.gain)),
|
||||
"phase_rad": float(np.angle(estimate.gain)),
|
||||
"polarity": "resolved from P3 full preamble",
|
||||
})
|
||||
result = carrier_tracking.track_t2(
|
||||
symbols,
|
||||
self.symbol_rate,
|
||||
estimate,
|
||||
start_index=carrier_tracking.PREAMBLE_SYMBOLS,
|
||||
)
|
||||
context = CURRENT.get()
|
||||
if context is not None:
|
||||
context.cache.setdefault("carrier_tracking_traces", []).append(result)
|
||||
return result.corrected.real
|
||||
|
||||
@measured("extract_total")
|
||||
def extract(self, samples, frame_start, symbol_count):
|
||||
length = (symbol_count + 42) * SAMPLES_PER_SYMBOL
|
||||
length = (symbol_count + 42) * self.samples_per_symbol
|
||||
if frame_start < 0 or frame_start + length > samples.size:
|
||||
return previous.iq.SoftWindow(False, np.empty(0), 0, 0, 0, {}, "TRUNCATED_IQ_WINDOW")
|
||||
segment = samples[frame_start:frame_start + length]
|
||||
matched = self.filter(self.shift(segment))
|
||||
nominal = 16 * SAMPLES_PER_SYMBOL + self.taps.size - 1
|
||||
nominal = 16 * self.samples_per_symbol + self.taps.size - 1
|
||||
timing = self.timing(matched, nominal, self.expected)
|
||||
carrier = self.carrier(matched, nominal, timing, self.bits, len(self.bits))
|
||||
if carrier["abs_h"] == 0:
|
||||
@@ -174,7 +245,7 @@ class PacketProcessor:
|
||||
if values is None:
|
||||
return previous.iq.SoftWindow(False, np.empty(0), timing, 0, nominal, carrier, "TRUNCATED_SYMBOLS")
|
||||
return previous.iq.SoftWindow(True, values, int(timing),
|
||||
int((frame_start + nominal + timing) % SAMPLES_PER_SYMBOL), nominal, carrier, None)
|
||||
int((frame_start + nominal + timing) % self.samples_per_symbol), nominal, carrier, None)
|
||||
|
||||
def decode(self, samples, candidate, number=1):
|
||||
context = PacketMeasure()
|
||||
@@ -189,7 +260,11 @@ class PacketProcessor:
|
||||
"thread_cpu_seconds": time.thread_time() - cpu_started,
|
||||
"stages": dict(context.seconds), "exclusive_stages": dict(context.exclusive_seconds),
|
||||
"calls": dict(context.calls), "coarse": candidate.sample_offset,
|
||||
"refined": outcome.refined_start_sample}
|
||||
"refined": outcome.refined_start_sample,
|
||||
"carrier_tracking_mode": self.carrier_tracking_mode,
|
||||
"carrier_tracking_traces": tuple(
|
||||
context.cache.get("carrier_tracking_traces", ())
|
||||
)}
|
||||
if outcome.frame is not None:
|
||||
row.update(timing=outcome.frame.timing_offset_samples, cfo_hz=outcome.frame.estimated_cfo_hz,
|
||||
phase_rad=outcome.frame.estimated_phase_rad, polarity=outcome.frame.bpsk_polarity)
|
||||
@@ -201,6 +276,7 @@ class PacketProcessor:
|
||||
def run_functional_tests() -> None:
|
||||
processor = PacketProcessor()
|
||||
assert len(processor.bits) == 1280 and len(processor.taps) == 161
|
||||
assert processor.carrier_tracking_mode == "legacy"
|
||||
assert not processor.reference.flags.writeable
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user