def test_df2str_monotone(): """Test the monotonocity enforcement in df2str()""" # Don't touch all-zero columns assert (utils.df2str(pd.DataFrame(data=[0, 0, 0]), digits=2, monotone_column=0) == "0\n0\n0\n") # A constant nonzero column, makes no sense as capillary pressure # but still we ensure it runs in eclipse: assert (utils.df2str(pd.DataFrame(data=[1, 1, 1]), digits=2, monotone_column=0) == "1.00\n0.99\n0.98\n") assert (utils.df2str( pd.DataFrame(data=[1, 1, 1]), digits=2, monotone_column=0, monotone_direction=-1, ) == "1.00\n0.99\n0.98\n") assert (utils.df2str( pd.DataFrame(data=[1, 1, 1]), digits=2, monotone_column=0, monotone_direction="dec", ) == "1.00\n0.99\n0.98\n") assert (utils.df2str( pd.DataFrame(data=[1, 1, 1]), digits=2, monotone_column=0, monotone_direction=1, ) == "1.00\n1.01\n1.02\n") assert (utils.df2str( pd.DataFrame(data=[1, 1, 1]), digits=2, monotone_column=0, monotone_direction="inc", ) == "1.00\n1.01\n1.02\n") with pytest.raises(ValueError): assert (utils.df2str( pd.DataFrame(data=[1, 1, 1]), digits=2, monotone_column=0, monotone_direction="foo", ) == "1.00\n1.01\n1.02\n") assert (utils.df2str( pd.DataFrame(data=[1, 1, 1]), digits=7, monotone_column=0) == "1.0000000\n0.9999999\n0.9999998\n") # Actual data that has occured: dframe = pd.DataFrame( data=[0.0000027, 0.0000026, 0.0000024, 0.0000024, 0.0000017], columns=["pc"]) assert (utils.df2str(dframe, monotone_column="pc") == "0.0000027\n0.0000026\n0.0000024\n0.0000023\n0.0000017\n")
def WOTABLE(self, header=True, dataincommentrow=True): """Return a string for a Nexus WOTABLE""" string = "" if "pc" not in self.table.columns: self.table["pc"] = 0 self.pccomment = "-- Zero capillary pressure\n" if header: string += "WOTABLE\n" string += "SW KRW KROW PC\n" string += "! pyscal: " + str(pyscal.__version__) + "\n" if dataincommentrow: string += self.swcomment.replace("--", "!") string += self.krwcomment.replace("--", "!") string += self.krowcomment.replace("--", "!") if not self.fast: string += "! krw = krow @ sw=%1.5f\n" % self.crosspoint() string += self.pccomment.replace("--", "!") width = 10 string += ( "! " + "SW".ljust(width - 2) + "KRW".ljust(width) + "KROW".ljust(width) + "PC".ljust(width) + "\n" ) string += utils.df2str( self.table[["sw", "krw", "krow", "pc"]], monotone_column="pc", monotone_direction="dec", ) return string
def SWFN(self, header=True, dataincommentrow=True): """Return a SWFN keyword with data to Eclipse""" if not self.selfcheck(mode="SWFN"): # selfcheck will print errors/warnings return "" string = "" if "pc" not in self.table.columns: self.table["pc"] = 0 self.pccomment = "-- Zero capillary pressure\n" if header: string += "SWFN\n" string += utils.comment_formatter(self.tag) string += "-- pyscal: " + str(pyscal.__version__) + "\n" if dataincommentrow: string += self.swcomment string += self.krwcomment if "krow" in self.table.columns and not self.fast: string += "-- krw = krow @ sw=%1.5f\n" % self.crosspoint() string += self.pccomment width = 10 string += ( "-- " + "SW".ljust(width - 3) + "KRW".ljust(width) + "PC".ljust(width) + "\n" ) string += utils.df2str( self.table[["sw", "krw", "pc"]], monotone_column="pc", monotone_direction="dec", ) string += "/\n" # Empty line at the end return string
def SOF3(self, header=True, dataincommentrow=True): """Return a SOF3 string, combining data from the wateroil and gasoil objects. So - the oil saturation ranges from 0 to 1-swl. The saturation points from the WaterOil object is used to generate these """ if self.wateroil is None or self.gasoil is None: logger.error("Both WaterOil and GasOil is needed for SOF3") return "" self.threephaseconsistency() # Copy of the wateroil data: table = pd.DataFrame(self.wateroil.table[["sw", "krow"]]) table["so"] = 1 - table["sw"] # Copy of the gasoil data: gastable = pd.DataFrame(self.gasoil.table[["sg", "krog"]]) gastable["so"] = 1 - gastable["sg"] - self.wateroil.swl # Merge WaterOil and GasOil on oil saturation, interpolate for # missing data (potentially different sg- and sw-grids) sof3table = (pd.concat( [table, gastable], sort=True).set_index("so").sort_index().interpolate( method="slinear").fillna(method="ffill").fillna( method="bfill").reset_index()) sof3table["soint"] = list( map(int, list(map(round, sof3table["so"] * SWINTEGERS)))) sof3table.drop_duplicates("soint", inplace=True) # The 'so' column has been calculated from floating point numbers # and the zero value easily becomes a negative zero, circumvent this: zerorow = np.isclose(sof3table["so"], 0.0) sof3table.loc[zerorow, "so"] = abs(sof3table.loc[zerorow, "so"]) string = "" if header: string += "SOF3\n" wo_tag = utils.comment_formatter(self.wateroil.tag) go_tag = utils.comment_formatter(self.gasoil.tag) if wo_tag != go_tag: string += wo_tag string += go_tag else: # Only print once if they are equal string += wo_tag string += "-- pyscal: " + str(pyscal.__version__) + "\n" if dataincommentrow: string += self.wateroil.swcomment string += self.gasoil.sgcomment string += self.wateroil.krowcomment string += self.gasoil.krogcomment width = 10 string += ("-- " + "SW".ljust(width - 3) + "KROW".ljust(width) + "KROG".ljust(width) + "\n") string += utils.df2str(sof3table[["so", "krow", "krog"]]) string += "/\n" return string
def SWOF(self, header=True, dataincommentrow=True): """ Produce SWOF input for Eclipse reservoir simulator. The columns sw, krw, krow and pc are outputted and formatted accordingly. Meta-information for the tabulated data are printed as Eclipse comments. Args: header (bool): Indicate whether the SWOF string should be emitted. If you have multiple SATNUMs, you should set this to True only for the first (or False for all, and emit the SWOF yourself). Default True dataincommentrow (bool): Wheter metadata should be printed. Defualt True """ if not self.fast and not self.selfcheck(): # selfcheck failed and has issued an error message return "" string = "" if header: string += "SWOF\n" string += utils.comment_formatter(self.tag) string += "-- pyscal: " + str(pyscal.__version__) + "\n" if "pc" not in self.table.columns: self.table["pc"] = 0 self.pccomment = "-- Zero capillary pressure\n" if dataincommentrow: string += self.swcomment string += self.krwcomment string += self.krowcomment if not self.fast: string += "-- krw = krow @ sw=%1.5f\n" % self.crosspoint() string += self.pccomment width = 10 string += ( "-- " + "SW".ljust(width - 3) + "KRW".ljust(width) + "KROW".ljust(width) + "PC".ljust(width) + "\n" ) string += utils.df2str( self.table[["sw", "krw", "krow", "pc"]], monotone_column="pc", monotone_direction="dec", ) string += "/\n" # Empty line at the end return string
def SGOF(self, header=True, dataincommentrow=True): """ Produce SGOF input for Eclipse reservoir simulator. The columns sg, krg, krog and pc are outputted and formatted accordingly. Meta-information for the tabulated data are printed as Eclipse comments. Args: header (bool): Whether the SGOF string should be emitted. If you have multiple satnums, you should have True only for the first (or False for all, and emit the SGOF yourself). Defaults to True. dataincommentrow (bool): Whether metadata should be printed, defaults to True. """ if not self.fast and not self.selfcheck(): # selfcheck() will log error/warning messages return "" string = "" if "pc" not in self.table: self.table["pc"] = 0 self.pccomment = "-- Zero capillary pressure\n" if header: string += "SGOF\n" string += utils.comment_formatter(self.tag) string += "-- pyscal: " + str(pyscal.__version__) + "\n" if dataincommentrow: string += self.sgcomment string += self.krgcomment string += self.krogcomment if not self.fast: string += "-- krg = krog @ sw=%1.5f\n" % self.crosspoint() string += self.pccomment width = 10 string += ("-- " + "SG".ljust(width - 3) + "KRG".ljust(width) + "KROG".ljust(width) + "PC".ljust(width) + "\n") string += utils.df2str( self.table[["sg", "krg", "krog", "pc"]], monotone_column="pc", monotone_direction="inc", ) string += "/\n" return string
def GOTABLE(self, header=True, dataincommentrow=True): """ Produce GOTABLE input for the Nexus reservoir simulator. The columns sg, krg, krog and pc are outputted and formatted accordingly. Meta-information for the tabulated data are printed as Eclipse comments. Args: header: boolean for whether the SGOF string should be emitted. If you have multiple satnums, you should have True only for the first (or False for all, and emit the SGOF yourself). Defaults to True. dataincommentrow: boolean for wheter metadata should be printed, defaults to True. """ string = "" if "pc" not in self.table.columns: self.table["pc"] = 0 self.pccomment = "-- Zero capillary pressure\n" if header: string += "GOTABLE\n" string += "SG KRG KROG PC\n" string += "! pyscal: " + str(pyscal.__version__) + "\n" if dataincommentrow: string += self.sgcomment.replace("--", "!") string += self.krgcomment.replace("--", "!") string += self.krogcomment.replace("--", "!") string += "! krg = krog @ sw=%1.5f\n" % self.crosspoint() string += self.pccomment.replace("--", "!") width = 10 string += ("! " + "SG".ljust(width - 2) + "KRG".ljust(width) + "KROG".ljust(width) + "PC".ljust(width) + "\n") string += utils.df2str( self.table[["sg", "krg", "krog", "pc"]], monotone_column="pc", monotone_direction="inc", ) return string
def test_df2str(): """Test handling of roundoff issues when printing dataframes See also test_gasoil.py::test_roundoff() """ # Easy cases: assert utils.df2str(pd.DataFrame(data=[0.1]), digits=1).strip() == "0.1" assert utils.df2str(pd.DataFrame(data=[0.1]), digits=3).strip() == "0.100" assert (utils.df2str(pd.DataFrame(data=[0.1]), digits=3, roundlevel=3).strip() == "0.100") assert (utils.df2str(pd.DataFrame(data=[0.1]), digits=3, roundlevel=4).strip() == "0.100") assert (utils.df2str(pd.DataFrame(data=[0.1]), digits=3, roundlevel=5).strip() == "0.100") assert (utils.df2str(pd.DataFrame(data=[0.01]), digits=3, roundlevel=2).strip() == "0.010") # Here roundlevel will ruin the result: assert (utils.df2str(pd.DataFrame(data=[0.01]), digits=3, roundlevel=1).strip() == "0.000") # Tricky ones: # This one should be rounded down: assert utils.df2str(pd.DataFrame(data=[0.0034999]), digits=3).strip() == "0.003" # But if we are on the 9999 side due to representation error, the # number probably represents 0.0035 so it should be rounded up assert (utils.df2str(pd.DataFrame(data=[0.003499999999998]), digits=3, roundlevel=5).strip() == "0.004") # If we round to more digits than we have in IEE754, we end up truncating: assert (utils.df2str(pd.DataFrame(data=[0.003499999999998]), digits=3, roundlevel=20).strip() == "0.003" # Wrong ) # If we round straight to out output, we are not getting the chance to correct for # the representation error: assert (utils.df2str(pd.DataFrame(data=[0.003499999999998]), digits=3, roundlevel=3).strip() == "0.003" # Wrong ) # So roundlevel > digits assert (utils.df2str(pd.DataFrame(data=[0.003499999999998]), digits=3, roundlevel=4).strip() == "0.004") # But digits < roundlevel < 15 works: assert (utils.df2str(pd.DataFrame(data=[0.003499999999998]), digits=3, roundlevel=14).strip() == "0.004") assert (utils.df2str(pd.DataFrame(data=[0.003499999999998]), digits=3, roundlevel=15).strip() == "0.003" # Wrong ) # Double rounding is a potential issue, as: assert round(0.0034445, 5) == 0.00344 assert round(round(0.0034445, 6), 5) == 0.00345 # Wrong # So if pd.to_csv would later round instead of truncate, we could be victim # of this, having roundlevel > digits + 1 would avoid that: assert round(round(0.0034445, 7), 5) == 0.00344