@@ -601,8 +601,67 @@ def decode_iq(self, raw):
601601 iq /= 128.0 # normalize int8 full-scale to ~[-1, 1)
602602 return iq
603603
604- def load_iq (self , path , count = None , offset_samples = 0 ):
605- # Instance convenience for module-level load_iq (below).
604+ # ---- power / relative calibration (Level 1) ----------------------------
605+ # These turn the device's arbitrary dBFS amplitude into something that is
606+ # at least CONSISTENT across gain settings (and, with a user-supplied
607+ # offset, approximately absolute). They are PURE math on values you give
608+ # them -- the library never invents reference data or claims a calibrated
609+ # dBm figure it cannot honestly produce. Producing the offset and any
610+ # frequency-correction table is a hardware+reference workflow; see
611+ # examples/calibrate.py.
612+
613+ @staticmethod
614+ def power_dbfs (iq ):
615+ # Mean power of a complex64 block in dBFS (dB relative to full scale).
616+ # 0 dBFS == |amplitude| 1.0 (ADC full scale). Always <= 0 for real
617+ # captures. This is the raw, UNCALIBRATED reading.
618+ if len (iq ) == 0 :
619+ return float ("-inf" )
620+ power = float (np .mean ((iq .real .astype (np .float64 ) ** 2
621+ + iq .imag .astype (np .float64 ) ** 2 )))
622+ return 10.0 * np .log10 (power + 1e-20 )
623+
624+ @staticmethod
625+ def gain_db (lna = 0 , vga = 0 , amp = False ):
626+ # Total RX gain through the chain in dB: LNA (IF) + VGA (baseband) +
627+ # the fixed ~14 dB front-end amp if enabled. This is the quantity that
628+ # makes a raw dBFS reading ambiguous -- the SAME signal reads ~36 dB
629+ # different between min and max gain.
630+ return float (lna ) + float (vga ) + (C .AMP_DB if amp else 0.0 )
631+
632+ def relative_power_db (self , iq_or_dbfs , * , lna = None , vga = None , amp = None ,
633+ offset_db = 0.0 , freq_hz = None , freq_correction = None ):
634+ # Gain-normalized power: subtract the gain chain so readings taken at
635+ # DIFFERENT gain settings are directly comparable. This is the Level 1
636+ # relative calibration -- still not absolute dBm, but consistent.
637+ #
638+ # value = dBFS - total_gain_dB + offset_db [- freq_correction(freq)]
639+ #
640+ # iq_or_dbfs: a complex64 block (its power is measured) OR a dBFS float
641+ # lna/vga/amp: gain settings; if omitted, taken from last_params
642+ # offset_db: your single-point reference offset (Level 3), if any.
643+ # With a correct offset this approximates dBm; without
644+ # one it is relative dB (still gain-consistent).
645+ # freq_hz + freq_correction: optional per-frequency correction. If you
646+ # pass a callable freq_correction(freq_hz)->dB (e.g. built
647+ # from a sweep of a flat source, see examples/calibrate.py),
648+ # it is subtracted to flatten the front-end response
649+ # (Level 2). The library ships NO built-in curve, because
650+ # front-end response varies per unit.
651+ dbfs = (iq_or_dbfs if isinstance (iq_or_dbfs , (int , float ))
652+ else self .power_dbfs (iq_or_dbfs ))
653+ lp = self .last_params or {}
654+ if lna is None :
655+ lna = lp .get ("lna_gain" , lp .get ("lna" , 0 ))
656+ if vga is None :
657+ vga = lp .get ("vga_gain" , lp .get ("vga" , 0 ))
658+ if amp is None :
659+ amp = lp .get ("amp" , False )
660+ value = dbfs - self .gain_db (lna , vga , amp ) + float (offset_db )
661+ if freq_hz is not None and freq_correction is not None :
662+ value -= float (freq_correction (freq_hz ))
663+ return value
664+
606665 return load_iq (path , count = count , offset_samples = offset_samples )
607666
608667 def estimate_capture (self , sample_rate , num_samples = None , duration = None ,
@@ -675,4 +734,4 @@ def load_iq(path: str, count: int | None = None,
675734 iq .real = a [0 ::2 ]
676735 iq .imag = a [1 ::2 ]
677736 iq /= 128.0
678- return iq
737+ return iq
0 commit comments