a helper function to construct a full prior covariance matrix using
    a mixture of geostastical structures and parameter bounds information.
    The covariance of parameters associated with geostatistical structures is defined
    as a mixture of GeoStruct and bounds.  That is, the GeoStruct is used to construct a
    pyemu.Cov, then the entire pyemu.Cov is scaled by the uncertainty implied by the bounds and
    sigma_range. Sounds complicated...

    Parameters
    ----------
    pst : pyemu.Pst
        a control file (or the name of control file)
    struct_dict : dict
        a python dict of GeoStruct (or structure file), and list of pp tpl files pairs
        If the values in the dict are pd.DataFrames, then they must have an
        'x','y', and 'parnme' column.  If the filename ends in '.csv',
        then a pd.DataFrame is loaded, otherwise a pilot points file is loaded.
    sigma_range : float
        a float representing the number of standard deviations implied by parameter bounds.
        Default is 4.0, which implies 95% confidence parameter bounds.
    par_knowledge_dict : dict
        used to condition on existing knowledge about parameters.  This functionality is
        currently in dev - don't use it.
    verbose : bool
        stdout flag
    Returns
    -------
    Cov : pyemu.Cov
        a covariance matrix that includes all adjustable parameters in the control
        file.

    Example
    -------
    ``>>>import pyemu``

    ``>>>pst = pyemu.Pst("pest.pst")``

    ``>>>sd = {"struct.dat":["hkpp.dat.tpl","vka.dat.tpl"]}``

    ``>>>cov = pyemu.helpers.geostatistical_prior_builder(pst,struct_dict=sd)``

    ``>>>cov.to_ascii("prior.cov")``

def geostatistical_prior_builder(pst, struct_dict,sigma_range=4,
                                 par_knowledge_dict=None,verbose=False):
    """ a helper function to construct a full prior covariance matrix using
    a mixture of geostastical structures and parameter bounds information.
    The covariance of parameters associated with geostatistical structures is defined
    as a mixture of GeoStruct and bounds.  That is, the GeoStruct is used to construct a
    pyemu.Cov, then the entire pyemu.Cov is scaled by the uncertainty implied by the bounds and
    sigma_range. Sounds complicated...

    Parameters
    ----------
    pst : pyemu.Pst
        a control file (or the name of control file)
    struct_dict : dict
        a python dict of GeoStruct (or structure file), and list of pp tpl files pairs
        If the values in the dict are pd.DataFrames, then they must have an
        'x','y', and 'parnme' column.  If the filename ends in '.csv',
        then a pd.DataFrame is loaded, otherwise a pilot points file is loaded.
    sigma_range : float
        a float representing the number of standard deviations implied by parameter bounds.
        Default is 4.0, which implies 95% confidence parameter bounds.
    par_knowledge_dict : dict
        used to condition on existing knowledge about parameters.  This functionality is
        currently in dev - don't use it.
    verbose : bool
        stdout flag
    Returns
    -------
    Cov : pyemu.Cov
        a covariance matrix that includes all adjustable parameters in the control
        file.

    Example
    -------
    ``>>>import pyemu``

    ``>>>pst = pyemu.Pst("pest.pst")``

    ``>>>sd = {"struct.dat":["hkpp.dat.tpl","vka.dat.tpl"]}``

    ``>>>cov = pyemu.helpers.geostatistical_prior_builder(pst,struct_dict=sd)``

    ``>>>cov.to_ascii("prior.cov")``

    """

    if isinstance(pst,str):
        pst = pyemu.Pst(pst)
    assert isinstance(pst,pyemu.Pst),"pst arg must be a Pst instance, not {0}".\
        format(type(pst))
    if verbose: print("building diagonal cov")
    full_cov = pyemu.Cov.from_parameter_data(pst,sigma_range=sigma_range)

    full_cov_dict = {n:float(v) for n,v in zip(full_cov.col_names,full_cov.x)}
    #full_cov = None
    par = pst.parameter_data
    for gs,items in struct_dict.items():
        if verbose: print("processing ",gs)
        if isinstance(gs,str):
            gss = pyemu.geostats.read_struct_file(gs)
            if isinstance(gss,list):
                warnings.warn("using first geostat structure in file {0}".\
                              format(gs),PyemuWarning)
                gs = gss[0]
            else:
                gs = gss
        if not isinstance(items,list):
            items = [items]
        for item in items:
            if isinstance(item,str):
                assert os.path.exists(item),"file {0} not found".\
                    format(item)
                if item.lower().endswith(".tpl"):
                    df = pyemu.pp_utils.pp_tpl_to_dataframe(item)
                elif item.lower.endswith(".csv"):
                    df = pd.read_csv(item)
            else:
                df = item
            for req in ['x','y','parnme']:
                if req not in df.columns:
                    raise Exception("{0} is not in the columns".format(req))
            missing = df.loc[df.parnme.apply(
                    lambda x : x not in par.parnme),"parnme"]
            if len(missing) > 0:
                warnings.warn("the following parameters are not " + \
                              "in the control file: {0}".\
                              format(','.join(missing)),PyemuWarning)
                df = df.loc[df.parnme.apply(lambda x: x not in missing)]
            if "zone" not in df.columns:
                df.loc[:,"zone"] = 1
            zones = df.zone.unique()
            aset = set(pst.adj_par_names)
            for zone in zones:
                df_zone = df.loc[df.zone==zone,:].copy()
                df_zone = df_zone.loc[df_zone.parnme.apply(lambda x: x in aset), :]
                if df_zone.shape[0] == 0:
                    warnings.warn("all parameters in zone {0} tied and/or fixed, skipping...".format(zone),
                                  PyemuWarning)
                    continue
                #df_zone.sort_values(by="parnme",inplace=True)
                df_zone.sort_index(inplace=True)
                if verbose: print("build cov matrix")
                cov = gs.covariance_matrix(df_zone.x,df_zone.y,df_zone.parnme)
                if verbose: print("done")
                # find the variance in the diagonal cov
                if verbose: print("getting diag var cov",df_zone.shape[0])
                #tpl_var = np.diag(full_cov.get(list(df_zone.parnme)).x).max()
                tpl_var = max([full_cov_dict[pn] for pn in df_zone.parnme])
                #if np.std(tpl_var) > 1.0e-6:
                #    warnings.warn("pars have different ranges" +\
                #                  " , using max range as variance for all pars")
                #tpl_var = tpl_var.max()
                if verbose: print("scaling full cov by diag var cov")
                cov *= tpl_var
                if verbose: print("test for inversion")
                try:
                    ci = cov.inv
                except:
                    df_zone.to_csv("prior_builder_crash.csv")
                    raise Exception("error inverting cov {0}".
                                    format(cov.row_names[:3]))

                    if verbose: print('replace in full cov')
                full_cov.replace(cov)
                # d = np.diag(full_cov.x)
                # idx = np.argwhere(d==0.0)
                # for i in idx:
                #     print(full_cov.names[i])

    if par_knowledge_dict is not None:
        full_cov = condition_on_par_knowledge(full_cov,
                    par_knowledge_dict=par_knowledge_dict)
    return full_cov