smartg 2.0.2__tar.gz → 2.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. {smartg-2.0.2/smartg.egg-info → smartg-2.0.3}/PKG-INFO +6 -1
  2. {smartg-2.0.2 → smartg-2.0.3}/README.md +5 -0
  3. {smartg-2.0.2 → smartg-2.0.3}/pyproject.toml +1 -1
  4. {smartg-2.0.2 → smartg-2.0.3}/smartg/smartg.py +169 -21
  5. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/device.cu +27 -10
  6. smartg-2.0.3/smartg/tests/test_jax_context.py +46 -0
  7. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_smartg.py +173 -1
  8. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_smartg_host.py +12 -0
  9. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_smartg_jax.py +28 -0
  10. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_view.py +20 -0
  11. {smartg-2.0.2 → smartg-2.0.3}/smartg/view.py +7 -0
  12. {smartg-2.0.2 → smartg-2.0.3/smartg.egg-info}/PKG-INFO +6 -1
  13. {smartg-2.0.2 → smartg-2.0.3}/smartg.egg-info/SOURCES.txt +1 -0
  14. {smartg-2.0.2 → smartg-2.0.3}/LICENSE.TXT +0 -0
  15. {smartg-2.0.2 → smartg-2.0.3}/LICENSE_RNG.TXT +0 -0
  16. {smartg-2.0.2 → smartg-2.0.3}/MANIFEST.in +0 -0
  17. {smartg-2.0.2 → smartg-2.0.3}/setup.cfg +0 -0
  18. {smartg-2.0.2 → smartg-2.0.3}/smartg/__init__.py +0 -0
  19. {smartg-2.0.2 → smartg-2.0.3}/smartg/albedo.py +0 -0
  20. {smartg-2.0.2 → smartg-2.0.3}/smartg/atmosphere.py +0 -0
  21. {smartg-2.0.2 → smartg-2.0.3}/smartg/auxdata.py +0 -0
  22. {smartg-2.0.2 → smartg-2.0.3}/smartg/bandset.py +0 -0
  23. {smartg-2.0.2 → smartg-2.0.3}/smartg/cdf.py +0 -0
  24. {smartg-2.0.2 → smartg-2.0.3}/smartg/config.py +0 -0
  25. {smartg-2.0.2 → smartg-2.0.3}/smartg/conftest.py +0 -0
  26. {smartg-2.0.2 → smartg-2.0.3}/smartg/diff.py +0 -0
  27. {smartg-2.0.2 → smartg-2.0.3}/smartg/environ.py +0 -0
  28. {smartg-2.0.2 → smartg-2.0.3}/smartg/grid3d.py +0 -0
  29. {smartg-2.0.2 → smartg-2.0.3}/smartg/histories.py +0 -0
  30. {smartg-2.0.2 → smartg-2.0.3}/smartg/interp.py +0 -0
  31. {smartg-2.0.2 → smartg-2.0.3}/smartg/iprt/__init__.py +0 -0
  32. {smartg-2.0.2 → smartg-2.0.3}/smartg/iprt/common.py +0 -0
  33. {smartg-2.0.2 → smartg-2.0.3}/smartg/iprt/phase3.py +0 -0
  34. {smartg-2.0.2 → smartg-2.0.3}/smartg/iprt/phase_a.py +0 -0
  35. {smartg-2.0.2 → smartg-2.0.3}/smartg/iprt/phase_b.py +0 -0
  36. {smartg-2.0.2 → smartg-2.0.3}/smartg/kdis.py +0 -0
  37. {smartg-2.0.2 → smartg-2.0.3}/smartg/notebooks/demo_notebook.py +0 -0
  38. {smartg-2.0.2 → smartg-2.0.3}/smartg/notebooks/demo_notebook_objects.py +0 -0
  39. {smartg-2.0.2 → smartg-2.0.3}/smartg/notebooks/demo_notebook_photons_histories.py +0 -0
  40. {smartg-2.0.2 → smartg-2.0.3}/smartg/notebooks/validation_smartg_iprt_phase3.py +0 -0
  41. {smartg-2.0.2 → smartg-2.0.3}/smartg/notebooks/validation_smartg_iprt_phase_a.py +0 -0
  42. {smartg-2.0.2 → smartg-2.0.3}/smartg/notebooks/validation_smartg_iprt_phase_b_c2.py +0 -0
  43. {smartg-2.0.2 → smartg-2.0.3}/smartg/objects3d.py +0 -0
  44. {smartg-2.0.2 → smartg-2.0.3}/smartg/phase.py +0 -0
  45. {smartg-2.0.2 → smartg-2.0.3}/smartg/postprocess.py +0 -0
  46. {smartg-2.0.2 → smartg-2.0.3}/smartg/progress.py +0 -0
  47. {smartg-2.0.2 → smartg-2.0.3}/smartg/reptran.py +0 -0
  48. {smartg-2.0.2 → smartg-2.0.3}/smartg/rrs.py +0 -0
  49. {smartg-2.0.2 → smartg-2.0.3}/smartg/sensor.py +0 -0
  50. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/communs.h +0 -0
  51. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/device.h +0 -0
  52. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/geometry.h +0 -0
  53. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/helper_math.h +0 -0
  54. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/MicroURNG.hpp +0 -0
  55. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/ReinterpretCtr.hpp +0 -0
  56. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/aes.h +0 -0
  57. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/array.h +0 -0
  58. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/ars.h +0 -0
  59. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/conventional/Engine.hpp +0 -0
  60. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/conventional/gsl_cbrng.h +0 -0
  61. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/clangfeatures.h +0 -0
  62. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/compilerfeatures.h +0 -0
  63. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/gccfeatures.h +0 -0
  64. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/iccfeatures.h +0 -0
  65. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/msvcfeatures.h +0 -0
  66. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/nvccfeatures.h +0 -0
  67. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/open64features.h +0 -0
  68. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/openclfeatures.h +0 -0
  69. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/pgccfeatures.h +0 -0
  70. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/sse.h +0 -0
  71. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/sunprofeatures.h +0 -0
  72. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/features/xlcfeatures.h +0 -0
  73. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/gsl_microrng.h +0 -0
  74. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/philox.h +0 -0
  75. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/incRNGs/Random123/threefry.h +0 -0
  76. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/kernel2.cu +0 -0
  77. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/kernel2.h +0 -0
  78. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/math_constants.h +0 -0
  79. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/phase_grid.h +0 -0
  80. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/shapes.h +0 -0
  81. {smartg-2.0.2 → smartg-2.0.3}/smartg/src/transform.h +0 -0
  82. {smartg-2.0.2 → smartg-2.0.3}/smartg/surface.py +0 -0
  83. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/__init__.py +0 -0
  84. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/conftest.py +0 -0
  85. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/iprt_checks.py +0 -0
  86. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/logs/.gitkeep +0 -0
  87. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_aerosol.py +0 -0
  88. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_atm3d.py +0 -0
  89. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_auxdata.py +0 -0
  90. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_bandset.py +0 -0
  91. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_cdf.py +0 -0
  92. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_chowdhary_aos.py +0 -0
  93. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_conftest.py +0 -0
  94. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_cuda.py +0 -0
  95. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_environ.py +0 -0
  96. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_histories.py +0 -0
  97. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_iprt_checks.py +0 -0
  98. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_iprt_phase3.py +0 -0
  99. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_iprt_phase_b_c2.py +0 -0
  100. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_iprt_phase_b_c3.py +0 -0
  101. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_kdis.py +0 -0
  102. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_kokhanovsky_natraj.py +0 -0
  103. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_notebook.py +0 -0
  104. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_objects3d.py +0 -0
  105. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_objects3d_geometry.py +0 -0
  106. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_opac.py +0 -0
  107. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_phase_grid.py +0 -0
  108. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_phase_grid_ice.py +0 -0
  109. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_phase_mix.py +0 -0
  110. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_phase_readers.py +0 -0
  111. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_postprocess.py +0 -0
  112. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_profile.py +0 -0
  113. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_progress.py +0 -0
  114. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_quick_iprt_phase_a.py +0 -0
  115. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_reptran.py +0 -0
  116. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_rrs.py +0 -0
  117. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_self_consistency.py +0 -0
  118. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_surface.py +0 -0
  119. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_thermal.py +0 -0
  120. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_truncation.py +0 -0
  121. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_truncation_components.py +0 -0
  122. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_vrs.py +0 -0
  123. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_water.py +0 -0
  124. {smartg-2.0.2 → smartg-2.0.3}/smartg/tests/test_xarray.py +0 -0
  125. {smartg-2.0.2 → smartg-2.0.3}/smartg/truncation.py +0 -0
  126. {smartg-2.0.2 → smartg-2.0.3}/smartg/typing.py +0 -0
  127. {smartg-2.0.2 → smartg-2.0.3}/smartg/vrs.py +0 -0
  128. {smartg-2.0.2 → smartg-2.0.3}/smartg/water.py +0 -0
  129. {smartg-2.0.2 → smartg-2.0.3}/smartg/xarray.py +0 -0
  130. {smartg-2.0.2 → smartg-2.0.3}/smartg.egg-info/dependency_links.txt +0 -0
  131. {smartg-2.0.2 → smartg-2.0.3}/smartg.egg-info/requires.txt +0 -0
  132. {smartg-2.0.2 → smartg-2.0.3}/smartg.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smartg
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: SMART-G (Speed-up Monte Carlo Advanced Radiative Transfer Code using GPU) is a radiative transfer code using a Monte-Carlo technique to simulate the propagation of the polarized light in the atmosphere and/or ocean, and using GPU acceleration.
5
5
  Author-email: Mustapha Moulana <mm@hygeos.com>, Didier Ramon <dr@hygeos.com>, François Steinmetz <fs@hygeos.com>, Dominique Jolivet <dj@hygeos.com>, Mathieu Compiègne <mc@hygeos.com>
6
6
  Project-URL: Source, https://github.com/hygeos/smartg
@@ -326,6 +326,11 @@ environment. Without them it skips itself with a message rather than
326
326
  failing, so run it with `pixi run -e extra pytest
327
327
  smartg/tests/test_smartg_jax.py`.
328
328
 
329
+ `test_jax_context.py` needs JAX on a GPU, in the same environment, and
330
+ a pytest process of its own: `test_smartg_jax.py` keeps JAX on the CPU
331
+ for the whole process, and the test then skips itself. Run it with
332
+ `pixi run -e extra pytest smartg/tests/test_jax_context.py`.
333
+
329
334
  To avoid repeating some pytest arguments, a `pytest.ini` file can be created at the root of the project. The following is an example of the contents of such a file:
330
335
 
331
336
  ```
@@ -275,6 +275,11 @@ environment. Without them it skips itself with a message rather than
275
275
  failing, so run it with `pixi run -e extra pytest
276
276
  smartg/tests/test_smartg_jax.py`.
277
277
 
278
+ `test_jax_context.py` needs JAX on a GPU, in the same environment, and
279
+ a pytest process of its own: `test_smartg_jax.py` keeps JAX on the CPU
280
+ for the whole process, and the test then skips itself. Run it with
281
+ `pixi run -e extra pytest smartg/tests/test_jax_context.py`.
282
+
278
283
  To avoid repeating some pytest arguments, a `pytest.ini` file can be created at the root of the project. The following is an example of the contents of such a file:
279
284
 
280
285
  ```
@@ -4,7 +4,7 @@ requires = ["setuptools>=61"]
4
4
 
5
5
  [project]
6
6
  name = "smartg"
7
- version = "2.0.2"
7
+ version = "2.0.3"
8
8
  description = """
9
9
  SMART-G (Speed-up Monte Carlo Advanced Radiative Transfer Code using GPU) is a radiative \
10
10
  transfer code using a Monte-Carlo technique to simulate the propagation of the polarized \
@@ -32,6 +32,8 @@ reduce_diff
32
32
  Post-process ALIS finite-difference runs into sensitivities.
33
33
  """
34
34
 
35
+ import atexit
36
+ import functools
35
37
  import os
36
38
  import subprocess
37
39
  import tempfile
@@ -402,7 +404,7 @@ class Alis:
402
404
  """
403
405
  The options of the ALIS method, for Smartg.run.
404
406
 
405
- ALIS (Emde et al. 2010) computes the spectrum of a whole band from
407
+ ALIS (Emde et al. 2011) computes the spectrum of a whole band from
406
408
  a single set of photon paths, by weighting each path with the
407
409
  absorption of every wavelength. The Smartg object must have been
408
410
  compiled with alis=True, together with alt_pp=True or pp=False.
@@ -850,6 +852,122 @@ class LocalEstimate:
850
852
  )
851
853
 
852
854
 
855
+ # The CUDA contexts that clear_context destroyed, kept referenced so
856
+ # that no new context takes the place of one of them. The Smartg objects
857
+ # of a process share the context of pycuda.autoinit: once one of them
858
+ # clears it, the others must not make it current again, which crashes
859
+ # the driver.
860
+ _RELEASED_CONTEXTS: list[Any] = []
861
+
862
+
863
+ def _released(ctx: Any) -> bool:
864
+ """Return whether ctx is a context that clear_context destroyed."""
865
+ return any(c is ctx for c in _RELEASED_CONTEXTS)
866
+
867
+
868
+ def _make_current(ctx: Any) -> bool:
869
+ """Make the CUDA context ctx the current one, if it is not.
870
+
871
+ Another library of the process (JAX for instance) may have made its
872
+ own context current, where the kernels of SMART-G are unknown
873
+ ('invalid resource handle'). Returns True if ctx was pushed, to be
874
+ popped by the caller. Raises a RuntimeError if ctx was destroyed by
875
+ the clear_context of another Smartg object that shared it.
876
+ """
877
+ if ctx is None:
878
+ return False
879
+ if _released(ctx):
880
+ raise RuntimeError(
881
+ 'The CUDA context of this Smartg object was destroyed by the '
882
+ 'clear_context of another Smartg object that shared it: '
883
+ 'create a new Smartg object.')
884
+ # PyCUDA only knows the contexts it pushed itself: ask the driver
885
+ if _driver_current_context() == ctx.handle:
886
+ return False
887
+ ctx.push()
888
+ return True
889
+
890
+
891
+ def _autoinit_current_at_exit(sg: 'Smartg') -> None:
892
+ """At exit, make the pycuda.autoinit context of sg current again.
893
+
894
+ The exit handler of pycuda.autoinit pops its context, which fails
895
+ ('cannot pop non-current context') if another library (JAX) made
896
+ its own context current in the meantime.
897
+ """
898
+ import sys
899
+ mod = sys.modules.get('pycuda.autoinit')
900
+ if (sg.ctx is None or _released(sg.ctx) or mod is None
901
+ or getattr(mod, 'context', None) is not sg.ctx):
902
+ return
903
+ # set it current at the driver level: pushing it would leave it
904
+ # twice on the stack of PyCUDA, which aborts at the module cleanup
905
+ if _driver_current_context() == sg.ctx.handle:
906
+ return
907
+ try:
908
+ import ctypes
909
+ for name in ('libcuda.so.1', 'libcuda.so', 'nvcuda.dll'):
910
+ try:
911
+ set_current = ctypes.CDLL(name).cuCtxSetCurrent
912
+ break
913
+ except (OSError, AttributeError):
914
+ continue
915
+ else:
916
+ return
917
+ set_current(ctypes.c_void_p(sg.ctx.handle))
918
+ # an exit handler must not raise: the one of pycuda.autoinit, which
919
+ # runs next, reports a context that is still not current
920
+ except Exception: # noqa: BLE001, S110
921
+ pass
922
+
923
+
924
+ def _driver_current_context() -> int | None:
925
+ """Handle of the current CUDA context of the driver, or None."""
926
+ global _CU_CTX_GET_CURRENT
927
+ if _CU_CTX_GET_CURRENT is None:
928
+ import ctypes
929
+ import ctypes.util
930
+ names = ('libcuda.so.1', 'libcuda.so', 'nvcuda.dll',
931
+ ctypes.util.find_library('cuda'))
932
+ for name in names:
933
+ if not name:
934
+ continue
935
+ try:
936
+ _CU_CTX_GET_CURRENT = ctypes.CDLL(name).cuCtxGetCurrent
937
+ break
938
+ except (OSError, AttributeError):
939
+ continue
940
+ else:
941
+ _CU_CTX_GET_CURRENT = False
942
+ if not _CU_CTX_GET_CURRENT:
943
+ return None
944
+ import ctypes
945
+ handle = ctypes.c_void_p()
946
+ if _CU_CTX_GET_CURRENT(ctypes.byref(handle)) != 0:
947
+ return None
948
+ return handle.value
949
+
950
+
951
+ _CU_CTX_GET_CURRENT = None
952
+
953
+
954
+ def _in_context(method: Callable[..., Any]) -> Callable[..., Any]:
955
+ """Run a method of Smartg with its CUDA context current."""
956
+
957
+ @functools.wraps(method)
958
+ def wrapper(self: 'Smartg', *args: Any, **kwargs: Any) -> Any:
959
+ pushed = _make_current(self.ctx)
960
+ try:
961
+ return method(self, *args, **kwargs)
962
+ finally:
963
+ # run may have popped and released the context itself
964
+ # (autoinit=False, keep_context=False)
965
+ if pushed and self.ctx is not None:
966
+ cuda.Context.pop() # pyright: ignore[reportAttributeAccessIssue]
967
+
968
+ return wrapper
969
+
970
+
853
971
  class Smartg:
854
972
  """Initialize the Smartg object.
855
973
 
@@ -872,7 +990,7 @@ class Smartg:
872
990
  double : bool, optional
873
991
  Accumulate photons table in double precision (default double).
874
992
  alis : bool, optional
875
- Use the ALIS method (Emde et al. 2010) for treating gaseous
993
+ Use the ALIS method (Emde et al. 2011) for treating gaseous
876
994
  absorption and perturbed profile. Needs alt_pp=True or
877
995
  pp=False. The 3D objects are not available with it.
878
996
  back : bool, optional
@@ -906,7 +1024,8 @@ class Smartg:
906
1024
  Include the Sun Induced Fluorescence, an experimental option.
907
1025
  Needs alis=True, together with alt_pp=True or pp=False.
908
1026
  thermal : bool, optional
909
- Still in dev...
1027
+ Still in dev... The forward mode (back=False) needs alt_pp=True
1028
+ in the plane parallel geometry.
910
1029
  rng : str, optional
911
1030
  The pseudo-random number generator to use, only 2 choice:
912
1031
  - PHILOX
@@ -1027,6 +1146,13 @@ class Smartg:
1027
1146
  "geometry (pp=True): the spherical move does not follow "
1028
1147
  "its cells"
1029
1148
  )
1149
+ if thermal and not back and pp and not alt_pp:
1150
+ raise ValueError(
1151
+ "The forward thermal emission (thermal=True, back=False) "
1152
+ "needs alt_pp=True in the plane parallel geometry: the "
1153
+ "fast move does not start the emitted photons at their "
1154
+ "altitude"
1155
+ )
1030
1156
 
1031
1157
  if device is not None:
1032
1158
  env_modif = {'CUDA_DEVICE': str(device)}
@@ -1091,6 +1217,10 @@ class Smartg:
1091
1217
  self.ctx = make_default_context()
1092
1218
 
1093
1219
  self.autoinit = autoinit
1220
+ if autoinit:
1221
+ # registered after the exit handler of pycuda.autoinit, so
1222
+ # run before it: its context must be current to be popped
1223
+ atexit.register(_autoinit_current_at_exit, self)
1094
1224
  self.pp = pp
1095
1225
  self.double = double
1096
1226
  self.alis = alis
@@ -1197,6 +1327,9 @@ class Smartg:
1197
1327
  # 3D Object mode
1198
1328
  options.append('-DOBJ3D')
1199
1329
  options.append('-D' + rng)
1330
+ # CUDA 13 deprecates double4 (warning 1444) for double4_16a and
1331
+ # double4_32a, which older toolkits lack
1332
+ options.append('-diag-suppress=1444')
1200
1333
  # options.append('-lineinfo')
1201
1334
 
1202
1335
  #
@@ -1208,23 +1341,29 @@ class Smartg:
1208
1341
  with open(SRC_DEVICE, encoding='ascii', errors='ignore') as f:
1209
1342
  src_device_content = f.read()
1210
1343
 
1211
- # kernel compilation
1212
- self.mod = SourceModule(
1213
- src_device_content,
1214
- nvcc='nvcc',
1215
- options=options,
1216
- no_extern_c=True,
1217
- cache_dir=str(cache_dir),
1218
- include_dirs=[
1219
- str(DIR_SRC),
1220
- str(DIR_SRC / 'incRNGs' / 'Random123'),
1221
- ],
1222
- )
1344
+ # kernel compilation, in the context of this object (another
1345
+ # library may have made its own context current)
1346
+ pushed = _make_current(self.ctx)
1347
+ try:
1348
+ self.mod = SourceModule(
1349
+ src_device_content,
1350
+ nvcc='nvcc',
1351
+ options=options,
1352
+ no_extern_c=True,
1353
+ cache_dir=str(cache_dir),
1354
+ include_dirs=[
1355
+ str(DIR_SRC),
1356
+ str(DIR_SRC / 'incRNGs' / 'Random123'),
1357
+ ],
1358
+ )
1223
1359
 
1224
- # load the kernel
1225
- self.kernel = self.mod.get_function('launchKernel')
1226
- # self.kernel2 = self.mod.get_function('launchKernel2')
1227
- self.kernel2 = self.mod.get_function('reduce_absorption_gpu')
1360
+ # load the kernel
1361
+ self.kernel = self.mod.get_function('launchKernel')
1362
+ # self.kernel2 = self.mod.get_function('launchKernel2')
1363
+ self.kernel2 = self.mod.get_function('reduce_absorption_gpu')
1364
+ finally:
1365
+ if pushed:
1366
+ cuda.Context.pop() # pyright: ignore[reportAttributeAccessIssue]
1228
1367
 
1229
1368
  #
1230
1369
  # common attributes
@@ -1280,25 +1419,34 @@ class Smartg:
1280
1419
  Notes
1281
1420
  -----
1282
1421
  Once this method has been called, the run method can no longer
1283
- be used: the Smartg object must be reinitialized.
1422
+ be used: the Smartg object must be reinitialized. The Smartg
1423
+ objects with autoinit share one context: once one of them has
1424
+ cleared it, the others can no longer run, and their own
1425
+ clear_context only prints that there is no context to clear.
1284
1426
  """
1285
1427
  try:
1286
1428
  assert self.ctx is not None
1429
+ # another library (JAX) may have made its context current
1430
+ _make_current(self.ctx)
1287
1431
  self.ctx.pop()
1288
1432
  self.ctx.detach()
1433
+ _RELEASED_CONTEXTS.append(self.ctx)
1289
1434
  self.ctx = None
1290
1435
  from pycuda.tools import clear_context_caches
1291
1436
 
1292
1437
  clear_context_caches()
1293
1438
  if self.autoinit:
1294
- # In case of autoinit delete pycuda.autoinit
1439
+ # With autoinit, delete pycuda.autoinit and its exit
1440
+ # handler, which would pop the destroyed context
1295
1441
  import pycuda.autoinit
1296
1442
 
1443
+ atexit.unregister(pycuda.autoinit._finish_up)
1297
1444
  del pycuda.autoinit
1298
1445
  # pycuda's own error classes again, nothing else to do here
1299
1446
  except Exception: # noqa: BLE001
1300
1447
  print("There is no current context to clear.")
1301
1448
 
1449
+ @_in_context
1302
1450
  def run(
1303
1451
  self,
1304
1452
  wavelength: NumericArrayLike | BandSet | Sequence[BandLike],
@@ -3529,7 +3529,9 @@ __device__ void move_pp2(Photon* ph, struct Profile *prof_atm, struct Profile *p
3529
3529
  h_cur = coef_cur * intTime1;
3530
3530
 
3531
3531
  #ifndef ALIS
3532
- h_cur_abs = prof[cell[ph->layer].iabs+ilam].OD_abs * intTime1;
3532
+ // the absorption of the cell: gases of the level, and the
3533
+ // particles of a 3D component (its own optical properties)
3534
+ h_cur_abs = prof[cell[ph->layer].iopt+ilam].OD_abs * intTime1;
3533
3535
  #endif
3534
3536
 
3535
3537
  //
@@ -3552,7 +3554,12 @@ __device__ void move_pp2(Photon* ph, struct Profile *prof_atm, struct Profile *p
3552
3554
  for (int k=0; k<NLOWd; k++)
3553
3555
  {
3554
3556
  coef = get_OD(1,prof[cell[ph->layer].iopt + k*DL*NL]);
3555
- ph->weight_sca[k] *= exp(-(coef-coef_cur)*intTime1);
3557
+ // the absorption of the particles of a 3D component,
3558
+ // smooth in wavelength, goes with the weights at the
3559
+ // nodes; that of the gases with the distances
3560
+ const float dabs = fmaxf(prof[cell[ph->layer].iopt + k*DL*NL].OD_abs
3561
+ - prof[cell[ph->layer].iabs + k*DL*NL].OD_abs, 0.F);
3562
+ ph->weight_sca[k] *= exp(-(coef-coef_cur+dabs)*intTime1);
3556
3563
  }
3557
3564
  #endif
3558
3565
  break;
@@ -3579,7 +3586,9 @@ __device__ void move_pp2(Photon* ph, struct Profile *prof_atm, struct Profile *p
3579
3586
  for (int k=0; k<NLOWd; k++)
3580
3587
  {
3581
3588
  coef = get_OD(1,prof[cell[ph->layer].iopt + k*DL*NL]);
3582
- ph->weight_sca[k] *= __expf(-(coef-coef_cur)*intTime1);
3589
+ const float dabs = fmaxf(prof[cell[ph->layer].iopt + k*DL*NL].OD_abs
3590
+ - prof[cell[ph->layer].iabs + k*DL*NL].OD_abs, 0.F);
3591
+ ph->weight_sca[k] *= __expf(-(coef-coef_cur+dabs)*intTime1);
3583
3592
  }
3584
3593
  #endif
3585
3594
 
@@ -4923,11 +4932,13 @@ __device__ void surfaceWaterRough(Photon* ph, int le,
4923
4932
 
4924
4933
  // A refraction crosses the facet: the incident and the refracted
4925
4934
  // directions both go against its normal, else the photon would
4926
- // reach the facet from behind and there is no transmission
4935
+ // reach the facet from behind and there is no transmission. The
4936
+ // virtual photon is removed rather than given a null weight, which
4937
+ // the counts of photons (N_*) took for a photon
4927
4938
  if (((ph->loc==SURF0P) && (count_level==DOWN0M) ||
4928
4939
  (ph->loc==SURF0M) && (count_level==UP0P)) &&
4929
4940
  ((dot(no, ph->v) >= 0.F) || (dot(no, v) >= 0.F))) {
4930
- ph->weight = 0.;
4941
+ ph->loc=REMOVED;
4931
4942
  return;
4932
4943
  }
4933
4944
 
@@ -7610,14 +7621,16 @@ __device__ void countPhoton(Photon* ph, struct Spectrum *spectrum,
7610
7621
  /*for (int n=0; n<(NATMd); n++){
7611
7622
  wabs += prof_atm[n+1 + il*(NATMd+1)].OD_abs * ph->cdist_atm[n];
7612
7623
  }*/
7624
+ // the 3D move adds the path in a cell to cdist_*[iabs], iabs
7625
+ // counted from 1 at the top: level n+1 goes with cdist_*[n+1]
7613
7626
  for (int n=0; n<(NATM_ABSd); n++){
7614
- wabs += prof_atm[n+1 + il*(NATMd+1)].OD_abs * ph->cdist_atm[n];
7627
+ wabs += prof_atm[n+1 + il*(NATMd+1)].OD_abs * ph->cdist_atm[n+1];
7615
7628
  }
7616
7629
  /*for (int n=0; n<(NOCEd); n++){
7617
7630
  wabs += prof_oc[ n+1 + il*(NOCEd+1)].OD_abs * ph->cdist_oc[n];
7618
7631
  }*/
7619
7632
  for (int n=0; n<(NOCE_ABSd); n++){
7620
- wabs += prof_oc[ n+1 + il*(NOCEd+1)].OD_abs * ph->cdist_oc[n];
7633
+ wabs += prof_oc[ n+1 + il*(NOCEd+1)].OD_abs * ph->cdist_oc[n+1];
7621
7634
  }
7622
7635
  #endif
7623
7636
 
@@ -7702,7 +7715,11 @@ __device__ void countPhoton(Photon* ph, struct Spectrum *spectrum,
7702
7715
  #if ( defined(SPHERIQUE) || defined(ALT_PP) )
7703
7716
  unsigned long long K = NBTHETAd*NBPHId*NSENSORd; /* number of potential output per photon*/
7704
7717
  unsigned long long LL;
7705
- if (HISTd==1 && ((count_level==UPTOA) || (count_level==DOWN0P))) { // Histories stored for absorption computation afterward (only spherical || alt_pp)
7718
+ // Histories stored for absorption computation afterward (only
7719
+ // spherical || alt_pp); a count of zero intensity (a local estimate
7720
+ // that did not reach the level, for instance) contributes nothing
7721
+ if (HISTd==1 && ((count_level==UPTOA) || (count_level==DOWN0P))
7722
+ && (weight*(st.x+st.y) != 0.F)) {
7706
7723
  //int idx = blockIdx.x * blockDim.x + threadIdx.x;
7707
7724
  unsigned long long counter2;
7708
7725
  counter2=atomicAdd(NPhotonsOut, 1);
@@ -7821,10 +7838,10 @@ __device__ void countPhoton(Photon* ph, struct Spectrum *spectrum,
7821
7838
  }
7822
7839
  #else
7823
7840
  for (int n=0; n<(NATM_ABSd); n++){
7824
- wabs_cdist += prof_atm[n+1 + ph->ilam*(NATMd+1)].OD_abs * ph->cdist_atm[n];
7841
+ wabs_cdist += prof_atm[n+1 + ph->ilam*(NATMd+1)].OD_abs * ph->cdist_atm[n+1];
7825
7842
  }
7826
7843
  for (int n=0; n<(NOCE_ABSd); n++){
7827
- wabs_cdist += prof_oc[n+1 + ph->ilam*(NOCEd+1)].OD_abs * ph->cdist_oc[n];
7844
+ wabs_cdist += prof_oc[n+1 + ph->ilam*(NOCEd+1)].OD_abs * ph->cdist_oc[n+1];
7828
7845
  }
7829
7846
  #endif
7830
7847
  wabs_cdist = exp(-wabs_cdist);
@@ -0,0 +1,46 @@
1
+ """Tests of the CUDA context of SMART-G next to the one of JAX.
2
+
3
+ JAX opens a CUDA context of its own on the GPU, where the kernels of
4
+ SMART-G are unknown. These tests need JAX with a GPU (the extra
5
+ environment) and are skipped otherwise. Run them in a pytest process of
6
+ their own: test_smartg_jax.py keeps JAX on the CPU for the whole
7
+ process.
8
+ """
9
+
10
+ import os
11
+
12
+ import numpy as np
13
+ import pytest
14
+
15
+ from smartg.atmosphere import Atm1D
16
+ from smartg.smartg import LocalEstimate, Smartg
17
+
18
+
19
+ def test_run_after_jax_on_gpu() -> None:
20
+ """Check that Smartg runs after JAX has made its context current.
21
+
22
+ The Smartg object compiled before runs again, and a new one
23
+ compiles and runs, with the radiance of the first run. They failed
24
+ with 'invalid resource handle' before 2.0.3.
25
+ """
26
+ # a small share of the GPU memory is enough for this test
27
+ os.environ.setdefault("XLA_PYTHON_CLIENT_PREALLOCATE", "false")
28
+ jax = pytest.importorskip("jax")
29
+ if not any(dv.platform == "gpu" for dv in jax.devices()):
30
+ pytest.skip("JAX has no GPU")
31
+ import jax.numpy as jnp
32
+
33
+ kwargs = {
34
+ "atmosphere": Atm1D("afglms"),
35
+ "le": LocalEstimate(th_deg=[0.0], phi_deg=[0.0]),
36
+ "n_photons": 1e5,
37
+ "seed": 1,
38
+ "progress": False,
39
+ }
40
+ s1 = Smartg()
41
+ i1 = s1.run(550.0, **kwargs)["I_up (TOA)"].values
42
+ float(jnp.sum(jnp.arange(10.0)))
43
+ i2 = s1.run(550.0, **kwargs)["I_up (TOA)"].values
44
+ i3 = Smartg(alt_pp=True).run(550.0, **kwargs)["I_up (TOA)"].values
45
+ np.testing.assert_allclose(i2, i1, rtol=0.05)
46
+ np.testing.assert_allclose(i3, i1, rtol=0.05)
@@ -6,6 +6,10 @@ the outputs and the arguments the run accepts.
6
6
  """
7
7
 
8
8
  import math
9
+ import subprocess
10
+ import sys
11
+ import textwrap
12
+ from pathlib import Path
9
13
  from typing import Any
10
14
 
11
15
  import numpy as np
@@ -17,7 +21,7 @@ from pycuda.compiler import SourceModule
17
21
  import smartg.smartg as smartg_mod
18
22
  from smartg import conftest
19
23
  from smartg.albedo import AlbedoCst, AlbedoMap
20
- from smartg.atmosphere import AerOPAC, Atm1D, Atm3D, Cloud
24
+ from smartg.atmosphere import Aer3D, AerOPAC, Atm1D, Atm3D, Cloud, od2k
21
25
  from smartg.cdf import icdf_2d
22
26
  from smartg.grid3d import Grid3D
23
27
  from smartg.reptran import Reptran, reduce_reptran
@@ -72,6 +76,38 @@ def test_basic(request: pytest.FixtureRequest) -> None:
72
76
  conftest.savefig(request)
73
77
 
74
78
 
79
+ def test_clear_context_leaves_nothing_to_pop_at_exit() -> None:
80
+ """Check that clear_context leaves pycuda no context to pop at exit.
81
+
82
+ Two Smartg objects of one process run in turn, each cleared after
83
+ its run. The exit handler of pycuda.autoinit used to pop the
84
+ contexts that clear_context had destroyed, and printed 'context::pop
85
+ failed' twice. The script runs in a subprocess, whose exit is the
86
+ point.
87
+ """
88
+ script = textwrap.dedent(
89
+ """
90
+ from smartg.atmosphere import Atm1D
91
+ from smartg.smartg import Smartg
92
+
93
+ for alt_pp in (False, True):
94
+ sg = Smartg(alt_pp=alt_pp)
95
+ sg.run(550.0, atmosphere=Atm1D("afglt"), n_photons=1e4,
96
+ progress=False)
97
+ sg.clear_context()
98
+ print("done")
99
+ """
100
+ )
101
+ out = subprocess.run(
102
+ [sys.executable, "-c", script], capture_output=True, text=True,
103
+ timeout=600, check=False,
104
+ )
105
+ assert out.returncode == 0, out.stderr[-2000:]
106
+ assert "done" in out.stdout
107
+ assert "There is no current context" not in out.stdout
108
+ assert "context::pop failed" not in out.stderr
109
+
110
+
75
111
  @pytest.mark.parametrize("wavelength", wavelength_list)
76
112
  def test_atm(sg: Smartg, wavelength: Wavelength) -> None:
77
113
  """Check that a run keeps a wavelength axis only for a list."""
@@ -1130,6 +1166,108 @@ def test_brdf_surface_in_3d_atmosphere() -> None:
1130
1166
  )
1131
1167
 
1132
1168
 
1169
+ def _scene_3d(
1170
+ comp_3d: list[Aer3D] | None = None,
1171
+ mol_abs_1d: NDArray[np.floating] | None = None,
1172
+ ) -> dict[str, Any]:
1173
+ """Return the run arguments of a periodic 3D atmosphere.
1174
+
1175
+ The AFGL US standard atmosphere on 7 layers, 10 km wide with
1176
+ 0.25 km columns along x, at 430 and 440 nm. Six sensors at the
1177
+ TOA above x 4.75-5.25 km look at it in backward mode, through a
1178
+ local estimate at 40 degrees, over a Lambertian ground of albedo
1179
+ 0.3. mol_abs_1d replaces the absorption coefficients of the
1180
+ levels (km-1, levels from the top).
1181
+ """
1182
+ z = np.array([100.0, 60.0, 30.0, 15.0, 8.0, 4.0, 1.0, 0.0])
1183
+ dx = 0.25
1184
+ grid3 = Grid3D(
1185
+ np.arange(0.0, 10.0 + dx / 2, dx),
1186
+ np.array([0.0, 4.0, 6.0, 10.0]),
1187
+ z[::-1],
1188
+ periodic=True,
1189
+ )
1190
+ wavelengths = np.array([430.0, 440.0])
1191
+ p1 = Atm1D("afglus", grid=z).calc(wavelengths)
1192
+ if mol_abs_1d is None:
1193
+ mol_abs_1d = np.asarray(od2k(p1, "OD_g"))
1194
+ atmosphere = Atm3D(
1195
+ atm_1d=Atm1D("afglus"),
1196
+ grid_3d=grid3,
1197
+ comp_3d=comp_3d,
1198
+ mol_sca_1d=np.asarray(od2k(p1, "OD_r")),
1199
+ mol_abs_1d=mol_abs_1d,
1200
+ ).calc(wavelengths)
1201
+ sensors = get_sensors_grid(
1202
+ np.array([4.75, 5.0, 5.25]), np.array([4.0, 6.0]), pos_z=100.0,
1203
+ th_deg=180.0, ph_deg=0.0, loc="ATMOS", grid_3d=grid3,
1204
+ )
1205
+ return {
1206
+ "wavelength": wavelengths,
1207
+ "atmosphere": atmosphere,
1208
+ "surface": LambSurface(alb=AlbedoCst(0.3)),
1209
+ "sensor": sensors,
1210
+ "le": LocalEstimate(
1211
+ th_deg=[40.0], phi_deg=[180.0], count_level=[0]
1212
+ ),
1213
+ "n_photons": 2e5,
1214
+ "seed": 1,
1215
+ "stdev": True,
1216
+ "progress": False,
1217
+ }
1218
+
1219
+
1220
+ def test_3d_alis_absorbs_each_layer_along_its_path() -> None:
1221
+ """Check that 3D ALIS applies each layer's absorption to its path.
1222
+
1223
+ Only the lowest layer (0-1 km) of a horizontally uniform 3D
1224
+ atmosphere absorbs. ALIS, which applies the absorption to the path
1225
+ lengths when the photon is counted, must agree with the
1226
+ Beer-Lambert absorption of the move. Before 2.0.3 the count paired
1227
+ each layer with the path in the layer above: here the 1-4 km layer,
1228
+ three times thicker.
1229
+ """
1230
+ kabs = np.zeros((2, 8)) # (wavelength, level from the top)
1231
+ kabs[:, -1] = 0.5
1232
+ kwargs = _scene_3d(mol_abs_1d=kabs)
1233
+ beer = Smartg(opt3d=True, alt_pp=True, back=True, double=True).run(
1234
+ beer=1, **kwargs
1235
+ )
1236
+ alis = Smartg(
1237
+ opt3d=True, alt_pp=True, alis=True, back=True, double=True
1238
+ ).run(alis_options=Alis(n_low=2), **kwargs)
1239
+ assert np.all(np.abs(_z_scores(alis, beer, "up (TOA)")) < 4)
1240
+
1241
+
1242
+ def test_absorbing_3d_particles_with_beer_and_alis() -> None:
1243
+ """Check that the particles of a 3D component absorb in every mode.
1244
+
1245
+ A plume of desert particles of single scattering albedo 0.8 fills
1246
+ x 4-6 km and z 0-4 km under the sensors. Its absorption must be the
1247
+ same whether the photon weight carries it through the single
1248
+ scattering albedo (beer=0), the Beer-Lambert law (beer=1) or the
1249
+ ALIS weights. Before 2.0.3 the last two read the absorption of the
1250
+ 1D level, without the particles: 1.6 times the beer=0 radiance.
1251
+ """
1252
+ # 1-based (ix, iy, iz): x 4-6 km, the middle y cell, z 0-4 km
1253
+ cells = np.array(
1254
+ [[i, 2, k] for i in range(17, 25) for k in (1, 2)], dtype=np.int32
1255
+ )
1256
+ plume = Aer3D(
1257
+ "desert", w_ref=435.0, ext_ref=np.full(len(cells), 1.0),
1258
+ cell_indices=cells, rh=np.full(len(cells), 50.0), ssa_cst=0.8,
1259
+ )
1260
+ kwargs = _scene_3d(comp_3d=[plume])
1261
+ sg = Smartg(opt3d=True, alt_pp=True, back=True, double=True)
1262
+ ssa = sg.run(beer=0, **kwargs)
1263
+ beer = sg.run(beer=1, **kwargs)
1264
+ alis = Smartg(
1265
+ opt3d=True, alt_pp=True, alis=True, back=True, double=True
1266
+ ).run(alis_options=Alis(n_low=2), **kwargs)
1267
+ assert np.all(np.abs(_z_scores(beer, ssa, "up (TOA)")) < 4)
1268
+ assert np.all(np.abs(_z_scores(alis, ssa, "up (TOA)")) < 4)
1269
+
1270
+
1133
1271
  def _sza_max_run(
1134
1272
  sg: Smartg, n_theta: int, sza_max: float, seed: int
1135
1273
  ) -> xr.Dataset:
@@ -1621,3 +1759,37 @@ def test_sensor_on_either_side_of_the_surface(name: str, alt_pp: bool) -> None:
1621
1759
  sb = bw["I_stdev_up (TOA)"].values.ravel()[0]
1622
1760
  assert b > 0.1
1623
1761
  assert abs(a - b) < 5 * np.hypot(sa, sb), (name, a, sa, b, sb)
1762
+
1763
+
1764
+ def test_clear_context_of_two_objects() -> None:
1765
+ """Check that two Smartg objects sharing a context clear safely.
1766
+
1767
+ Both run in the context of pycuda.autoinit: once the first has
1768
+ cleared it, the second refuses to run and to clear it again, where
1769
+ it crashed the driver, and a new Smartg object runs. The objects
1770
+ live in a subprocess, the context being that of the whole process.
1771
+ """
1772
+ code = textwrap.dedent("""
1773
+ from smartg.atmosphere import Atm1D
1774
+ from smartg.smartg import Smartg
1775
+ a, b = Smartg(), Smartg(alt_pp=True)
1776
+ a.clear_context()
1777
+ b.clear_context()
1778
+ try:
1779
+ b.run(500.0, n_photons=1e3, progress=False)
1780
+ except RuntimeError as err:
1781
+ print("refused:", err)
1782
+ c = Smartg()
1783
+ m = c.run(500.0, atmosphere=Atm1D("afglms"), n_photons=1e4,
1784
+ progress=False)
1785
+ print("run:", float(m["I_up (TOA)"].mean()))
1786
+ c.clear_context()
1787
+ """)
1788
+ proc = subprocess.run(
1789
+ [sys.executable, "-B", "-c", code], capture_output=True, text=True,
1790
+ cwd=Path(__file__).resolve().parents[2], timeout=600, check=False,
1791
+ )
1792
+ assert proc.returncode == 0, proc.stderr[-2000:]
1793
+ assert "There is no current context to clear." in proc.stdout
1794
+ assert "refused: The CUDA context of this Smartg" in proc.stdout
1795
+ assert "run: " in proc.stdout