cr-hydra 0.2.0__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.
@@ -0,0 +1 @@
1
+ include debian/*
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.1
2
+ Name: cr_hydra
3
+ Version: 0.2.0
4
+ Summary: CRHydra - distributed CRTomo computing
5
+ Author: Maximilian Weigand
6
+ Author-email: mweigand@geo.uni-bonn.de
7
+ License: MIT
@@ -0,0 +1,5 @@
1
+ cr-hydra (1.0-0) UNRELEASED; urgency=medium
2
+
3
+ * Initial release.
4
+
5
+ -- mweigand <mweigand@nathan> Thu, 27 Aug 2020 15:51:56 +0200
@@ -0,0 +1 @@
1
+ 9
@@ -0,0 +1,14 @@
1
+ Source: cr-hydra
2
+ Section: python
3
+ Maintainer: Maximilian Weigand <mweigand@geo.uni-bonn.de>
4
+ Architecture: all
5
+ Build-Depends: debhelper (>= 9), dh-python,
6
+ python3-all-dev (>= 3.1.2-8~),
7
+ python-setuptools (>= 0.6b3-1~), python3-setuptools,
8
+ Depends: ${python3:Depends}, ${misc:Depends}
9
+ Description: cr_hydra program suite for distributed CRTomo inversions
10
+
11
+ Package: python3-crhydra
12
+ Architecture: all
13
+ Description: cr_hydra program suite for distributed CRTomo inversions
14
+
@@ -0,0 +1,7 @@
1
+ #! /usr/bin/make -f
2
+
3
+ #export DH_VERBOSE = 1
4
+ export PYBUILD_NAME = cr_hydra
5
+
6
+ %:
7
+ dh $@ --with python3 --buildsystem=pybuild
@@ -0,0 +1,7 @@
1
+ import logging
2
+
3
+ logging.basicConfig(
4
+ level=logging.INFO,
5
+ format='{asctime} - {name} - %{levelname} - {message}',
6
+ style='{',
7
+ )
@@ -0,0 +1,42 @@
1
+ """Manage settings, for now only by means of a config file
2
+ """
3
+ import os
4
+ import configparser
5
+
6
+
7
+ def _find_best_config_file():
8
+ """Look for a configuration file in certain locations of the file system
9
+
10
+ Returns
11
+ -------
12
+ filename : None|str
13
+ None if no config file was found, otherwise an absolute path to the
14
+ configuration file
15
+ """
16
+
17
+ # look at these file paths in increasing priority
18
+ locations = [
19
+ '/etc/crhydra/crhydra.cfg',
20
+ os.getenv('HOME') + os.sep + '.crhydra.cfg',
21
+ os.getcwd() + os.sep + 'crhydra.cfg',
22
+ ]
23
+
24
+ config_file = None
25
+ for filename in locations:
26
+ if os.path.isfile(filename):
27
+ config_file = os.path.abspath(filename)
28
+ return config_file
29
+
30
+
31
+ def get_config():
32
+ """Return the configuration of crhydra, if a config file is available
33
+ """
34
+ config_file = _find_best_config_file()
35
+ if config_file is None:
36
+ raise IOError(
37
+ 'No config file was found! It is required to supply the database' +
38
+ ' credentials!'
39
+ )
40
+ config = configparser.ConfigParser()
41
+ config.read(config_file)
42
+ return config
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env python
2
+ import os
3
+ import glob
4
+
5
+ from setuptools import setup
6
+
7
+ version_long = '0.2.0'
8
+
9
+ # generate entry points
10
+ entry_points = {'console_scripts': []}
11
+ scripts = [os.path.basename(script)[0:-3] for script in glob.glob('src/*.py')]
12
+ for script in scripts:
13
+ print(script)
14
+ entry_points['console_scripts'].append(
15
+ '{0} = {0}:main'.format(script)
16
+ )
17
+
18
+ print(scripts, entry_points)
19
+
20
+ if __name__ == '__main__':
21
+ setup(
22
+ name='cr_hydra',
23
+ version=version_long,
24
+ description='CRHydra - distributed CRTomo computing',
25
+ author='Maximilian Weigand',
26
+ author_email='mweigand@geo.uni-bonn.de',
27
+ license='MIT',
28
+ # url='https://github.com/geophysics-ubonn/reda',
29
+ packages=['cr_hydra', ],
30
+ package_dir={
31
+ '': 'src',
32
+ 'cr_hydra': 'lib/cr_hydra',
33
+ },
34
+ py_modules=scripts,
35
+ entry_points=entry_points,
36
+ install_requires=[
37
+ 'sqlalchemy >= 2.0',
38
+ 'psycopg2-binary',
39
+ 'ipython',
40
+ 'numpy',
41
+ 'pandas',
42
+ ],
43
+ )
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.1
2
+ Name: cr-hydra
3
+ Version: 0.2.0
4
+ Summary: CRHydra - distributed CRTomo computing
5
+ Author: Maximilian Weigand
6
+ Author-email: mweigand@geo.uni-bonn.de
7
+ License: MIT
@@ -0,0 +1,18 @@
1
+ MANIFEST.in
2
+ setup.py
3
+ debian/changelog
4
+ debian/compat
5
+ debian/control
6
+ debian/rules
7
+ lib/cr_hydra/__init__.py
8
+ lib/cr_hydra/settings.py
9
+ src/crh_add.py
10
+ src/crh_get_file.py
11
+ src/crh_retrieve.py
12
+ src/crh_worker.py
13
+ src/cr_hydra.egg-info/PKG-INFO
14
+ src/cr_hydra.egg-info/SOURCES.txt
15
+ src/cr_hydra.egg-info/dependency_links.txt
16
+ src/cr_hydra.egg-info/entry_points.txt
17
+ src/cr_hydra.egg-info/requires.txt
18
+ src/cr_hydra.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ crh_add = crh_add:main
3
+ crh_get_file = crh_get_file:main
4
+ crh_retrieve = crh_retrieve:main
5
+ crh_worker = crh_worker:main
@@ -0,0 +1,5 @@
1
+ ipython
2
+ numpy
3
+ pandas
4
+ psycopg2-binary
5
+ sqlalchemy>=2.0
@@ -0,0 +1,5 @@
1
+ cr_hydra
2
+ crh_add
3
+ crh_get_file
4
+ crh_retrieve
5
+ crh_worker
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env python
2
+ # *-* coding: utf-8 *-*
3
+ """Look for all unfinished tomodirs in the present directory (and
4
+ subdirectories), e called.
5
+ """
6
+ import logging
7
+ import hashlib
8
+ import io
9
+ import shutil
10
+ import uuid
11
+ import os
12
+ import datetime
13
+ import json
14
+ import tarfile
15
+ import platform
16
+
17
+ from sqlalchemy import create_engine
18
+ from sqlalchemy import text
19
+ from optparse import OptionParser
20
+ import IPython
21
+
22
+ from cr_hydra.settings import get_config
23
+ IPython
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def handle_cmd_options():
29
+ parser = OptionParser()
30
+ # parser.add_option(
31
+ # '-t', "--threads",
32
+ # dest="number_threads",
33
+ # type="int",
34
+ # help="number of threads EACH CRMod/CRTomo instance uses. If not " +
35
+ # "set, will be determined automatically",
36
+ # default=None,
37
+ # )
38
+
39
+ # parser.add_option(
40
+ # "-n", "--number",
41
+ # dest="number_processes",
42
+ # help="How many CRMod/CRTomo instances to start in parallel. " +
43
+ # "Default: number of detected CPUs/2",
44
+ # type='int',
45
+ # default=None,
46
+ # )
47
+
48
+ parser.add_option(
49
+ "-f", "--force",
50
+ dest="force_registration",
51
+ help="Assume that no other crh_add is running" +
52
+ " and add not fully initialized simulations",
53
+ action='store_true',
54
+ )
55
+
56
+ (options, args) = parser.parse_args()
57
+ return options
58
+
59
+
60
+ def is_tomodir(subdirectories):
61
+ """provided with the subdirectories of a given directory, check if this is
62
+ a tomodir
63
+ """
64
+ required = (
65
+ 'exe',
66
+ 'config',
67
+ 'rho',
68
+ 'mod',
69
+ 'inv'
70
+ )
71
+ is_tomodir = True
72
+ for subdir in required:
73
+ if subdir not in subdirectories:
74
+ is_tomodir = False
75
+ return is_tomodir
76
+
77
+
78
+ def check_if_needs_modeling(tomodir):
79
+ """check of we need to run CRMod in a given tomodir
80
+ """
81
+ required_files = (
82
+ 'config' + os.sep + 'config.dat',
83
+ 'rho' + os.sep + 'rho.dat',
84
+ 'grid' + os.sep + 'elem.dat',
85
+ 'grid' + os.sep + 'elec.dat',
86
+ 'exe' + os.sep + 'crmod.cfg',
87
+ )
88
+
89
+ not_allowed = (
90
+ 'mod' + os.sep + 'volt.dat',
91
+ )
92
+ needs_modeling = True
93
+ for filename in not_allowed:
94
+ if os.path.isfile(tomodir + os.sep + filename):
95
+ needs_modeling = False
96
+
97
+ for filename in required_files:
98
+ full_file = tomodir + os.sep + filename
99
+ if not os.path.isfile(full_file):
100
+ needs_modeling = False
101
+
102
+ return needs_modeling
103
+
104
+
105
+ def check_if_needs_inversion(tomodir):
106
+ """check of we need to run CRTomo in a given tomodir
107
+
108
+ Parameters
109
+ ----------
110
+ tomodir : str
111
+ Tomodir to check
112
+
113
+ Returns
114
+ -------
115
+ needs_inversion : bool
116
+ True if not finished yet
117
+ """
118
+ required_files = (
119
+ 'grid' + os.sep + 'elem.dat',
120
+ 'grid' + os.sep + 'elec.dat',
121
+ 'exe' + os.sep + 'crtomo.cfg',
122
+ )
123
+
124
+ needs_inversion = True
125
+
126
+ for filename in required_files:
127
+ if not os.path.isfile(tomodir + os.sep + filename):
128
+ needs_inversion = False
129
+
130
+ # check for crmod OR modeling capabilities
131
+ if not os.path.isfile(tomodir + os.sep + 'mod' + os.sep + 'volt.dat'):
132
+ if not check_if_needs_modeling(tomodir):
133
+ needs_inversion = False
134
+
135
+ # check if finished
136
+ inv_ctr_file = tomodir + os.sep + 'inv' + os.sep + 'inv.ctr'
137
+ if os.path.isfile(inv_ctr_file):
138
+ inv_lines = open(inv_ctr_file, 'r').readlines()
139
+ if inv_lines[-1].startswith('***finished***'):
140
+ needs_inversion = False
141
+
142
+ return needs_inversion
143
+
144
+
145
+ def find_unfinished_tomodirs(directory):
146
+ needs_modeling = []
147
+ needs_inversion = []
148
+ for root, dirs, files in os.walk(directory):
149
+ dirs.sort()
150
+ if is_tomodir(dirs):
151
+ logging.info('found tomodir: {}'.format(root))
152
+ if check_if_needs_modeling(root):
153
+ needs_modeling.append(root)
154
+ if check_if_needs_inversion(root):
155
+ needs_inversion.append(root)
156
+
157
+ return sorted(needs_modeling), sorted(needs_inversion)
158
+
159
+
160
+ def _register_tomodir_for_processing(
161
+ tomodir_raw, sim_type, global_settings, cmd_options):
162
+ """
163
+ sim_type: inv|mod
164
+ """
165
+ tomodir = os.path.abspath(tomodir_raw)
166
+ # should be read from the configuration file
167
+ username = 'mweigand'
168
+
169
+ crh_file = tomodir + '.crh'
170
+ if os.path.isfile(crh_file):
171
+ if cmd_options.force_registration:
172
+ logger.info('Checking existing .crh file: {}'.format(tomodir_raw))
173
+ # load crh file
174
+ with open(crh_file, 'r') as fid:
175
+ settings_tmp = json.load(fid)
176
+ if len(
177
+ settings_tmp.keys()
178
+ ) == 1 and 'datetime_init' in settings_tmp:
179
+ logger.info('Simulation was not fully registered.')
180
+ logger.info('Deleting .crh file and registering again')
181
+ os.unlink(crh_file)
182
+ # now proceed
183
+ else:
184
+ return
185
+ else:
186
+ # do nothing - assume another process is working with this tomodir
187
+ return
188
+
189
+ tomodir_id = username + '_' + str(uuid.uuid4())
190
+ archive_file = os.path.abspath(tomodir_id + '.tar.xz')
191
+
192
+ crh_settings = {
193
+ 'datetime_init': '{}'.format(
194
+ datetime.datetime.now(tz=datetime.timezone.utc)
195
+ ),
196
+ }
197
+ # touch the crh file as a crude locking mechanism
198
+ with open(crh_file, 'w') as fid:
199
+ json.dump(crh_settings, fid)
200
+
201
+ # create archive
202
+ pwdx = os.getcwd()
203
+ os.chdir(os.path.dirname(tomodir))
204
+
205
+ data_archive = io.BytesIO()
206
+ with tarfile.open(fileobj=data_archive, mode='w:xz') as tar:
207
+ tar.add(os.path.basename(tomodir), recursive=True)
208
+ os.chdir(pwdx)
209
+
210
+ # prepare data for simulation registration
211
+ crh_settings['source_computer'] = platform.node()
212
+ crh_settings['sim_type'] = sim_type
213
+ crh_settings['crh_file'] = os.path.abspath(crh_file)
214
+ crh_settings['username'] = username
215
+
216
+ print(
217
+ 'Connecting to: {}'.format(
218
+ global_settings['general']['db_credentials']
219
+ )
220
+ )
221
+ engine = create_engine(
222
+ global_settings['general']['db_credentials'],
223
+ echo=False,
224
+ pool_size=1,
225
+ pool_recycle=600,
226
+ )
227
+ connection = engine.connect()
228
+ # create hash of final data
229
+ m = hashlib.sha256()
230
+ data_archive.seek(0)
231
+ m.update(data_archive.read())
232
+ sha256 = m.hexdigest()
233
+
234
+ # upload archive to database
235
+ query = ' '.join((
236
+ 'insert into binary_data (filename, hash, data) values',
237
+ '(:filename, :file_hash, :bin_data) returning index;'
238
+ ))
239
+
240
+ data_archive.seek(0)
241
+ result = connection.execute(
242
+ text(query),
243
+ parameters={
244
+ 'filename': os.path.basename(archive_file),
245
+ 'file_hash': sha256,
246
+ 'bin_data': data_archive.read(),
247
+ },
248
+ )
249
+ connection.commit()
250
+ assert result.rowcount == 1
251
+ file_id = result.fetchone()[0]
252
+ crh_settings['tomodir_unfinished_file'] = file_id
253
+
254
+ query = ' '.join((
255
+ 'insert into inversions (',
256
+ 'username,',
257
+ 'datetime_init,',
258
+ 'tomodir_unfinished_file,',
259
+ 'source_computer,',
260
+ 'sim_type,',
261
+ 'crh_file',
262
+ ') values (',
263
+ ':username,',
264
+ ':datetime_init,',
265
+ ':tomodir_unfinished_file,',
266
+ ':source_computer,',
267
+ ':sim_type,',
268
+ ':crh_file',
269
+ ')',
270
+ 'returning index;'
271
+ ))
272
+ result = connection.execute(
273
+ text(query),
274
+ parameters=crh_settings
275
+ )
276
+ print('rowcount pre commit', result.rowcount)
277
+ connection.commit()
278
+ print('rowcount post commit', result.rowcount)
279
+ assert result.rowcount == 1
280
+ sim_id = result.fetchone()[0]
281
+
282
+ crh_settings['sim_id'] = sim_id
283
+ # update crh file
284
+ with open(crh_file, 'w') as fid:
285
+ json.dump(crh_settings, fid, sort_keys=True, indent=4)
286
+
287
+ # now we are ready for processing
288
+ connection.execute(
289
+ text(
290
+ 'update inversions set '
291
+ 'ready_for_processing=\'t\' where index=:id;'
292
+ ),
293
+ parameters={'id': sim_id},
294
+ )
295
+ connection.commit()
296
+ # delete tomodir
297
+ shutil.rmtree(tomodir)
298
+ logger.info('Added {} to queue'.format(os.path.relpath(tomodir)))
299
+ connection.close()
300
+ engine.dispose()
301
+
302
+
303
+ def main():
304
+
305
+ global_settings = get_config()
306
+ cmd_options = handle_cmd_options()
307
+
308
+ needs_modeling, needs_inversion = find_unfinished_tomodirs('.')
309
+ print('-' * 20)
310
+ print('modeling:', needs_modeling)
311
+ print('inversion:', needs_inversion)
312
+ print('-' * 20)
313
+ for directory in needs_modeling:
314
+ _register_tomodir_for_processing(
315
+ directory, 'mod', global_settings, cmd_options)
316
+ for directory in needs_inversion:
317
+ _register_tomodir_for_processing(
318
+ directory, 'inv', global_settings, cmd_options)
319
+
320
+
321
+ if __name__ == '__main__':
322
+ main()
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python
2
+ import sys
3
+ import json
4
+ import logging
5
+
6
+ from sqlalchemy import create_engine
7
+
8
+ from cr_hydra.settings import get_config
9
+
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format='{asctime} - {name} - %{levelname} - {message}',
13
+ style='{',
14
+ )
15
+
16
+
17
+ def main():
18
+ # settings: should be read from config file/cmd
19
+ global_settings = get_config()
20
+
21
+ engine = create_engine(
22
+ global_settings['general']['db_credentials'],
23
+ echo=False, pool_size=1, pool_recycle=3600,
24
+ )
25
+
26
+ crh_file = sys.argv[1]
27
+
28
+ with open(crh_file, 'r') as fid:
29
+ settings = json.load(fid)
30
+ print(crh_file)
31
+ print(settings)
32
+
33
+ file_id = settings['tomodir_unfinished_file']
34
+
35
+ result = engine.execute(
36
+ 'select data from binary_data where index=%(file_id)s;',
37
+ file_id=file_id
38
+ )
39
+ assert result.rowcount == 1
40
+ data = result.fetchone()[0]
41
+
42
+ outfile = crh_file + '.tar.xz'
43
+ with open(outfile, 'wb') as fid:
44
+ fid.write(data)
45
+
46
+ # check if an inversion is present
47
+ sim_id = settings['sim_id']
48
+ result = engine.execute(
49
+ ' '.join((
50
+ 'select tomodir_finished_file from inversions',
51
+ 'where index=%(sim_id)s and status=\'finished\'',
52
+ 'and downloaded=\'f\'',
53
+ ';'
54
+ )),
55
+ sim_id=sim_id
56
+ )
57
+ assert result.rowcount == 1
58
+ tomodir_finished_file = result.fetchone()[0]
59
+
60
+ result = engine.execute(
61
+ 'select hash, data from binary_data where index=%(data_id)s;',
62
+ data_id=tomodir_finished_file
63
+ )
64
+ assert result.rowcount == 1
65
+ file_hash, binary_data = result.fetchone()
66
+
67
+ outfile = crh_file + '_finished.tar.xz'
68
+ with open(outfile, 'wb') as fid:
69
+ fid.write(binary_data)
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ check the database for inversions
4
+ """
5
+ import logging
6
+ import os
7
+ import hashlib
8
+ import io
9
+ import tarfile
10
+ import json
11
+
12
+ from sqlalchemy import create_engine, text
13
+ import IPython
14
+
15
+ from cr_hydra.settings import get_config
16
+
17
+ IPython
18
+
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='{asctime} - {name} - %{levelname} - {message}',
22
+ style='{',
23
+ )
24
+ logger = logging.getLogger(__name__)
25
+
26
+ global_settings = get_config()
27
+
28
+ engine = create_engine(
29
+ global_settings['general']['db_credentials'],
30
+ echo=False, pool_size=10, pool_recycle=60,
31
+ )
32
+
33
+
34
+ def _is_finished(sim_id, conn):
35
+ """Check if the simulation has been processed.
36
+
37
+ Ignore any rows already locked by other processes (i.e., concurrent runs
38
+ of crh_retrieve)
39
+
40
+ """
41
+ result = conn.execute(
42
+ text(
43
+ ' '.join((
44
+ 'select tomodir_finished_file from inversions',
45
+ 'where index=:sim_id and status=\'finished\'',
46
+ 'and downloaded=\'f\'',
47
+ 'for update',
48
+ 'skip locked',
49
+ ';'
50
+ ))
51
+ ),
52
+ parameters={
53
+ 'sim_id': sim_id,
54
+ }
55
+ )
56
+ if result.rowcount == 1:
57
+ return result.fetchone()[0]
58
+ else:
59
+ result.close()
60
+ return None
61
+
62
+
63
+ def _check_and_retrieve(filename):
64
+ """For a given .crh file, check if the inversion results are ready to be
65
+ downloaded and extract the results
66
+ """
67
+ logger.info('Checking: {}'.format(filename))
68
+ sim_settings = json.load(open(filename, 'r'))
69
+ # ignore any simulation not successfully uploade
70
+ if 'sim_id' not in sim_settings:
71
+ return False
72
+
73
+ conn = engine.connect()
74
+ # transaction = conn.begin_nested()
75
+
76
+ final_data_id = _is_finished(sim_settings['sim_id'], conn)
77
+
78
+ tomodir_name = os.path.basename(filename)[:-4]
79
+ basedir = os.path.abspath(os.path.dirname(filename))
80
+
81
+ pwd = os.getcwd()
82
+
83
+ if final_data_id is not None:
84
+ # we got data
85
+ result = conn.execute(
86
+ text(
87
+ 'select hash, data from binary_data where index=:data_id;'
88
+ ),
89
+ parameters={
90
+ 'data_id': final_data_id,
91
+ },
92
+ )
93
+ assert result.rowcount == 1
94
+ file_hash, binary_data = result.fetchone()
95
+
96
+ # check hash
97
+ m = hashlib.sha256()
98
+ m.update(binary_data)
99
+ assert file_hash == m.hexdigest()
100
+
101
+ logger.info('retrieving and unpacking')
102
+ os.chdir(basedir)
103
+
104
+ # unpack
105
+ fid = io.BytesIO(bytes(binary_data))
106
+ with tarfile.open(fileobj=fid, mode='r') as tar:
107
+ assert os.path.abspath(os.getcwd()) == os.path.abspath(basedir)
108
+
109
+ # make sure there are only files in the archive that go into the
110
+ # tomodir
111
+ for entry in tar.getnames():
112
+ if entry == '.':
113
+ continue
114
+ # strip leading './'
115
+ if entry.startswith('./'):
116
+ entry = entry[2:]
117
+ if not entry.startswith(tomodir_name):
118
+ raise Exception('Content should go into tomodir')
119
+ # now extract
120
+ tar.extractall('.')
121
+ os.chdir(pwd)
122
+ mark_sim_as_downloaded(sim_settings['sim_id'], conn)
123
+ os.unlink(filename)
124
+ # IPython.embed()
125
+ # transaction.commit()
126
+ conn.close()
127
+ engine.dispose()
128
+
129
+
130
+ def mark_sim_as_downloaded(sim_id, conn):
131
+ # mark the simulation as downloaded and delete the files
132
+ result = conn.execute(
133
+ text(
134
+ 'select tomodir_unfinished_file, tomodir_finished_file from ' +
135
+ 'inversions where index=:sim_id;'
136
+ ),
137
+ parameters={
138
+ 'sim_id': sim_id,
139
+ }
140
+ )
141
+ assert result.rowcount == 1
142
+ file_ids = list(result.fetchone())
143
+ result = conn.execute(
144
+ text(
145
+ 'update inversions set ' +
146
+ 'tomodir_unfinished_file=NULL, ' +
147
+ 'tomodir_finished_file=NULL, ' +
148
+ 'downloaded=\'t\' where index=:sim_id;'
149
+ ),
150
+ parameters={
151
+ 'sim_id': sim_id,
152
+ },
153
+ )
154
+ conn.commit()
155
+ assert result.rowcount == 1
156
+ result.close()
157
+ # delete
158
+ result = conn.execute(
159
+ text('delete from binary_data where index in (:id1, :id2);'),
160
+ parameters={
161
+ 'id1': file_ids[0],
162
+ 'id2': file_ids[1],
163
+ },
164
+ )
165
+ conn.commit()
166
+ result.close()
167
+
168
+
169
+ def main():
170
+ for root, dirs, files in os.walk('.'):
171
+ dirs.sort()
172
+ files.sort()
173
+ for filename in files:
174
+ if filename.endswith('.crh'):
175
+ _check_and_retrieve(root + os.sep + filename)
176
+ print(engine.pool.status())
177
+
178
+
179
+ if __name__ == '__main__':
180
+ main()
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env python
2
+ """This cr_hydra worker program - it queries the database and does the actual
3
+ processing of simulations (modelings/inversions).
4
+
5
+ """
6
+ import shutil
7
+ import glob
8
+ import time
9
+ import datetime
10
+ import hashlib
11
+ import io
12
+ from multiprocessing import Process
13
+ import logging
14
+ import tarfile
15
+ import platform
16
+ import os
17
+ import tempfile
18
+ import subprocess
19
+ from optparse import OptionParser
20
+
21
+ import pandas as pd
22
+ from sqlalchemy import create_engine
23
+ import IPython
24
+
25
+ from cr_hydra.settings import get_config
26
+
27
+ IPython
28
+
29
+ logging.basicConfig(
30
+ level=logging.INFO,
31
+ format='{asctime} - {name} - %{levelname} - {message}',
32
+ style='{',
33
+ )
34
+
35
+ # settings: should be read from config file/cmd
36
+ global_settings = get_config()
37
+ # [s]
38
+ query_interval = 15
39
+ # settings end
40
+
41
+ engine = create_engine(
42
+ global_settings['general']['db_credentials'],
43
+ echo=False, pool_size=1, pool_recycle=3600,
44
+ )
45
+ node_name = platform.node()
46
+ logging.info('Identifying as node: {}'.format(node_name))
47
+ results = engine.execute(
48
+ ' '.join((
49
+ 'select nice_level, nr_cpus, nr_threads from node_settings',
50
+ 'where node_name = %(node_name)s;',
51
+ )),
52
+ node_name=node_name
53
+ )
54
+ if results.rowcount == 1:
55
+ logging.info('Found node settings in database for node: {}'.format(
56
+ node_name
57
+ ))
58
+ (nice_level, nr_cpus, number_of_worker_threads) = results.fetchone()
59
+ number_of_workers = int(nr_cpus / number_of_worker_threads)
60
+ else:
61
+ # hopefully sane defaults
62
+ number_of_workers = 2
63
+ number_of_worker_threads = 2
64
+ nice_level = 20
65
+ results.close()
66
+
67
+
68
+ class hydra_worker(Process):
69
+
70
+ def __init__(self, name, cmd_opts, **kwargs):
71
+ Process.__init__(self)
72
+ self.name = name
73
+ self.cmd_opts = cmd_opts
74
+ self.logger = logging.getLogger('crh_worker-' + name)
75
+ self.engine = create_engine(
76
+ global_settings['general']['db_credentials'],
77
+ echo=False, pool_size=10, pool_recycle=3600,
78
+ )
79
+
80
+ def run(self):
81
+ """
82
+
83
+ """
84
+ while(True):
85
+ if self._check_node_active():
86
+ did_sim = self._query_db_and_run_sim()
87
+ if did_sim:
88
+ # try to get the next inversion immediately
89
+ interval = 0
90
+ else:
91
+ if self.cmd_opts.quit_after_empty:
92
+ self.logger.info(
93
+ 'thread quitting due to empty db queue ' +
94
+ '(--quit option)'
95
+ )
96
+ return
97
+ interval = query_interval
98
+ else:
99
+ # sleep 30 seconds
100
+ self.logger.info(
101
+ 'Node was disabled by db, sleeping 30 seconds')
102
+ interval = 30
103
+ time.sleep(interval)
104
+
105
+ def _check_node_active(self):
106
+ """Check if this node should be active
107
+ """
108
+ result = self.engine.execute(
109
+ 'select active from node_settings where node_name=%(node_name)s;',
110
+ node_name=node_name
111
+ )
112
+ if result.rowcount == 1:
113
+ is_active = result.fetchone()[0]
114
+ return is_active
115
+ else:
116
+ result.close()
117
+ # by default we assume that this node is active
118
+ return True
119
+
120
+ def _query_db_and_run_sim(self):
121
+ """
122
+ Returns
123
+ -------
124
+ did_sim: bool
125
+ We want to know if there were simulations to run, e.g., to adapt
126
+ the sleeping interval between db queries
127
+ """
128
+ self.logger.info('Querying DB for new simulations')
129
+ # query database for open inversions
130
+ self.conn = self.engine.connect()
131
+ transaction = self.conn.begin_nested()
132
+ # get the next free, unfinished simulation
133
+ r = self.conn.execute(
134
+ ' '.join((
135
+ 'select index from inversions where ',
136
+ 'ready_for_processing=\'t\' and status <> \'finished\'',
137
+ 'and error=0',
138
+ 'order by ',
139
+ 'index asc for update skip locked limit 1;'
140
+ ))
141
+ )
142
+ if r.rowcount == 0:
143
+ r.close()
144
+ transaction.commit()
145
+ self.conn.close()
146
+ return False
147
+ # now the row is locked for us
148
+ job_id = r.fetchone()[0]
149
+ self.logger.info('job id: {}'.format(job_id))
150
+ query = ' '.join((
151
+ 'select',
152
+ 'tomodir_unfinished_file, sim_type,',
153
+ 'ready_for_processing, status',
154
+ 'from inversions',
155
+ 'where index=%(index)s;'
156
+ ))
157
+ job_data = pd.read_sql_query(
158
+ query, self.conn, params={'index': job_id})
159
+
160
+ self.logger.info('Processing job id: {}'.format(job_id))
161
+
162
+ self._run_sim(
163
+ job_id,
164
+ int(job_data['tomodir_unfinished_file'].values.take(0)),
165
+ # job_data['hydra_location'].values.take(0),
166
+ # job_data['archive_hash'].values.take(0),
167
+ job_data['sim_type'].values.take(0)
168
+ )
169
+
170
+ # this actually updates the database
171
+ transaction.commit()
172
+ self.conn.close()
173
+ return True
174
+
175
+ def _run_sim(self, job_id, file_id, sim_type):
176
+ tempdir = tempfile.mkdtemp('_crhydra')
177
+ self.logger.info('tempdir: {}'.format(tempdir))
178
+
179
+ # get unfinished data and unpack
180
+ result = self.conn.execute(
181
+ 'select hash, data from binary_data where index=%(file_id)s;',
182
+ file_id=file_id
183
+ )
184
+ file_hash, binary_data = result.fetchone()
185
+
186
+ # check hash
187
+ m = hashlib.sha256()
188
+ m.update(binary_data)
189
+ assert file_hash == m.hexdigest()
190
+
191
+ # unpack to tempir
192
+ fid = io.BytesIO(bytes(binary_data))
193
+ with tarfile.open(fileobj=fid, mode='r') as tar:
194
+ tar.extractall(path=tempdir)
195
+
196
+ # call td run
197
+ old_pwd = os.getcwd()
198
+ os.chdir(tempdir)
199
+ self.logger.info('Running inversion')
200
+ dt_inv_started = datetime.datetime.now(tz=datetime.timezone.utc)
201
+
202
+ inv_cpu = subprocess.check_output(
203
+ 'cat /proc/cpuinfo | grep "model name" | head -1',
204
+ shell=True
205
+ )[13:].decode('utf-8').strip()
206
+ # 0: everything ok
207
+ error_code = 0
208
+ try:
209
+ subprocess.check_output(
210
+ 'nice -n {} td_run_all_local -n 1 -t {}'.format(
211
+ nice_level,
212
+ number_of_worker_threads
213
+ ),
214
+ stderr=subprocess.STDOUT,
215
+ shell=True
216
+ )
217
+ except subprocess.CalledProcessError as e:
218
+ self.logger.error('There was an error executing CRTomo')
219
+ self.logger.error(e)
220
+ # it seems something went wrong while running CRTomo...
221
+ query = ' '.join((
222
+ 'update inversions set',
223
+ 'error = %(error_code)s,',
224
+ 'error_msg=%(error_msg)s,',
225
+ 'datetime_inversion_started=%(dt_started)s,',
226
+ 'datetime_finished=%(dt_finished)s, ',
227
+ 'inv_computer=%(node_name)s,',
228
+ 'inv_cpu=%(inv_cpu)s',
229
+ 'where index=%(job_id)s;',
230
+ ))
231
+ r = self.conn.execute(
232
+ query,
233
+ {
234
+ 'job_id': job_id,
235
+ 'error_code': 2,
236
+ 'node_name': node_name,
237
+ 'inv_cpu': inv_cpu,
238
+ 'dt_started': dt_inv_started,
239
+ 'dt_finished': datetime.datetime.now(
240
+ tz=datetime.timezone.utc),
241
+ 'error_msg': e.cmd + '_' + e.output.decode('utf-8'),
242
+ }
243
+ )
244
+ return
245
+
246
+ # special case: an error.dat file appeared, indicating that the
247
+ # inversion itself broke. This is seen as a successful simulation
248
+ # from the view of cr_hydra
249
+ tomodir_name = glob.glob(tempdir + '/*')[0]
250
+ error_file = os.sep.join((
251
+ tomodir_name, 'exe', 'error.dat'
252
+ ))
253
+ if os.path.isfile(error_file):
254
+ self.logger.info('found error.dat file - setting error code to 1')
255
+ error_code = 1
256
+
257
+ dt_inv_ended = datetime.datetime.now(tz=datetime.timezone.utc)
258
+
259
+ self.logger.info('finished')
260
+ # create in-memory archive
261
+ finished_data = io.BytesIO()
262
+ with tarfile.open(fileobj=finished_data, mode='w:xz') as tar_out:
263
+ tar_out.add('.')
264
+
265
+ # create hash of final data
266
+ m = hashlib.sha256()
267
+ finished_data.seek(0)
268
+ m.update(finished_data.read())
269
+ hash_final = m.hexdigest()
270
+
271
+ # upload
272
+ finished_data.seek(0)
273
+ result = self.conn.execute(
274
+ 'insert into binary_data (hash, data) values' +
275
+ '(%(data_hash)s, %(bin_data)s) returning index;',
276
+ data_hash=hash_final,
277
+ bin_data=finished_data.read()
278
+ )
279
+ finished_data_index = result.fetchone()[0]
280
+
281
+ os.chdir(old_pwd)
282
+ self.logger.info('updating to finished')
283
+ # mark as finished
284
+ query = ' '.join((
285
+ 'update inversions set',
286
+ 'status=\'finished\',',
287
+ 'error=%(error_code)s,',
288
+ 'tomodir_finished_file=%(finished_data)s,',
289
+ 'datetime_inversion_started=%(dt_started)s,',
290
+ 'datetime_finished=%(dt_finished)s, ',
291
+ 'inv_computer=%(node_name)s,',
292
+ 'inv_cpu=%(inv_cpu)s',
293
+ 'where index=%(job_id)s;',
294
+ ))
295
+ r = self.conn.execute(
296
+ query,
297
+ {
298
+ 'job_id': job_id,
299
+ 'finished_data': finished_data_index,
300
+ 'node_name': node_name,
301
+ 'inv_cpu': inv_cpu,
302
+ 'error_code': error_code,
303
+ 'dt_started': dt_inv_started,
304
+ 'dt_finished': dt_inv_ended,
305
+ }
306
+ )
307
+ assert r.rowcount == 1
308
+
309
+ # delete temporary directory
310
+ shutil.rmtree(tempdir)
311
+
312
+ def _get_hash_sha256(self, filename):
313
+ sha256 = subprocess.check_output(
314
+ 'sha256sum "{}"'.format(filename), shell=True).decode('utf-8')
315
+ sha256 = sha256[0:sha256.find(' ')]
316
+ return sha256
317
+
318
+
319
+ def handle_cmd_options():
320
+ parser = OptionParser()
321
+ # parser.add_option(
322
+ # "-n", "--number",
323
+ # dest="number_processes",
324
+ # help="How many CRMod/CRTomo instances to start in parallel. " +
325
+ # "Default: number of detected CPUs/2",
326
+ # type='int',
327
+ # default=None,
328
+ # )
329
+
330
+ parser.add_option(
331
+ "-q", "--quit",
332
+ dest="quit_after_empty",
333
+ help="Exit the program when no more simulations are queries in the DB",
334
+ action='store_true',
335
+ )
336
+
337
+ (options, args) = parser.parse_args()
338
+ return options
339
+
340
+
341
+ def main():
342
+ cmd_opts = handle_cmd_options()
343
+
344
+ workers = []
345
+ for i in range(number_of_workers):
346
+ worker = hydra_worker('thread_{:02}'.format(i), cmd_opts)
347
+ workers.append(worker)
348
+ worker.start()
349
+
350
+
351
+ if __name__ == '__main__':
352
+ main()