Skip to content

mors.data

data

FWL_DATA_DIR = Path(os.environ.get('FWL_DATA', platformdirs.user_data_dir('fwl_data'))) module-attribute

_BARAFFE_KEY = 'star.tracks.baraffe_2015' module-attribute

_FWL_IO_FLOOR = '26.7.22' module-attribute

log = logging.getLogger('fwl.' + __name__) module-attribute

project_id = '9u3fb' module-attribute

DownloadEvolutionTracks(fname='')

Download evolution track data

Inputs : - fname (optional) : folder name, "Spada" or "Baraffe" if not provided download both

Baraffe is fetched through fwl-io into the versioned data layout; Spada is a single tarball and comes down the legacy Zenodo/OSF path, untarred in place.

Source code in src/mors/data.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def DownloadEvolutionTracks(fname=''):
    """
    Download evolution track data

    Inputs :
        - fname (optional) :    folder name, "Spada" or "Baraffe"
                                if not provided download both

    Baraffe is fetched through fwl-io into the versioned data layout; Spada is a
    single tarball and comes down the legacy Zenodo/OSF path, untarred in place.
    """

    # If no folder name specified download both Spada and Baraffe
    if not fname:
        folder_list = ('Spada', 'Baraffe')
    elif fname in ('Spada', 'Baraffe'):
        folder_list = [fname]
    else:
        raise ValueError(f'Unrecognised folder name: {fname}')

    if 'Baraffe' in folder_list:
        _fetch_baraffe()
    if 'Spada' in folder_list:
        _download_spada()

    return

GetFWLData()

Get path to FWL data directory on the disk

Source code in src/mors/data.py
177
178
179
180
181
def GetFWLData() -> Path:
    """
    Get path to FWL data directory on the disk
    """
    return Path(FWL_DATA_DIR).absolute()

_baraffe_dataset()

Return the Baraffe dataset declared in the shipped manifest.

An fwl-io older than the manifest schema rejects the shipped manifest as malformed, which points the reader at a file they should not edit, so that case is reported as the version mismatch it is. A manifest error under a current fwl-io is a real error in the shipped file and propagates unchanged.

Source code in src/mors/data.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def _baraffe_dataset():
    """Return the Baraffe dataset declared in the shipped manifest.

    An fwl-io older than the manifest schema rejects the shipped manifest as
    malformed, which points the reader at a file they should not edit, so that
    case is reported as the version mismatch it is. A manifest error under a
    current fwl-io is a real error in the shipped file and propagates unchanged.
    """
    from fwl_io import load_manifest

    try:
        datasets = {ds.key: ds for ds in load_manifest(manifest_path())}
    except ValueError as exc:
        if _fwl_io_derives_the_location():
            raise
        raise RuntimeError(
            f'fwl-io could not read the manifest MORS ships ({exc}); the installed '
            f'fwl-io predates the manifest schema: upgrade to fwl-io>={_FWL_IO_FLOOR}.'
        ) from exc
    return datasets[_BARAFFE_KEY]

_baraffe_fetcher()

Build an fwl-io fetcher for the Baraffe tracks from the manifest pin.

Source code in src/mors/data.py
79
80
81
82
83
84
85
86
87
88
89
def _baraffe_fetcher():
    """Build an fwl-io fetcher for the Baraffe tracks from the manifest pin."""
    from fwl_io import create_fetcher

    ds = _baraffe_dataset()
    return create_fetcher(
        subdir=ds.subdir,
        zenodo=ds.zenodo,
        registry=ds.registry(),
        data_root=GetFWLData(),
    )

_download_spada()

Download and unpack the Spada grid via Zenodo, falling back to OSF.

Source code in src/mors/data.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
def _download_spada():
    """Download and unpack the Spada grid via Zenodo, falling back to OSF."""
    # Create stellar evolution tracks data repository if not existing
    data_dir = GetFWLData() / 'stellar_evolution_tracks'
    data_dir.mkdir(parents=True, exist_ok=True)

    folder = 'Spada'
    folder_dir = data_dir / folder
    if folder_dir.exists():
        return

    # Link with OSF project repository (fallback mirror)
    osf = OSF()
    project = osf.project(project_id)
    storage = project.storage('osfstorage')

    max_tries = 2  # Maximum download attempts, could be a function argument
    log.info(f'Downloading stellar evolution tracks to {data_dir}')
    for i in range(max_tries):
        log.info(f'Attempt {i + 1} of {max_tries}')
        success = False

        try:
            download_zenodo_folder(folder=folder, data_dir=data_dir)
            success = True
        except (subprocess.CalledProcessError, OSError) as e:
            # zenodo_get exits non-zero on failure (CalledProcessError via
            # check=True) or is missing (FileNotFoundError); neither is a
            # RuntimeError, so both must be caught for the fallback to run.
            log.error(f'Zenodo download failed: {e}')
            # A non-zero exit can leave partial files, so clear the whole tree;
            # rmdir would raise on a non-empty directory and mask the failure.
            shutil.rmtree(folder_dir, ignore_errors=True)

        if not success:
            try:
                download_OSF_folder(storage=storage, folders=folder, data_dir=data_dir)
                success = True
            except (RuntimeError, OSError) as e:
                log.error(f'OSF download failed: {e}')

        if success:
            break

        if i < max_tries - 1:
            log.info('Retrying download...')
            sleep(5)  # Wait 5 seconds before retrying
        else:
            log.error('Max retries reached. Download failed.')

    # Unzip Spada evolution tracks (only when the download populated the folder)
    if folder_dir.exists():
        wrk_dir = os.getcwd()
        os.chdir(os.path.join(data_dir, 'Spada'))
        subprocess.call(['tar', 'xvfz', 'fs255_grid.tar.gz'])
        subprocess.call(['rm', '-f', 'fs255_grid.tar.gz'])
        os.chdir(wrk_dir)

    return

_fetch_baraffe()

Fetch the Baraffe tracks through fwl-io (idempotent, hash-verified).

Source code in src/mors/data.py
212
213
214
def _fetch_baraffe():
    """Fetch the Baraffe tracks through fwl-io (idempotent, hash-verified)."""
    _baraffe_fetcher().fetch_all()

_fwl_io_derives_the_location()

Report whether the installed fwl-io derives a dataset location from its key.

The question is whether subdir is still a manifest field, so the answer is read off the dataset fields rather than off how the attribute happens to be implemented. Only positive evidence of the older schema counts: an fwl-io that cannot be introspected is reported as current, so the caller never blames a version mismatch it cannot demonstrate.

Source code in src/mors/data.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def _fwl_io_derives_the_location() -> bool:
    """Report whether the installed fwl-io derives a dataset location from its key.

    The question is whether ``subdir`` is still a manifest field, so the answer
    is read off the dataset fields rather than off how the attribute happens to
    be implemented. Only positive evidence of the older schema counts: an fwl-io
    that cannot be introspected is reported as current, so the caller never
    blames a version mismatch it cannot demonstrate.
    """
    try:
        from fwl_io.manifest import Dataset

        return 'subdir' not in {field.name for field in dataclasses.fields(Dataset)}
    except Exception:
        return True

baraffe_data_dir()

Return the versioned directory that holds the Baraffe track files.

The r<record-id> version segment is resolved by fwl-io from the pinned manifest, so the layout stays a single source of truth. Resolving the path creates the FWL_DATA root if absent (an fwl-io side effect) but does not download the tracks; call DownloadEvolutionTracks("Baraffe") (or mors download baraffe) to populate it.

Source code in src/mors/data.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def baraffe_data_dir() -> Path:
    """Return the versioned directory that holds the Baraffe track files.

    The ``r<record-id>`` version segment is resolved by fwl-io from the pinned
    manifest, so the layout stays a single source of truth. Resolving the path
    creates the ``FWL_DATA`` root if absent (an fwl-io side effect) but does not
    download the tracks; call ``DownloadEvolutionTracks("Baraffe")`` (or
    ``mors download baraffe``) to populate it.
    """
    fetcher = _baraffe_fetcher()
    # A fetcher without a version_dir resolves the bare location, which would
    # put the tracks one directory above where every reader looks for them.
    if getattr(fetcher, 'version_dir', None) is None:
        raise RuntimeError(
            f'fwl-io resolved an unversioned Baraffe directory {fetcher.target_dir}; '
            'the tracks are expected under an r<record-id> version directory.'
        )
    return fetcher.target_dir

download_OSF_folder(*, storage, folders, data_dir)

Download a specific folder in the OSF repository

Inputs : - storage : OSF storage name - folders : folder names to download - data_dir : local repository where data are saved

Source code in src/mors/data.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def download_OSF_folder(*, storage, folders: list[str], data_dir: Path):
    """
    Download a specific folder in the OSF repository

    Inputs :
        - storage : OSF storage name
        - folders : folder names to download
        - data_dir : local repository where data are saved
    """
    for file in storage.files:
        for folder in folders:
            if not file.path[1:].startswith(folder):
                continue
            parts = file.path.split('/')[1:]
            target = Path(data_dir, *parts)
            target.parent.mkdir(parents=True, exist_ok=True)
            log.info(f'Downloading {file.path}...')
            with open(target, 'wb') as f:
                file.write_to(f)
            break

download_zenodo_folder(folder, data_dir)

Download a specific Zenodo record into specified folder

Inputs : - folder : str Folder name to download - folder_dir : Path local repository where data are saved

Source code in src/mors/data.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def download_zenodo_folder(folder: str, data_dir: Path):
    """
    Download a specific Zenodo record into specified folder

    Inputs :
        - folder : str
            Folder name to download
        - folder_dir : Path
            local repository where data are saved
    """

    folder_dir = data_dir / folder
    folder_dir.mkdir(parents=True)
    zenodo_id = get_zenodo_record(folder)
    cmd = ['zenodo_get', zenodo_id, '-o', folder_dir]
    out = os.path.join(GetFWLData(), 'zenodo.log')
    log.debug('    logging to %s' % out)
    with open(out, 'w') as hdl:
        subprocess.run(cmd, check=True, stdout=hdl, stderr=hdl)

get_zenodo_record(folder)

Get Zenodo record ID for a given folder.

Inputs : - folder : str Folder name to get the Zenodo record ID for

Returns : - str | None : Zenodo record ID or None if not found

Source code in src/mors/data.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def get_zenodo_record(folder: str) -> str | None:
    """
    Get Zenodo record ID for a given folder.

    Inputs :
        - folder : str
            Folder name to get the Zenodo record ID for

    Returns :
        - str | None : Zenodo record ID or None if not found
    """
    # Baraffe is fetched through fwl-io and is intentionally absent here.
    # This pin sits outside the manifest, so it is outside everything the
    # nightly cache key hashes, and _download_spada skips the download whenever
    # the folder is already there. Changing the record here therefore keeps
    # serving the cached grid until that cache is cleared by hand.
    zenodo_map = {
        'Spada': '15729101',
    }
    return zenodo_map.get(folder, None)

manifest_path()

Entry-point target: the MORS dataset manifest read by fwl-io.

Source code in src/mors/data.py
52
53
54
def manifest_path() -> Path:
    """Entry-point target: the MORS dataset manifest read by fwl-io."""
    return Path(__file__).parent / 'data' / 'mors_manifest.toml'