Repository: yt-project/yt

Issue:
BUG: RAMSES star_age and particle_birth_time are swapped 
### Bug report

**Bug summary**

Looking at the yt docs (https://yt-project.org/doc/examining/loading_data.html#ramses-data), `('star', particle_birth_time')` should give me the formation time relative to the big bang and `('star', 'star_age')` should give me the number of years a star particle's been around. Instead, it seems the other way around -- I'm getting formation times of 0 Myr and ages equal to the output's current time (see example below)., where I'd expect the smallest birth time to be ~171 Myr and the smallest age to be ~0 Myr.

The ages from ramses/utils/f90/getstarlist.f90 match `particle_birth_time` as currently calculated in yt, mostly to within +/- 1 Myr, with some tail. Indeed, the `return (tsim - t) * t_scale` line in `convert_ramses_ages` is how ages are calculated in getstarlist.f90 (see line 336 in https://bitbucket.org/rteyssie/ramses/src/master/utils/f90/getstarlist.f90).

So, in yt `particle_birth_time = formation_time = time_simu - t`  should actually be `star_age` , and the function `star_age` returns `data.ds.current_time - formation_time = time_tot + time_simu - time_simu + t = time_tot + t = particle_birth_time`. These `time_simu` and `time_tot` parameters are calculated from the `friedman` function from ramses, which is reproduced in yt. `t` is the birth time of the star. I think the fix then is something like: `convert_rames_ages` in frontends/ramses/io.py instead of returning

`(tsim - t) * t_scale`

should return

`data.ds.current_time - ((tsim - t) * t_scale)`

or similar.

**Code for reproduction**
(Sorry, haven't had time to test with the yt data files, can do if you need me to)

```python
import yt
import numpy as np
from yt.data_objects.particle_filters import add_particle_filter

print('yt version:', yt.__version__)

bbox = [[0.4, 0.4, 0.4], [0.6, 0.6, 0.6]]
ds = yt.load('./output_00031', bbox=bbox)
c = ds.arr([50., 50., 50.], 'Mpccm/h')
r = ds.arr(5., 'Mpccm/h')

# Get a sphere
s = ds.sphere(c, r)

# Print current time
print(f'ds.current_time: {ds.current_time.in_units("Myr"):.6f}')
print(f'ds.cosmology.t_from_z(ds.current_redshift): {ds.cosmology.t_from_z(ds.current_redshift).in_units("Myr"):.6f}')

# Check they all have the right family value
assert((s['star', 'particle_family'] == 2).all())

# Try native particle_birth_time from yt-determined stars
t = s['star', 'particle_birth_time'].in_units('Myr')
a = s['star', 'star_age'].in_units('Myr')
print('-- using star family')
print(f'(birth) min: {t.min():.3f}, min nonzero: {t[t>0.].min():.3f}, max: {t.max():.3f}')
print(f'(age)   min: {a.min():.3f}, min nonzero: {a[a>0.].min():.3f}, max: {a.max():.3f}')
print(f'(b+a)   allclose(a+t, ds.current_time): {np.allclose(np.array(a+t), np.array(ds.current_time.in_units("Myr")))}')


# Try explicitly filtering particles a la docs
def filtered_stars(pfilter, data):
    filter = data['io', 'conformal_birth_time'] != 0
    return filter

add_particle_filter('filtered_stars',
                    function=filtered_stars,
                    filtered_type='io',
                    requires=['conformal_birth_time'] )
ds.add_particle_filter('filtered_stars')

# Get a sphere
s = ds.sphere(c, r)

t = s['filtered_stars', 'particle_birth_time'].in_units('Myr')
a = s['filtered_stars', 'star_age'].in_units('Myr')
print('-- using particle filter')
print(f'(birth) min: {t.min():.3f}, min nonzero: {t[t>0.].min():.3f}, max: {t.max():.3f}')
print(f'(age)   min: {a.min():.3f}, min nonzero: {a[a>0.].min():.3f}, max: {a.max():.3f}')
print(f'(b+a)   allclose(a+t, ds.current_time): {np.allclose(np.array(a+t), np.array(ds.current_time.in_units("Myr")))}')
```

**Actual outcome**

<!--The output produced by the above code, which may be a screenshot, console
output, etc.-->

```
yt version: 4.0.2
yt : [INFO     ] 2022-03-24 17:21:00,117 Parameters: current_time              = 4.223387956490154
yt : [INFO     ] 2022-03-24 17:21:00,118 Parameters: domain_dimensions         = [256 256 256]
yt : [INFO     ] 2022-03-24 17:21:00,119 Parameters: domain_left_edge          = [0. 0. 0.]
yt : [INFO     ] 2022-03-24 17:21:00,120 Parameters: domain_right_edge         = [1. 1. 1.]
yt : [INFO     ] 2022-03-24 17:21:00,120 Parameters: cosmological_simulation   = 1
yt : [INFO     ] 2022-03-24 17:21:00,120 Parameters: current_redshift          = 11.690083646079087
yt : [INFO     ] 2022-03-24 17:21:00,120 Parameters: omega_lambda              = 0.685999989509583
yt : [INFO     ] 2022-03-24 17:21:00,120 Parameters: omega_matter              = 0.314000010490417
yt : [INFO     ] 2022-03-24 17:21:00,120 Parameters: omega_radiation           = 0.0
yt : [INFO     ] 2022-03-24 17:21:00,121 Parameters: hubble_constant           = 0.673000030517578
yt : [INFO     ] 2022-03-24 17:22:03,834 Adding particle_type: DM
yt : [INFO     ] 2022-03-24 17:22:03,849 Adding particle_type: star
yt : [INFO     ] 2022-03-24 17:22:03,864 Adding particle_type: cloud
yt : [INFO     ] 2022-03-24 17:22:03,880 Adding particle_type: dust
yt : [INFO     ] 2022-03-24 17:22:03,896 Adding particle_type: star_tracer
yt : [INFO     ] 2022-03-24 17:22:03,912 Adding particle_type: cloud_tracer
yt : [INFO     ] 2022-03-24 17:22:03,928 Adding particle_type: dust_tracer
yt : [INFO     ] 2022-03-24 17:22:03,944 Adding particle_type: gas_tracer
ds.current_time: 381.033802 Myr
ds.cosmology.t_from_z(ds.current_redshift): 382.298844 Myr
-- using star family
(birth) min: 0.000 Myr, min nonzero: 0.002 Myr, max: 210.223 Myr
(age)   min: 170.811 Myr, min nonzero: 170.811 Myr, max: 381.034 Myr
(b+a)   allclose(a+t, ds.current_time): True
-- using particle filter
(birth) min: 0.000 Myr, min nonzero: 0.002 Myr, max: 210.223 Myr
(age)   min: 170.811 Myr, min nonzero: 170.811 Myr, max: 381.034 Myr
(b+a)   allclose(a+t, ds.current_time): True
```

**Expected outcome**
Would expect birth times >~ 171 Myr (from looking at the simulation output logs) and stellar ages from 0 (youngest) to 381 - 171 = 210 Myr (current time - oldest).

**Version Information**
<!--Please specify your platform and versions of the relevant libraries you are
using:-->
  * Operating System: Ubuntu 18.04
  * Python Version: 3.8.5
  * yt version: 4.0.2
  * Other Libraries (if applicable): numpy

python from anaconda distribution, yt installed through pip.



Discussion:

Resolving patch:
diff --git a/yt/frontends/ramses/fields.py b/yt/frontends/ramses/fields.py
--- a/yt/frontends/ramses/fields.py
+++ b/yt/frontends/ramses/fields.py
@@ -5,7 +5,7 @@
 from yt import units
 from yt._typing import KnownFieldsT
 from yt.fields.field_info_container import FieldInfoContainer
-from yt.frontends.ramses.io import convert_ramses_ages
+from yt.frontends.ramses.io import convert_ramses_conformal_time_to_physical_age
 from yt.utilities.cython_fortran_utils import FortranFile
 from yt.utilities.linear_interpolators import BilinearFieldInterpolator
 from yt.utilities.logger import ytLogger as mylog
@@ -162,11 +162,13 @@ def setup_particle_fields(self, ptype):
         def star_age(field, data):
             if data.ds.cosmological_simulation:
                 conformal_age = data[ptype, "conformal_birth_time"]
-                formation_time = convert_ramses_ages(data.ds, conformal_age)
-                formation_time = data.ds.arr(formation_time, "code_time")
+                physical_age = convert_ramses_conformal_time_to_physical_age(
+                    data.ds, conformal_age
+                )
+                return data.ds.arr(physical_age, "code_time")
             else:
                 formation_time = data[ptype, "particle_birth_time"]
-            return data.ds.current_time - formation_time
+                return data.ds.current_time - formation_time
 
         self.add_field(
             (ptype, "star_age"),
diff --git a/yt/frontends/ramses/io.py b/yt/frontends/ramses/io.py
--- a/yt/frontends/ramses/io.py
+++ b/yt/frontends/ramses/io.py
@@ -4,6 +4,8 @@
 
 import numpy as np
 
+from yt._maintenance.deprecation import issue_deprecation_warning
+from yt.frontends.ramses.definitions import VAR_DESC_RE, VERSION_RE
 from yt.utilities.cython_fortran_utils import FortranFile
 from yt.utilities.exceptions import (
     YTFieldTypeNotFound,
@@ -14,30 +16,58 @@
 from yt.utilities.logger import ytLogger as mylog
 from yt.utilities.physical_ratios import cm_per_km, cm_per_mpc
 
-from .definitions import VAR_DESC_RE, VERSION_RE
-
 
 def convert_ramses_ages(ds, conformal_ages):
+    issue_deprecation_warning(
+        msg=(
+            "The `convert_ramses_ages' function is deprecated. It should be replaced "
+            "by the `convert_ramses_conformal_time_to_physical_age' function."
+        ),
+        since="4.0.2",
+        removal="4.0.4",
+    )
+    return convert_ramses_conformal_time_to_physical_age(ds, conformal_ages)
+
+
+def convert_ramses_conformal_time_to_physical_age(
+    ds, conformal_time: np.ndarray
+) -> np.ndarray:
+    """
+    Convert conformal times (as defined in RAMSES) to physical age.
+
+    Arguments
+    ---------
+    ds : RAMSESDataset
+        The RAMSES dataset to use for the conversion
+    conformal_time : np.ndarray
+        The conformal time as read from disk
+
+    Returns
+    -------
+    physical_age : np.ndarray
+        The physical age in code units
+    """
     tf = ds.t_frw
     dtau = ds.dtau
     tauf = ds.tau_frw
-    tsim = ds.time_simu
     h100 = ds.hubble_constant
     nOver2 = ds.n_frw / 2
     unit_t = ds.parameters["unit_t"]
+    tsim = ds.time_simu
+
     t_scale = 1.0 / (h100 * 100 * cm_per_km / cm_per_mpc) / unit_t
 
     # calculate index into lookup table (n_frw elements in
     # lookup table)
-    dage = 1 + (10 * conformal_ages / dtau)
+    dage = 1 + (10 * conformal_time / dtau)
     dage = np.minimum(dage, nOver2 + (dage - nOver2) / 10.0)
     iage = np.array(dage, dtype=np.int32)
 
     # linearly interpolate physical times from tf and tauf lookup
     # tables.
-    t = tf[iage] * (conformal_ages - tauf[iage - 1]) / (tauf[iage] - tauf[iage - 1])
+    t = tf[iage] * (conformal_time - tauf[iage - 1]) / (tauf[iage] - tauf[iage - 1])
     t = t + (
-        tf[iage - 1] * (conformal_ages - tauf[iage]) / (tauf[iage - 1] - tauf[iage])
+        tf[iage - 1] * (conformal_time - tauf[iage]) / (tauf[iage - 1] - tauf[iage])
     )
     return (tsim - t) * t_scale
 
@@ -62,6 +92,7 @@ def _ramses_particle_file_handler(fname, foffsets, data_types, subset, fields, c
     """
     tr = {}
     ds = subset.domain.ds
+    current_time = ds.current_time.in_units("code_time").v
     with FortranFile(fname) as fd:
         # We do *all* conversion into boxlen here.
         # This means that no other conversions need to be applied to convert
@@ -76,11 +107,14 @@ def _ramses_particle_file_handler(fname, foffsets, data_types, subset, fields, c
             if field[1].startswith("particle_position"):
                 np.divide(tr[field], ds["boxlen"], tr[field])
             if ds.cosmological_simulation and field[1] == "particle_birth_time":
-                conformal_age = tr[field]
-                tr[field] = convert_ramses_ages(ds, conformal_age)
+                conformal_time = tr[field]
+                physical_age = convert_ramses_conformal_time_to_physical_age(
+                    ds, conformal_time
+                )
+                tr[field] = current_time - physical_age
                 # arbitrarily set particles with zero conformal_age to zero
                 # particle_age. This corresponds to DM particles.
-                tr[field][conformal_age == 0] = 0
+                tr[field][conformal_time == 0] = 0
     return tr
 
 


Test patch: