Exemplo n.º 1
0
def MakeLatLngPairs(n_pairs,
                    dmin_meters=0, dmax_meters=150000.,
                    lat_min=32.8, lat_max=41.0,
                    lng_min=-117.0, lng_max=-80):
  """Generates random pairs of points within distance.

  Each pair of points are  within a bounding box, and at at
  controllable distance from each other.

  Inputs:
    n_pairs:  number of pairs to generate.
    d_min_meters, d_max_meters: range of distance for each pair of
        points (default 0 to 150km)
    lat_min, lat_max, lng_min, lng_max: bounding box that contain
        pair of points
        By default uses a bounding box fully included in continental
        US border (but not covering all US).

  Returns:
    an array of tuple (lat1,lng1, lat2, lng2) of size n_pairs
  """
  pair_points = []
  while len(pair_points) < n_pairs:
    lat1 = np.random.uniform(lat_min, lat_max)
    lng1 = np.random.uniform(lng_min, lng_max)
    bearing = np.random.uniform(0., 360.)
    dist = np.random.uniform(dmin_meters, dmax_meters)
    lat2, lng2, _ = vincenty.GeodesicPoint(lat1, lng1, dist, bearing)
    if (lat2 < lat_min or lat2 > lat_max or
        lng2 < lng_min or lng2 > lng_max):
      continue
    pair_points.append((lat1, lng1, lat2, lng2))

  return pair_points
    def test_computeFssBlocking(self):
        # Mock things propag and FSS antenna. -70dBm at 30km
        wf_itm.CalcItmPropagationLoss = testutils.FakePropagationPredictor(
            dist_type='REAL', factor=1.0, offset=70 - 30.0)
        antenna.GetFssAntennaGains = mock.create_autospec(
            antenna.GetFssAntennaGains, return_value=2.8)
        # Create FSS and a CBSD at 30km
        fss_point, fss_info, _ = data.getFssInfo(TestAggInterf.fss_record)
        fss_freq_range = (3650e6, 3750e6)
        cbsd_lat, cbsd_lon, _ = vincenty.GeodesicPoint(fss_point[1],
                                                       fss_point[0], 30, 0)
        cbsd = entities.CBSD_TEMPLATE_CAT_A_OUTDOOR._replace(
            latitude=cbsd_lat, longitude=cbsd_lon)
        grant = entities.ConvertToCbsdGrantInfo([cbsd], 3640, 3680)[0]
        constraint = data.ProtectionConstraint(
            fss_point[1], fss_point[0], 3550e6, fss_freq_range[0],
            data.ProtectedEntityType.FSS_BLOCKING)

        itf = interf.computeInterferenceFssBlocking(grant, constraint,
                                                    fss_info, grant.max_eirp)
        self.assertAlmostEqual(
            itf,
            20 +  # EIRP/MHZ
            10 +  # 10MHz effective bandwidth
            -70  # pathloss
            + 2.8  # FSS antenna gain
            - 3.1634,  # FSS mask loss for adjacent 10MHz
            4)
Exemplo n.º 3
0
  def test_SimplePpaCircle(self):
    # Configuring for -96dBm circle at 16km includes
    wf_hybrid.CalcHybridPropagationLoss = testutils.FakePropagationPredictor(
        dist_type='REAL', factor=1.0, offset=(96+30-0.1) - 16.0)
    expected_ppa = sgeo.Polygon(
        [vincenty.GeodesicPoint(
            TestPpa.devices[0]['installationParam']['latitude'],
            TestPpa.devices[0]['installationParam']['longitude'],
            dist_km=16.0, bearing=angle)[1::-1]  # reverse to lng,lat
         for angle in xrange(360)])

    ppa_zone = ppa.PpaCreationModel(TestPpa.devices, TestPpa.pal_records)
    ppa_zone = json.loads(ppa_zone)

    self.assertAlmostSamePolygon(
        utils.ToShapely(ppa_zone), expected_ppa, 0.001)
Exemplo n.º 4
0
def GenerateCbsdList(n_cbsd, template_cbsd,
                     ref_latitude, ref_longitude,
                     min_distance_km=1,
                     max_distance_km=150,
                     min_angle=0, max_angle=360):
  """Generate a random list of CBSDs from a CBSD template.

  The CBSD are randomly generated in a circular sector around a reference point,
  and using a CBSD template.
  If the template contains a list of azimuth, then multi-sector CBSD are built
  with the given azimuths. Otherwise a random azimuth is used.

  Inputs:
    n_cbsd:          Number of CBSD to generate.
    template_cbsd:   A |Cbsd| namedtuple used as a template for all non location
                     parameters.
    ref_latitude:    Reference point latitude (degrees).
    ref_longitude:   Reference point longitude (degrees).
    min_distance_km: Minimum distance of CBSD from central point (km). Default 1km.
    max_distance_km: Maximum distance of CBSD from central point (km). Default 150km.
    min_angle:       Minimum angle of the circular sector (degrees). Default 0
    max_angle:       Maximum angle of the circular sector (degrees). Default 360
                     Note that 0 degree is the north, and the angle goes clockwise.

  Returns:
    a list of |Cbsd| namedtuple
  """
  distances = np.random.uniform(min_distance_km, max_distance_km, n_cbsd)
  bearings = np.random.uniform(min_angle, max_angle, n_cbsd)
  t = template_cbsd
  azimuths = (t.antenna_azimuth if isinstance(t.antenna_azimuth, list)
              else [t.antenna_azimuth])
  cbsds = []
  for k in xrange(n_cbsd):
    lat, lng, _ = vincenty.GeodesicPoint(ref_latitude, ref_longitude,
                                         distances[k], bearings[k])
    for azimuth in azimuths:
      cbsd = Cbsd(lat, lng,
                  t.height_agl, t.is_indoor, t.category, t.eirp_dbm_mhz,
                  int(azimuth + np.random.uniform(0, 360)) % 360,
                  t.antenna_beamwidth, t.antenna_gain)
      cbsds.append(cbsd)

  return cbsds
Exemplo n.º 5
0
def _GetPolygon(device):
    """Returns the PPA contour for a single CBSD device, as a shapely polygon."""
    install_param = device['installationParam']
    eirp_capability = install_param.get(
        'eirpCapability', MAX_ALLOWABLE_EIRP_PER_10_MHZ_CAT_A if
        device['cbsdCategory'] == 'A' else MAX_ALLOWABLE_EIRP_PER_10_MHZ_CAT_B)

    # Compute all the Points in 0-359 every 200m up to 40km
    distances = np.arange(0.2, 40.1, 0.2)
    azimuths = np.arange(0.0, 360.0)
    latitudes, longitudes, _ = zip(*[
        vincenty.GeodesicPoints(install_param['latitude'],
                                install_param['longitude'], distances, azimuth)
        for azimuth in azimuths
    ])
    # Compute the Gain for all Direction
    # Note: some parameters are optional for catA, so falling back to None (omni) then
    antenna_gains = antenna.GetStandardAntennaGains(
        azimuths, install_param['antennaAzimuth'] if 'antennaAzimuth'
        in install_param else None, install_param['antennaBeamwidth']
        if 'antennaBeamwidth' in install_param else None,
        install_param['antennaGain'])
    # Get the Nlcd Region Type for Cbsd
    cbsd_region_code = drive.nlcd_driver.GetLandCoverCodes(
        install_param['latitude'], install_param['longitude'])
    cbsd_region_type = nlcd.GetRegionType(cbsd_region_code)
    # Compute the Path Loss, and contour based on Gain and Path Loss Comparing with Threshold
    # Smoothing Contour using Hamming Filter
    contour_dists_km = _HammingFilter([
        _CalculateDbLossForEachPointAndGetContour(install_param,
                                                  eirp_capability, ant_gain,
                                                  cbsd_region_type,
                                                  radial_lats, radial_lons)
        for radial_lats, radial_lons, ant_gain in zip(latitudes, longitudes,
                                                      antenna_gains)
    ])
    # Generating lat, lon for Contour
    contour_lats, contour_lons, _ = zip(*[
        vincenty.GeodesicPoint(install_param['latitude'],
                               install_param['longitude'], dists, az)
        for dists, az in zip(contour_dists_km, azimuths)
    ])

    return sgeo.Polygon(zip(contour_lons, contour_lats)).buffer(0)
Exemplo n.º 6
0
def CalcItmPropagationLoss(lat_cbsd,
                           lon_cbsd,
                           height_cbsd,
                           lat_rx,
                           lon_rx,
                           height_rx,
                           cbsd_indoor=False,
                           reliability=0.5,
                           freq_mhz=3625.,
                           its_elev=None,
                           is_height_cbsd_amsl=False,
                           return_internals=False):
    """Implements the WinnForum-compliant ITM point-to-point propagation model.

  According to WinnForum spec R2-SGN-17, R2-SGN-22 and R2-SGN-5 to 10.

  One can use this routine in 3 ways:
    reliability = -1 : to get the average path loss
    reliability in [0,1] : to get a pathloss for given quantile
    sequence of reliabilities: to get an array of pathloss. Used to obtain
      inverse CDF of the pathloss.

  Inputs:
    lat_cbsd, lon_cbsd, height_cbsd: Lat/lon (deg) and height AGL (m) of CBSD
    lat_rx, lon_rx, height_rx:       Lat/lon (deg) and height AGL (m) of Rx point
    cbsd_indoor:         CBSD indoor status - Default=False.
    reliability:         Reliability. Default is 0.5 (median value)
                         Different options:
                           value in [0,1]: returns the CDF quantile
                           -1: returns the mean path loss
                           iterable sequence: returns a list of path losses
    freq_mhz:            Frequency (MHz). Default is mid-point of band.
    its_elev:            Optional profile to use (in ITM format). Default=None
                           If not specified, it is extracted from the terrain.
    is_height_cbsd_amsl: If True, the CBSD height shall be considered as AMSL (Average
                         mean sea level).
    return_internals: If True, returns internal variables.

  Returns:
    A namedtuple of:
      db_loss            Path Loss in dB, either a scalar if reliability is scalar
                           or a list of path losses if reliability is an iterable.

      incidence_angles:  A namedtuple of
          hor_cbsd:        Horizontal departure angle (bearing) from CBSD to Rx
          ver_cbsd:        Vertical departure angle at CBSD
          hor_rx:          Horizontal incidence angle (bearing) from Rx to CBSD
          ver_rx:          Vertical incidence angle at Rx

      internals:         A dictionary of internal data for advanced analysis
                         (only if return_internals=True):
          itm_err_num:     ITM error code from ItmErrorCode (see GetInfoOnItmCode).
          itm_str_mode:    String containing description of dominant prop mode.
          dist_km:         Distance between end points (km).
          prof_d_km        ndarray of distances (km) - x values to plot terrain.
          prof_elev        ndarray of terrain heightsheights (m) - y values to plot terrain,

  Raises:
    Exception if input parameters invalid or out of range.
  """
    # Case of same points
    if (lat_cbsd == lat_rx and lon_cbsd == lon_rx):
        return _PropagResult(db_loss=0 if np.isscalar(reliability) else [0] *
                             len(reliability),
                             incidence_angles=_IncidenceAngles(0, 0, 0, 0),
                             internals=None)

    # Sanity checks on input parameters
    if freq_mhz < 40.0 or freq_mhz > 10000:
        raise Exception('Frequency outside range [40MHz - 10GHz]')

    if is_height_cbsd_amsl:
        altitude_cbsd = drive.terrain_driver.GetTerrainElevation(
            lat_cbsd, lon_cbsd)
        height_cbsd = height_cbsd - altitude_cbsd

    # Ensure minimum height of 1 meter
    if height_cbsd < 1:
        height_cbsd = 1
    if height_rx < 1:
        height_rx = 1

    # Internal ITM parameters are always set to following values in WF version:
    confidence = 0.5  # Confidence (always 0.5)
    dielec = 25.  # Dielectric constant (always 25.)
    conductivity = 0.02  # Conductivity (always 0.02)
    polarization = 1  # Polarization (always vertical = 1)
    mdvar = 13

    # Get the terrain profile, using Vincenty great circle route, and WF
    # standard (bilinear interp; 1500 pts for all distances over 45 km)
    if its_elev is None:
        its_elev = drive.terrain_driver.TerrainProfile(lat1=lat_cbsd,
                                                       lon1=lon_cbsd,
                                                       lat2=lat_rx,
                                                       lon2=lon_rx,
                                                       target_res_meter=30.,
                                                       do_interp=True,
                                                       max_points=1501)

    # Find the midpoint of the great circle path
    dist_km, bearing_cbsd, bearing_rx = vincenty.GeodesicDistanceBearing(
        lat_cbsd, lon_cbsd, lat_rx, lon_rx)
    latmid, lonmid, _ = vincenty.GeodesicPoint(lat_cbsd, lon_cbsd,
                                               dist_km / 2., bearing_cbsd)

    # Determine climate value, based on ITU-R P.617 method:
    climate = drive.climate_driver.TropoClim(latmid, lonmid)
    # If the common volume lies over the sea, the climate value to use depends
    # on the climate values at either end. A simple min() function should
    # properly implement the logic, since water is the max.
    if climate == 7:
        climate = min(drive.climate_driver.TropoClim(lat_cbsd, lon_cbsd),
                      drive.climate_driver.TropoClim(lat_rx, lon_rx))

    # Look up the refractivity at the path midpoint, if not explicitly provided
    refractivity = drive.refract_driver.Refractivity(latmid, lonmid)

    # Call ITM prop loss.
    reliabilities = reliability
    do_avg = False
    if np.isscalar(reliabilities) and reliability == -1:
        # Pathloss mean: average the value for 1% to 99% included
        reliabilities = np.arange(0.01, 1.0, 0.01)
        do_avg = True

    db_loss, ver_cbsd, ver_rx, str_mode, err_num = itm.point_to_point(
        its_elev, height_cbsd, height_rx, dielec, conductivity, refractivity,
        freq_mhz, climate, polarization, confidence, reliabilities, mdvar,
        False)
    if do_avg:
        db_loss = -10 * np.log10(np.mean(10**(-np.array(db_loss) / 10.)))

    # Add indoor losses
    if cbsd_indoor:
        if np.isscalar(db_loss):
            db_loss += 15
        else:
            db_loss = [loss + 15 for loss in db_loss]

    # Create distance/terrain arrays for plotting if desired
    internals = None
    if return_internals:
        prof_d_km = (its_elev[1] / 1000.) * np.arange(len(its_elev) - 2)
        prof_elev = np.asarray(its_elev[2:])
        internals = {
            'itm_err_num': err_num,
            'itm_str_mode': str_mode,
            'dist_km': dist_km,
            'prof_d_km': prof_d_km,
            'prof_elev': prof_elev
        }

    return _PropagResult(db_loss=db_loss,
                         incidence_angles=_IncidenceAngles(
                             hor_cbsd=bearing_cbsd,
                             ver_cbsd=ver_cbsd,
                             hor_rx=bearing_rx,
                             ver_rx=ver_rx),
                         internals=internals)
    def generate_FAD_2_default_config(self, filename):
        """Generates the WinnForum configuration for FAD_2"""
        # Create the actual config for FAD_2

        # Load the esc sensor in SAS Test Harness
        esc_sensor = json.load(
            open(
                os.path.join('testcases', 'testdata',
                             'esc_sensor_record_0.json')))

        # Load the device_c1 with registration in SAS Test Harness
        device_c1 = json.load(
            open(os.path.join('testcases', 'testdata', 'device_a.json')))

        # Get a latitude and longitude within 40 kms from ESC.
        latitude, longitude, _ = vincenty.GeodesicPoint(
            esc_sensor['installationParam']['latitude'],
            esc_sensor['installationParam']['longitude'], 0.1,
            30)  # distance of 0.1 km from ESC at 30 degrees

        # Load the device_c1 in the neighborhood area of the ESC sensor
        device_c1['installationParam']['latitude'] = latitude
        device_c1['installationParam']['longitude'] = longitude

        # Load grant request for device_c1
        grant_g1 = json.load(
            open(os.path.join('testcases', 'testdata', 'grant_0.json')))
        grant_g1['operationParam']['maxEirp'] = 20

        # Load one ppa in SAS Test Harness
        pal_record_0 = json.load(
            open(os.path.join('testcases', 'testdata', 'pal_record_0.json')))
        pal_low_frequency = 3550000000
        pal_high_frequency = 3560000000
        ppa_record_0 = json.load(
            open(os.path.join('testcases', 'testdata', 'ppa_record_0.json')))

        ppa_record_a, pal_records = makePpaAndPalRecordsConsistent(
            ppa_record_0, [pal_record_0], pal_low_frequency,
            pal_high_frequency, 'test_user_1')

        # Load device_c3 in SAS Test Harness
        device_c3 = json.load(
            open(os.path.join('testcases', 'testdata', 'device_c.json')))

        # Load the device_c3 in the neighborhood area of the PPA
        device_c3['installationParam']['latitude'] = 38.821322
        device_c3['installationParam']['longitude'] = -97.280040

        # Load grant request for device_c3 in SAS Test Harness
        grant_g3 = json.load(
            open(os.path.join('testcases', 'testdata', 'grant_0.json')))
        grant_g3['operationParam']['maxEirp'] = 20

        # Update grant_g3 frequency to overlap with PPA zone frequency for C3 device
        grant_g3['operationParam']['operationFrequencyRange'] = {
            'lowFrequency': 3550000000,
            'highFrequency': 3555000000
        }

        # Load the device_c2 with registration to SAS UUT
        device_c2 = json.load(
            open(os.path.join('testcases', 'testdata', 'device_b.json')))
        # Get a latitude and longitude within 40 kms from ESC.
        latitude, longitude, _ = vincenty.GeodesicPoint(
            esc_sensor['installationParam']['latitude'],
            esc_sensor['installationParam']['longitude'], 0.1,
            30)  # distance of 0.1 km from ESC at 30 degrees

        device_c2['installationParam']['latitude'] = latitude
        device_c2['installationParam']['longitude'] = longitude

        # Creating conditionals for C2 and C4.
        self.assertEqual(device_c2['cbsdCategory'], 'B')
        conditional_parameters_c2 = {
            'cbsdCategory': device_c2['cbsdCategory'],
            'fccId': device_c2['fccId'],
            'cbsdSerialNumber': device_c2['cbsdSerialNumber'],
            'airInterface': device_c2['airInterface'],
            'installationParam': device_c2['installationParam'],
            'measCapability': device_c2['measCapability']
        }
        del device_c2['cbsdCategory']
        del device_c2['airInterface']
        del device_c2['installationParam']

        # Load grant request for device_c2 to SAS UUT
        grant_g2 = json.load(
            open(os.path.join('testcases', 'testdata', 'grant_0.json')))
        grant_g2['operationParam']['maxEirp'] = 30

        # Load the device_c4 with registration to SAS UUT.
        device_c4 = json.load(
            open(os.path.join('testcases', 'testdata', 'device_d.json')))

        # Move device_c4 in the neighborhood area of the PPA
        device_c4['installationParam']['latitude'] = 38.8363
        device_c4['installationParam']['longitude'] = -97.2642
        device_c4['installationParam']['antennaBeamwidth'] = 0

        # Creating conditionals for Cat B devices.
        self.assertEqual(device_c4['cbsdCategory'], 'B')
        conditional_parameters_c4 = {
            'cbsdCategory': device_c4['cbsdCategory'],
            'fccId': device_c4['fccId'],
            'cbsdSerialNumber': device_c4['cbsdSerialNumber'],
            'airInterface': device_c4['airInterface'],
            'installationParam': device_c4['installationParam'],
            'measCapability': device_c4['measCapability']
        }
        del device_c4['cbsdCategory']
        del device_c4['airInterface']
        del device_c4['installationParam']

        # Load grant request for device_c4 to SAS UUT
        grant_g4 = json.load(
            open(os.path.join('testcases', 'testdata', 'grant_0.json')))
        grant_g4['operationParam']['maxEirp'] = 20

        #Update grant_g4 frequency to overlap with PPA zone frequency for C4 device
        grant_g4['operationParam']['operationFrequencyRange'] = {
            'lowFrequency': 3550000000,
            'highFrequency': 3555000000
        }

        conditionals = {
            'registrationData':
            [conditional_parameters_c2, conditional_parameters_c4]
        }

        cbsd_records = [device_c1, device_c3]
        grant_record_list = [[grant_g1], [grant_g3]]
        ppa_records = [ppa_record_a]

        # Creating CBSD reference IDs of valid format
        cbsd_reference_id1 = generateCbsdReferenceId('test_fcc_id_x',
                                                     'test_serial_number_x')
        cbsd_reference_id2 = generateCbsdReferenceId('test_fcc_id_y',
                                                     'test_serial_number_y')
        cbsd_reference_ids = [[cbsd_reference_id1, cbsd_reference_id2]]

        # SAS test harness configuration
        sas_harness_config = {
            'sasTestHarnessName': 'SAS-TH-1',
            'hostName': getFqdnLocalhost(),
            'port': getUnusedPort(),
            'serverCert': getCertFilename('sas.cert'),
            'serverKey': getCertFilename('sas.key'),
            'caCert': "certs/ca.cert"
        }
        # Generate FAD Records for each record type like cbsd,zone and esc_sensor
        cbsd_fad_records = generateCbsdRecords(cbsd_records, grant_record_list)
        ppa_fad_records = generatePpaRecords(ppa_records, cbsd_reference_ids)
        esc_fad_records = [esc_sensor]

        sas_harness_dump_records = {
            'cbsdRecords': cbsd_fad_records,
            'ppaRecords': ppa_fad_records,
            'escSensorRecords': esc_fad_records
        }
        config = {
            'registrationRequestC2': device_c2,
            'registrationRequestC4': device_c4,
            'conditionalRegistrationData': conditionals,
            'grantRequestG2': grant_g2,
            'grantRequestG4': grant_g4,
            'palRecords': pal_records,
            'sasTestHarnessConfig': sas_harness_config,
            'sasTestHarnessDumpRecords': sas_harness_dump_records
        }
        writeConfig(filename, config)
Exemplo n.º 8
0
def CalcHybridPropagationLoss(lat_cbsd,
                              lon_cbsd,
                              height_cbsd,
                              lat_rx,
                              lon_rx,
                              height_rx,
                              cbsd_indoor=False,
                              reliability=-1,
                              freq_mhz=3625.,
                              region='RURAL',
                              is_height_cbsd_amsl=False,
                              return_internals=False):
    """Implements the Hybrid ITM/eHata NTIA propagation model.

  As specified by Winforum, see:
    R2-SGN-03, R2-SGN-04 through R2-SGN-10 and NTIA TR 15-517 Appendix A.

  Note that contrary to the ITM model, this function does not provide the
  possibility to retrieve a CDF of path losses, as it is not required in the
  intended use of that model (it shall be used currently only for protecting
  zones using average aggregated interference).

  Warning: Only 'reliability' values 0.5 (median) and -1 (average) are currently
  fully supported, as other values will not return the quantile when using
  internally the eHata model. Workaround is to apply it afterwards when the
  returned opcode=4, and using the standard deviation obtained with the
  'GetEHataStdev()' routine.

  Inputs:
    lat_cbsd, lon_cbsd, height_cbsd: Lat/lon (deg) and height AGL (m) of CBSD
    lat_rx, lon_rx, height_rx:       Lat/lon (deg) and height AGL (m) of Rx point
    cbsd_indoor:        CBSD indoor status - Default=False.
    freq_mhz:           Frequency (MHz). Default is mid-point of band.
    reliability:        Reliability. Default is -1 (average value).
                        Options:
                          Value in [0,1]: returns the CDF quantile
                          -1: returns the mean path loss
    region:             Region type among 'URBAN', 'SUBURBAN, 'RURAL'
    is_height_cbsd_amsl: If True, the CBSD height shall be considered as AMSL (Average
                         mean sea level).

  Returns:
    A namedtuple of:
      db_loss:          Path Loss in dB.

      incidence_angles: A namedtuple of angles (degrees)
          hor_cbsd:       Horizontal departure angle (bearing) from CBSD to Rx
          ver_cbsd:       Vertical departure angle at CBSD
          hor_rx:         Horizontal incidence angle (bearing) from Rx to CBSD
          ver_rx:         Vertical incidence angle at Rx

      internals:        A dictionary of internal data for advanced analysis
                        (only if return_internals=True):
          hybrid_opcode:  Opcode from HybridCode - See GetInfoOnHybridCodes()
          effective_height_cbsd: Effective CBSD antenna height
          itm_db_loss:    Loss in dB for the ITM model.
          itm_err_num:    ITM error code (see wf_itm module).
          itm_str_mode:   Description (string) of dominant prop mode in ITM.
          dist_km:        Distance between end points (km)
          prof_d_km       ndarray of distances (km) - x values to plot terrain.
          prof_elev       ndarray of terrain heightsheights (m) - y values to plot terrain,

  Raises:
    Exception if input parameters invalid or out of range.
  """
    # Case of same points
    if (lat_cbsd == lat_rx and lon_cbsd == lon_rx):
        return _PropagResult(db_loss=0,
                             incidence_angles=_IncidenceAngles(0, 0, 0, 0),
                             internals=None)

    # Sanity checks on input parameters
    if freq_mhz < 40 or freq_mhz > 10000:
        raise Exception('Frequency outside range [40MHz - 10GHz].')
    if region not in ['RURAL', 'URBAN', 'SUBURBAN']:
        raise Exception('Region %s not allowed' % region)
    if reliability not in (-1, 0.5):
        raise Exception('Hybrid model only computes the median or the mean.')

    if is_height_cbsd_amsl:
        altitude_cbsd = drive.terrain_driver.GetTerrainElevation(
            lat_cbsd, lon_cbsd)
        height_cbsd = height_cbsd - altitude_cbsd

    # Get the terrain profile, using Vincenty great circle route, and WF
    # standard (bilinear interp; 1501 pts for all distances over 45 km)
    its_elev = drive.terrain_driver.TerrainProfile(lat1=lat_cbsd,
                                                   lon1=lon_cbsd,
                                                   lat2=lat_rx,
                                                   lon2=lon_rx,
                                                   target_res_meter=30.,
                                                   do_interp=True,
                                                   max_points=1501)

    # Structural CBSD and mobile height corrections
    height_cbsd = max(height_cbsd, 20.)
    height_rx = 1.5

    # Calculate the predicted ITM loss
    # and get the distance and profile for use in further logic
    db_loss_itm, incidence_angles, internals = wf_itm.CalcItmPropagationLoss(
        lat_cbsd,
        lon_cbsd,
        height_cbsd,
        lat_rx,
        lon_rx,
        height_rx,
        False,
        reliability,
        freq_mhz,
        its_elev,
        return_internals=True)
    internals['itm_db_loss'] = db_loss_itm

    # Calculate the effective heights of the tx
    height_cbsd_eff = ehata.CbsdEffectiveHeights(height_cbsd, its_elev)
    internals['effective_height_cbsd'] = height_cbsd_eff

    # Use ITM if CBSD effective height greater than 200 m
    if height_cbsd_eff >= 200:
        return _BuildOutput(db_loss_itm, incidence_angles, internals,
                            HybridMode.ITM_HIGH_HEIGHT, cbsd_indoor)

    # Set the environment code number.
    if region == 'URBAN':
        region_code = 23
    elif region == 'SUBURBAN':
        region_code = 22
    else:  # 'RURAL': use ITM
        if not return_internals: return_internals = None
        return _BuildOutput(db_loss_itm, incidence_angles, internals,
                            HybridMode.ITM_RURAL, cbsd_indoor)

    # The eHata offset to apply (only in case the mean is requested)
    offset_median_to_mean = _GetMedianToMeanOffsetDb(freq_mhz,
                                                     region == 'URBAN')

    # Now process the different cases
    dist_km = internals['dist_km']
    if not return_internals: return_internals = None

    if dist_km <= 0.1:  # Use Free Space Loss
        db_loss = CalcFreeSpaceLoss(dist_km, freq_mhz, height_cbsd, height_rx)
        return _BuildOutput(db_loss, incidence_angles, internals,
                            HybridMode.FSL, cbsd_indoor)

    elif dist_km > 0.1 and dist_km < 1:  # Use E-Hata Median Basic Prop Loss
        fsl_100m = CalcFreeSpaceLoss(0.1, freq_mhz, height_cbsd, height_rx)
        median_basic_loss = ehata.MedianBasicPropLoss(freq_mhz, height_cbsd,
                                                      height_rx, 1,
                                                      region_code)
        alpha = 1. + math.log10(dist_km)
        db_loss = fsl_100m + alpha * (median_basic_loss - fsl_100m)

        # TODO: validate the following approach with WinnForum participants:
        # Weight the offset as well from 0 (100m) to 1.0 (1km).
        if reliability == -1:
            db_loss += alpha * offset_median_to_mean
        return _BuildOutput(db_loss, incidence_angles, internals,
                            HybridMode.EHATA_FSL_INTERP, cbsd_indoor)

    elif dist_km >= 1 and dist_km <= 80:  # Use best of E-Hata / ITM
        ehata_loss_med = ehata.ExtendedHata(its_elev, freq_mhz, height_cbsd,
                                            height_rx, region_code)
        if reliability == 0.5:
            ehata_loss = ehata_loss_med
            itm_loss_med = db_loss_itm
        else:
            ehata_loss = ehata_loss_med + offset_median_to_mean
            itm_loss_med = wf_itm.CalcItmPropagationLoss(
                lat_cbsd, lon_cbsd, height_cbsd, lat_rx, lon_rx, height_rx,
                False, 0.5, freq_mhz, its_elev).db_loss

        if itm_loss_med >= ehata_loss_med:
            return _BuildOutput(db_loss_itm, incidence_angles, internals,
                                HybridMode.ITM_DOMINANT, cbsd_indoor)
        else:
            return _BuildOutput(ehata_loss, incidence_angles, internals,
                                HybridMode.EHATA_DOMINANT, cbsd_indoor)

    elif dist_km > 80:  # Use the ITM with correction from E-Hata @ 80km
        # Calculate the ITM median and eHata median losses at 80km
        bearing = incidence_angles.hor_cbsd

        lat_80km, lon_80km, _ = vincenty.GeodesicPoint(lat_cbsd, lon_cbsd, 80.,
                                                       bearing)
        its_elev_80km = drive.terrain_driver.TerrainProfile(
            lat_cbsd,
            lon_cbsd,
            lat_80km,
            lon_80km,
            target_res_meter=30.,
            do_interp=True,
            max_points=1501)
        ehata_loss_80km = ehata.ExtendedHata(its_elev_80km, freq_mhz,
                                             height_cbsd, height_rx,
                                             region_code)
        itm_loss_80km = wf_itm.CalcItmPropagationLoss(lat_cbsd, lon_cbsd,
                                                      height_cbsd, lat_80km,
                                                      lon_80km, height_rx,
                                                      False, 0.5, freq_mhz,
                                                      its_elev_80km).db_loss

        J = max(ehata_loss_80km - itm_loss_80km, 0)
        db_loss = db_loss_itm + J

        return _BuildOutput(db_loss, incidence_angles, internals,
                            HybridMode.ITM_CORRECTED, cbsd_indoor)