def configure_workspace(verbosity=0): """Configures the ARTS application. Args: verbosity: ARTS verbosity level. Returns: A Workspace object. """ workspace = Workspace(verbosity=0) for name in ["general", "continua", "agendas"]: workspace.execute_controlfile(join("general", "{}.arts".format(name))) workspace.verbositySetScreen(workspace.verbosity, verbosity) workspace.jacobianOff() workspace.Copy(workspace.abs_xsec_agenda, workspace.abs_xsec_agenda__noCIA) workspace.AtmosphereSet1D() return workspace
class _ARTS: def __init__(self, ws=None, threads=None, nstreams=4, scale_vmr=True, verbosity=0): """Initialize a wrapper for an ARTS workspace. Parameters: ws (pyarts.workspace.Workspace): An ARTS workspace. threads (int): Number of threads to use. Default is all available threads. nstreams (int): Number of viewing angles to base the radiative flux calculation on. scale_vmr (bool): Control whether dry volume mixing ratios are scaled with the water-vapor concentration (default is `False.`) verbosity (int): Control the ARTS verbosity from 0 (quiet) to 2. """ from pyarts.workspace import Workspace, arts_agenda self.nstreams = nstreams self.scale_vmr = scale_vmr if ws is None: self.ws = Workspace(verbosity=verbosity) self.ws.execute_controlfile("general/general.arts") self.ws.execute_controlfile("general/continua.arts") self.ws.execute_controlfile("general/agendas.arts") self.ws.execute_controlfile("general/planet_earth.arts") # Agenda settings self.ws.Copy(self.ws.abs_xsec_agenda, self.ws.abs_xsec_agenda__noCIA) self.ws.Copy(self.ws.iy_main_agenda, self.ws.iy_main_agenda__Emission) self.ws.Copy(self.ws.iy_space_agenda, self.ws.iy_space_agenda__CosmicBackground) self.ws.Copy(self.ws.iy_surface_agenda, self.ws.iy_surface_agenda__UseSurfaceRtprop) self.ws.Copy( self.ws.propmat_clearsky_agenda, self.ws.propmat_clearsky_agenda__LookUpTable, ) self.ws.Copy(self.ws.ppath_agenda, self.ws.ppath_agenda__FollowSensorLosPath) self.ws.Copy(self.ws.ppath_step_agenda, self.ws.ppath_step_agenda__GeometricPath) @arts_agenda def p_eq_agenda(workspace): workspace.water_p_eq_fieldMK05() self.ws.Copy(self.ws.water_p_eq_agenda, p_eq_agenda) @arts_agenda def cloudbox_agenda(workspace): workspace.iyInterpCloudboxField() self.ws.Copy(self.ws.iy_cloudbox_agenda, cloudbox_agenda) # Number of Stokes components to be computed self.ws.IndexSet(self.ws.stokes_dim, 1) self.ws.jacobianOff() # No jacobian calculation self.ws.cloudboxOff() # Clearsky = No scattering # Set Absorption Species self.ws.abs_speciesSet(species=[ "O2, O2-CIAfunCKDMT100", "H2O, H2O-SelfContCKDMT252, H2O-ForeignContCKDMT252", "O3", "CO2, CO2-CKDMT252", "N2, N2-CIAfunCKDMT252, N2-CIArotCKDMT252", "N2O", "CH4", "CO", ]) # Surface handling self.ws.VectorSetConstant(self.ws.surface_scalar_reflectivity, 1, 0.0) self.ws.Copy( self.ws.surface_rtprop_agenda, self.ws. surface_rtprop_agenda__Specular_NoPol_ReflFix_SurfTFromt_surface, ) # Read lookup table abs_lookup = os.getenv("KONRAD_LOOKUP_TABLE", join(dirname(__file__), "data/abs_lookup.xml")) if not isfile(abs_lookup): raise FileNotFoundError( "Could not find ARTS absorption lookup table.\n" "To perform ARTS calculations you have to download the lookup " "table at:\n\n https://doi.org/10.5281/zenodo.3885410\n\n" "Afterwards, use the following environment variable to tell " "konrad where to find it:\n\n" " $ export KONRAD_LOOKUP_TABLE='/path/to/abs_lookup.xml'") self.ws.ReadXML(self.ws.abs_lookup, abs_lookup) self.ws.f_gridFromGasAbsLookup() self.ws.abs_lookupAdapt() # Sensor settings self.ws.sensorOff() # No sensor properties # Atmosphere self.ws.AtmosphereSet1D() # Set number of OMP threads if threads is not None: self.ws.SetNumberOfThreads(threads) def calc_lookup_table(self, filename=None, fnum=2**15, wavenumber=None): """Calculate an absorption lookup table. The lookup table is constructed to cover surface temperatures between 200 and 400 K, and water vapor mixing ratio up to 40%. The frequency grid covers the whole outgoing longwave spectrum from 10 to 3,250 cm^-1. References: An absorption lookup table can be found at https://doi.org/10.5281/zenodo.3885410 Parameters: filename (str): (Optional) path to an ARTS XML file to store the lookup table. fnum (int): Number of frequencies in frequency grid. Ignored if `wavenumber` is set. wavenumber (ndarray): Wavenumber grid [m-1]. """ # Create a frequency grid if wavenumber is None: wavenumber = np.linspace(10e2, 3_250e2, fnum) self.ws.f_grid = ty.physics.wavenumber2frequency(wavenumber) # Read line catagloge and create absorption lines. self.ws.ReadSplitARTSCAT( abs_lines=self.ws.abs_lines, abs_species=self.ws.abs_species, basename="hitran_split_artscat5/", fmin=0.0, fmax=1e99, globalquantumnumbers="", localquantumnumbers="", ignore_missing=0, ) # Set line shape and cut off. self.ws.abs_linesSetLineShapeType(self.ws.abs_lines, "VP") self.ws.abs_linesSetNormalization(self.ws.abs_lines, "VVH") self.ws.abs_linesSetCutoff(self.ws.abs_lines, "ByLine", 750e9) self.ws.abs_lines_per_speciesCreateFromLines() self.ws.abs_lines_per_speciesCompact() # Create a standard atmosphere p_grid = get_quadratic_pgrid(1_200e2, 0.5, 80) atmosphere = Atmosphere(p_grid) atmosphere["T"][ -1, :] = 300.0 + 5.0 * np.log(atmosphere["plev"] / 1000e2) atmosphere.tracegases_rcemip() atmosphere["O2"][:] = 0.2095 atmosphere["CO2"][:] = 1.5 * 348e-6 h2o = 0.01 * (p_grid / 1000e2)**0.2 atmosphere["H2O"][:] = h2o[:-1] # Convert the konrad atmosphere into an ARTS atm_fields_compact. atm_fields_compact = atmosphere.to_atm_fields_compact() self.ws.atm_fields_compact = atm_fields_compact self.ws.atm_fields_compactAddConstant( atm_fields_compact=self.ws.atm_fields_compact, name="abs_species-N2", value=0.7808, condensibles=["abs_species-H2O"], ) # Setup the lookup table calculation self.ws.AtmFieldsAndParticleBulkPropFieldFromCompact() self.ws.vmr_field.value = self.ws.vmr_field.value.clip(min=0.0) self.ws.atmfields_checkedCalc() self.ws.abs_lookupSetup(p_step=1.0) # Do not refine p_grid self.ws.abs_t_pert = np.arange(-160, 61, 20) nls_idx = [ i for i, tag in enumerate(self.ws.abs_species.value) if "H2O" in tag[0] ] self.ws.abs_speciesSet( abs_species=self.ws.abs_nls, species=[", ".join(self.ws.abs_species.value[nls_idx[0]])], ) self.ws.abs_nls_pert = np.array( [10**x for x in [-9, -7, -5, -3, -1, 0, 0.5, 1, 1.5, 2]]) # Run checks self.ws.abs_xsec_agenda_checkedCalc() self.ws.lbl_checkedCalc() # Calculate actual lookup table. self.ws.abs_lookupCalc() if filename is not None: self.ws.WriteXML("binary", self.ws.abs_lookup, filename) def set_atmospheric_state(self, atmosphere, t_surface): """Set and check the atmospheric fields.""" import pyarts atm_fields_compact = atmosphere.to_atm_fields_compact() # Scale dry-air VMRs with H2O and CO2 content. if self.scale_vmr: variable_vmrs = (atm_fields_compact.get("abs_species-H2O")[0] + atm_fields_compact.get("abs_species-CO2")[0]) else: t3_shape = atm_fields_compact.get("abs_species-H2O")[0].shape variable_vmrs = np.zeros(t3_shape) for species in atm_fields_compact.grids[0]: if (species.startswith("abs_species-") and "H2O" not in species and "CO2" not in species): atm_fields_compact.scale(species, 1 - variable_vmrs) # Compute the N2 VMR as a residual of the full atmosphere composition. n2 = pyarts.types.GriddedField3( grids=atm_fields_compact.grids[1:], data=0.7808 * (1 - variable_vmrs), ) self.ws.atm_fields_compact = atm_fields_compact self.ws.atm_fields_compactAddSpecies( atm_fields_compact=self.ws.atm_fields_compact, name="abs_species-N2", value=n2, ) self.ws.AtmFieldsAndParticleBulkPropFieldFromCompact() self.ws.vmr_field = self.ws.vmr_field.value.clip(min=0) # Surface & TOA # Add pressure layers to the surface and top-of-the-atmosphere to # ensure consistent atmosphere boundaries between ARTS and RRTMG. self.ws.t_surface = np.array([[t_surface]]) self.ws.z_surface = np.array([[0.0]]) self.ws.z_field.value[0, 0, 0] = 0.0 # Perform configuration and atmosphere checks self.ws.atmfields_checkedCalc() self.ws.propmat_clearsky_agenda_checkedCalc() self.ws.atmgeom_checkedCalc() self.ws.cloudbox_checkedCalc() def calc_spectral_irradiance_field(self, atmosphere, t_surface): """Calculate the spectral irradiance field.""" self.set_atmospheric_state(atmosphere, t_surface) # get the zenith angle grid and the integrations weights self.ws.AngularGridsSetFluxCalc(N_za_grid=self.nstreams, N_aa_grid=1, za_grid_type="double_gauss") # calculate intensity field self.ws.Tensor3Create("trans_field") self.ws.spectral_radiance_fieldClearskyPlaneParallel( trans_field=self.ws.trans_field, use_parallel_za=0, ) self.ws.spectral_irradiance_fieldFromSpectralRadianceField() return ( self.ws.f_grid.value.copy(), self.ws.p_grid.value.copy(), self.ws.spectral_irradiance_field.value.copy(), self.ws.trans_field.value[:, 1:, 0].copy().prod(axis=1), ) def calc_optical_thickness(self, atmosphere, t_surface): """Calculate the spectral irradiance field.""" self.set_atmospheric_state(atmosphere, t_surface) self.ws.propmat_clearsky_fieldCalc() tau = np.trapz( y=self.ws.propmat_clearsky_field.value[:, :, 0, 0, :, 0, 0], x=self.ws.z_field.value[:, 0, 0], axis=-1, ) return self.ws.f_grid.value.copy(), tau @staticmethod def integrate_spectral_irradiance(frequency, irradiance): """Integrate the spectral irradiance field over the frequency. Parameters: frequency (ndarray): Frequency [Hz]. irradiance (ndarray): Spectral irradiance [W m^-2 / Hz]. Returns: ndarray, ndarray: Downward flux, upward, flux [W m^-2] """ F = np.trapz(irradiance, frequency, axis=0)[:, 0, 0, :] # Fluxes lw_down = -F[:, 0] lw_up = F[:, 1] return lw_down, lw_up def calc_spectral_olr(self, atmosphere, surface): """Calculate the outgoing longwave radiation as function of wavenumber. Parameters: atmosphere (konrad.atmosphere.Atmosphere): Atmosphere model. surface (konrad.surface.Surface): Surface model. Returns: ndarray: Outgoing longwave radiation [W m^-2 / cm^-1] """ f, _, irradiance_field, _ = self.calc_spectral_irradiance_field( atmosphere=atmosphere, t_surface=surface["temperature"][0]) return f, irradiance_field[:, -1, 0, 0, 1]
class TestAgendas: """ Tests the calling of ARTS workspace methods. """ def setup_method(self): """ This ensures a new Workspace for every test. """ self.ws = Workspace(verbosity = 0) self.setup_workspace() def setup_workspace(self): self.ws.execute_controlfile("artscomponents/clearsky/TestClearSky.arts") def test_assignment(self): """ Test assignment of agendas. """ ws = self.ws ws.ppath_agenda = ppath_agenda def test_include(self): ws = self.ws @arts_agenda def ppath_agenda_inc(ws): INCLUDE(ppath_agenda) ws.ppath_agenda = ppath_agenda_inc def test_execution(self): """ Test definition and execution of agendas. """ self.ws.atmosphere_dim = 1 @arts_agenda def add_1(ws): ws.IndexAdd(ws.atmosphere_dim, ws.atmosphere_dim, 1) add_1.execute(self.ws) assert self.ws.atmosphere_dim.value == 2 add_1.append(add_1) add_1.execute(self.ws) assert self.ws.atmosphere_dim.value == 4 args = [self.ws.atmosphere_dim, self.ws.atmosphere_dim, 1] @arts_agenda def add_2(ws): ws.IndexAdd(*args) add_2.execute(self.ws) assert self.ws.atmosphere_dim.value == 5 def test_callback(self): """ Test callbacks by re-implementing iy_space_agenda in Python and comparing results of yCalc. """ z_ppath = [] ws = self.ws ws.yCalc() y_old = np.copy(ws.y.value) import scipy.constants as c @arts_agenda(allow_callbacks=True) def space_agenda(ws): # Since everything happens in Python we need # to tell ARTS that we are using all in and outputs. ws.Ignore(ws.f_grid) ws.Ignore(ws.rtp_pos) ws.Ignore(ws.rtp_los) ws.Touch(ws.iy) # Temperatures and frequency t = 2.735 f = ws.f_grid.value # Compute radiances c1 = 2.0 * c.h / c.c ** 2 c2 = c.h / c.k b = c1 * f ** 3 / (np.exp(c2 * f / t) - 1.0) # Put into iy vector. ws.iy = np.zeros((f.size, ws.stokes_dim.value)) ws.iy.value[:, 0] = b # Copy ppath_agenda into workspace. ws.iy_space_agenda = space_agenda ws.yCalc() y_new = np.copy(ws.y.value) assert(np.allclose(y_new, y_old)) def test_callback_2(self): """ Test a very complicated Python callback. """ @arts_agenda(allow_callbacks=True) def agenda(ws): """ This agenda sets a workspace variable in a very obscure way. """ class Foo: def __init__(self, ws): self.ws = ws def ooo(self): self.ws.IndexSet(ws.stokes_dim, 42) foo = Foo(ws) ws.IndexSet(ws.stokes_dim, 21) foo.ooo() agenda.execute(self.ws) def test_unknown_wsv(self): """ Ensure that an exception is thrown when an unknown WSV is used inside an agenda. This covers https://github.com/atmtools/arts/issues/368 """ with pytest.raises(ValueError): @arts_agenda def my_agenda(ws): ws.UnknownMethod() def test_starred(self): """ Test expansion of starred expression. """ @arts_agenda def agenda(ws): """ This agenda uses a starred expression. """ ws.IndexSet(*[ws.stokes_dim, 42]) self.ws.stokes_dim = 0 agenda.execute(self.ws) assert self.ws.stokes_dim.value == 42 def test_double_starred(self): """ Test expansion of starred expression. """ @arts_agenda def agenda(ws): """ This agenda uses a starred expression. """ ws.IndexSet(**{"out" : ws.stokes_dim, "value" : 42}) self.ws.stokes_dim = 0 agenda.execute(self.ws) assert self.ws.stokes_dim.value == 42 def test_exception(self): """ Ensure that exception is thrown when a agenda variable is set to an invalid value. """ @arts_agenda(allow_callbacks=True) def abs_xsec_agenda(ws): pass self.ws = pyarts.workspace.Workspace() with pytest.raises(Exception): self.ws.abs_xsec_agenda = abs_xsec_agenda
class TestWorkspace: def setup_method(self): """This ensures a new Workspace for every test.""" self.dir = os.path.dirname(os.path.realpath(__file__)) self.ws = Workspace() self.setup_workspace() def setup_workspace(self): ws = self.ws ws.atmosphere_dim = 1 ws.p_grid = np.linspace(1e5, 1e3, 21) ws.Touch(ws.lat_grid) ws.Touch(ws.lon_grid) ws.f_grid = 183.0e9 * np.ones(1) ws.stokes_dim = 1 ws.sensor_los = 180.0 * np.ones((1, 1)) ws.sensor_pos = 830e3 * np.ones((1, 1)) ws.sensorOff() def test_execute_controlfile(self): dir = os.path.dirname(os.path.realpath(__file__)) test_dir = os.path.join(dir, "test_files") self.ws.WriteXML("ascii", np.array([1.0]), os.path.join(test_dir, "vector.xml")) os.chdir(test_dir) self.ws.execute_controlfile("controlfile.arts") os.remove(os.path.join(test_dir, "vector.xml")) def test_execute_controlfile(self): dir = os.path.dirname(os.path.realpath(__file__)) test_dir = os.path.join(dir, "test_files") self.ws.WriteXML("ascii", np.array([1.0]), os.path.join(test_dir, "vector.xml")) os.chdir(test_dir) agenda = self.ws.execute_controlfile("controlfile.arts") self.ws.foo = "not bar" @arts_agenda def execute(ws): ws.FlagOff(ws.jacobian_do) ws.StringSet(ws.foo, "still not bar") INCLUDE("controlfile.arts") INCLUDE(agenda) self.ws.execute_agenda(execute) assert self.ws.foo.value == "bar" os.remove(os.path.join(test_dir, "vector.xml")) def test_wsv_setattr(self): wsv = self.ws.atmosphere_dim wsv.value = 12 assert self.ws.atmosphere_dim.value == 12 def test_callbacks(self): @arts_agenda def agenda(ws): """ This agenda sets a workspace variable in a very obscure way. """ class Foo: def __init__(self, ws): self.ws = ws def ooo(self): self.ws.IndexSet(ws.stokes_dim, 42) foo = Foo(ws) ws.IndexSet(ws.stokes_dim, 21) foo.ooo() agenda.execute(self.ws) assert self.ws.stokes_dim.value == 42 def test_contiguous_arrays(self): x = np.linspace(0, 1, 256) xf = np.asarray(x, order='F') self.ws.f_grid = xf assert np.array_equal(self.ws.f_grid.value, xf) self.ws.f_grid = x[::2] assert np.array_equal(self.ws.f_grid.value, x[::2]) self.ws.f_grid = np.ascontiguousarray(x[::2]) assert np.array_equal(self.ws.f_grid.value, x[::2]) def test_name_collision(self): self.ws.VectorSetConstant(self.ws.f_grid, 10, 1.) self.ws.VectorCreate("np") f_grid = self.ws.f_grid.value assert np.all(np.isclose(f_grid, np.ones(10)))
class TestMethods: """ Tests the calling of ARTS workspace methods. """ def setup_method(self): """ This ensures a new Workspace for every test. """ self.ws = Workspace(verbosity=0) self.setup_workspace() def setup_workspace(self): self.ws.execute_controlfile( "artscomponents/clearsky/TestClearSky.arts") def test_mixed_arguments(self): """ Check that this raises a syntax error. """ ws = self.ws with pytest.raises(SyntaxError): ws.yCalc(ws.yf, y_f=ws.y_f) def test_unexpected_argument(self): """ Providing a named argument with a name that is not actually an argument of the WSM should raise an error. """ ws = self.ws with pytest.raises(Exception): ws.yCalc(nonsense=ws.y_f) def test_override_output(self): """ Test overriding of output parameters in the two possible ways. """ ws = self.ws y_ref = np.copy(ws.y.value) ws.yf = np.zeros(y_ref.size) ws.yCalc(ws.yf) assert (np.allclose(ws.yf.value, y_ref)) ws.yf = np.zeros(y_ref.size) ws.yCalc(y=ws.yf) assert (np.allclose(ws.yf.value, y_ref)) def test_override_input(self): """ Test overriding of input parameters in the two possible ways. atmgeom_checked WSV is set to zero so that both calculations should fail if input is not overridden. """ ws = self.ws ws.atmgeom_checked = 0 ws.rte_pos = np.array([600e3, 0, 0]) ws.rte_pos2 = np.array([600e3, 0]) ws.rte_los = np.array([180.0, 0]) ws.ppathCalc(ws.ppath, ws.ppath_agenda, ws.ppath_lmax, ws.ppath_lraytrace, 1) ws.ppathCalc(atmgeom_checked=1) def test_generic_input(self): """ Test overriding of generic input in the two possible ways. """ ws = self.ws species = ([ "H2O-SelfContStandardType, H2O-ForeignContStandardType, H2O", "N2-SelfContStandardType", "O3" ]) ws.ArrayOfArrayOfSpeciesTagCreate("abs_species_2") ws.abs_speciesSet(ws.abs_species_2, ws.abs_xsec_agenda_checked, ws.propmat_clearsky_agenda_checked, species) ws.ArrayOfArrayOfSpeciesTagCreate("abs_species_3") ws.abs_speciesSet(abs_species=ws.abs_species_3, species=species) assert (ws.abs_species_2.value == ws.abs_species_3.value) def test_generic_output(self): """ Test overriding of generic input in the two possible ways. """ ws = self.ws tempfile = NamedTemporaryFile() mat = np.ones((2, 2)) ws.sensor_los = np.ones((2, 2)) ws.WriteXML("ascii", ws.sensor_los, tempfile.name) ws.sensor_los = np.zeros((2, 2)) ws.ReadXML(ws.sensor_los, tempfile.name) assert (np.allclose(mat, ws.sensor_los.value)) ws.sensor_los = np.zeros((2, 2)) ws.ReadXML(out=ws.sensor_los, filename=tempfile.name) assert (np.allclose(mat, ws.sensor_los.value)) def test_supergeneric_overload_resolution(self): """ Test resolution of supergeneric methods. """ self.ws.ArrayOfIndexCreate("array_of_index") self.ws.ArrayOfArrayOfIndexCreate("array_of_array_of_index") self.ws.array_of_index = [1, 2, 3] self.ws.Append(self.ws.array_of_array_of_index, self.ws.array_of_index) self.ws.Append(self.ws.array_of_array_of_index, self.ws.array_of_index) def test_supergeneric_overload_failure(self): """ Test expected failure of supergeneric overload resolution. """ with pytest.raises(Exception): self.ws.NumericCreate("numeric_wsv") self.ws.StringCreate("string_wsv") self.ws.Copy(self.ws.string_wsv, self.ws.numeric_wsv) def test_wsm_error(self): """ Test error handling from ARTS WSMs. """ with pytest.raises(Exception): ws.atmgeom_checked = 0 self.ws.yCalc()