<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def apply_hds_obs(hds_file):
 """ process a modflow head save file. A companion function to setup_hds_obs that is called during the forward run process Parameters hds_file : str a modflow head save filename. if hds_file ends with 'ucn', then the file is treated as a UcnFile type. Note ---- requires flopy writes <hds_file>.dat expects <hds_file>.dat.ins to exist uses pyemu.pst_utils.parse_ins_file to get observation names """


    try:
        import flopy
    except Exception as e:
        raise Exception("apply_hds_obs(): error importing flopy: {0}".\
                        format(str(e)))
    from .. import pst_utils
    assert os.path.exists(hds_file)
    out_file = hds_file+".dat"
    ins_file = out_file + ".ins"
    assert os.path.exists(ins_file)
    df = pd.DataFrame({"obsnme":pst_utils.parse_ins_file(ins_file)})
    df.index = df.obsnme

    # populate metdata
    items = ["k","i","j","kper"]
    for i,item in enumerate(items):
        df.loc[:,item] = df.obsnme.apply(lambda x: int(x.split('_')[i+1]))

    if hds_file.lower().endswith('ucn'):
        hds = flopy.utils.UcnFile(hds_file)
    else:
        hds = flopy.utils.HeadFile(hds_file)
    kpers = df.kper.unique()
    df.loc[:,"obsval"] = np.NaN
    for kper in kpers:
        kstp = last_kstp_from_kper(hds,kper)
        data = hds.get_data(kstpkper=(kstp,kper))
        #jwhite 15jan2018 fix for really large values that are getting some
        #trash added to them...
        data[np.isnan(data)] = 0.0
        data[data>1.0e+20] = 1.0e+20
        data[data<-1.0e+20] = -1.0e+20
        df_kper = df.loc[df.kper==kper,:]
        df.loc[df_kper.index,"obsval"] = data[df_kper.k,df_kper.i,df_kper.j]
    assert df.dropna().shape[0] == df.shape[0]
    df.loc[:,["obsnme","obsval"]].to_csv(out_file,index=False,sep=" ")