Completed
Push — master ( c73972...b998ba )
by Klaus
32s
created

Experiment.get_usage()   A

Complexity

Conditions 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
c 1
b 0
f 1
dl 0
loc 8
rs 9.4285
1
#!/usr/bin/env python
2
# coding=utf-8
3
"""This module defines the Experiment class, which is central to sacred."""
4
from __future__ import division, print_function, unicode_literals
5
6
import inspect
7
import os.path
8
import shlex
9
import sys
10
from collections import OrderedDict
11
from docopt import docopt, printable_usage
12
13
from sacred.arg_parser import get_config_updates, parse_args, format_usage
14
from sacred.commandline_options import gather_command_line_options, ForceOption
15
from sacred.commands import print_config, print_dependencies, save_config, help_for_command
16
from sacred.config.signature import Signature
17
from sacred.ingredient import Ingredient
18
from sacred.initialize import create_run
19
from sacred.optional import basestring
20
from sacred.utils import print_filtered_stacktrace
21
22
__sacred__ = True  # marks files that should be filtered from stack traces
23
24
__all__ = ('Experiment',)
25
26
27
class Experiment(Ingredient):
28
    """
29
    The central class for each experiment in Sacred.
30
31
    It manages the configuration, the main function, captured methods,
32
    observers, commands, and further ingredients.
33
34
    An Experiment instance should be created as one of the first
35
    things in any experiment-file.
36
    """
37
38
    def __init__(self, name=None, ingredients=(), interactive=False):
39
        """
40
        Create a new experiment with the given name and optional ingredients.
41
42
        Parameters
43
        ----------
44
        name : str, optional
45
            Optional name of this experiment, defaults to the filename.
46
            (Required in interactive mode)
47
48
        ingredients : list[sacred.Ingredient], optional
49
            A list of ingredients to be used with this experiment.
50
51
        interactive : bool, optional
52
            If set to True will allow the experiment to be run in interactive
53
            mode (e.g. IPython or Jupyter notebooks).
54
            However, this mode is discouraged since it won't allow storing the
55
            source-code or reliable reproduction of the runs.
56
57
        """
58
        caller_globals = inspect.stack()[1][0].f_globals
59
        if name is None:
60
            if interactive:
61
                raise RuntimeError('name is required in interactive mode.')
62
            mainfile = caller_globals.get('__file__')
63
            if mainfile is None:
64
                raise RuntimeError('No main-file found. Are you running in '
65
                                   'interactive mode? If so please provide a '
66
                                   'name and set interactive=True.')
67
            name = os.path.basename(mainfile)
68
            if name.endswith('.py'):
69
                name = name[:-3]
70
            elif name.endswith('.pyc'):
71
                name = name[:-4]
72
        super(Experiment, self).__init__(path=name,
73
                                         ingredients=ingredients,
74
                                         interactive=interactive,
75
                                         _caller_globals=caller_globals)
76
        self.default_command = None
77
        self.command(print_config, unobserved=True)
78
        self.command(print_dependencies, unobserved=True)
79
        self.command(save_config, unobserved=True)
80
        self.observers = []
81
        self.current_run = None
82
        self.captured_out_filter = None
83
        """Filter function to be applied to captured output of a run"""
84
        self.option_hooks = []
85
86
    # =========================== Decorators ==================================
87
88
    def main(self, function):
89
        """
90
        Decorator to define the main function of the experiment.
91
92
        The main function of an experiment is the default command that is being
93
        run when no command is specified, or when calling the run() method.
94
95
        Usually it is more convenient to use ``automain`` instead.
96
        """
97
        captured = self.command(function)
98
        self.default_command = captured.__name__
99
        return captured
100
101
    def automain(self, function):
102
        """
103
        Decorator that defines *and runs* the main function of the experiment.
104
105
        The decorated function is marked as the default command for this
106
        experiment, and the command-line interface is automatically run when
107
        the file is executed.
108
109
        The method decorated by this should be last in the file because is
110
        equivalent to:
111
112
        .. code-block:: python
113
114
            @ex.main
115
            def my_main():
116
                pass
117
118
            if __name__ == '__main__':
119
                ex.run_commandline()
120
        """
121
        captured = self.main(function)
122
        if function.__module__ == '__main__':
123
            # Ensure that automain is not used in interactive mode.
124
            import inspect
125
            main_filename = inspect.getfile(function)
126
            if (main_filename == '<stdin>' or
127
                    (main_filename.startswith('<ipython-input-') and
128
                     main_filename.endswith('>'))):
129
                raise RuntimeError('Cannot use @ex.automain decorator in '
130
                                   'interactive mode. Use @ex.main instead.')
131
132
            self.run_commandline()
133
        return captured
134
135
    def option_hook(self, function):
136
        """
137
        Decorator for adding an option hook function.
138
139
        An option hook is a function that is called right before a run
140
        is created. It receives (and potentially modifies) the options
141
        dictionary. That is, the dictionary of commandline options used for
142
        this run.
143
144
        .. note::
145
            The decorated function MUST have an argument called options.
146
147
            The options also contain ``'COMMAND'`` and ``'UPDATE'`` entries,
148
            but changing them has no effect. Only modification on
149
            flags (entries starting with ``'--'``) are considered.
150
        """
151
        sig = Signature(function)
152
        if "options" not in sig.arguments:
153
            raise KeyError("option_hook functions must have an argument called"
154
                           " 'options', but got {}".format(sig.arguments))
155
        self.option_hooks.append(function)
156
        return function
157
158
    # =========================== Public Interface ============================
159
160
    def get_usage(self, program_name=None):
161
        """Get the commandline usage string for this experiment."""
162
        program_name = program_name or sys.argv[0]
163
        all_commands = OrderedDict(self.gather_commands())
164
        options = gather_command_line_options()
165
        long_usage = format_usage(program_name, self.doc, all_commands, options)
166
        short_usage = printable_usage(long_usage)
167
        return short_usage, long_usage
168
169
    def run(self, command_name=None, config_updates=None, named_configs=(),
170
            meta_info=None, options=None):
171
        """
172
        Run the main function of the experiment or a given command.
173
174
        Parameters
175
        ----------
176
        command_name : str, optional
177
            Name of the command to be run. Defaults to main function.
178
179
        config_updates : dict, optional
180
            Changes to the configuration as a nested dictionary
181
182
        named_configs : list[str], optional
183
            list of names of named_configs to use
184
185
        meta_info : dict, optional
186
            Additional meta information for this run.
187
188
        options : dict, optional
189
            Dictionary of options to use
190
191
        Returns
192
        -------
193
        sacred.run.Run
194
            the Run object corresponding to the finished run
195
196
        """
197
        run = self._create_run(command_name, config_updates, named_configs,
198
                               meta_info, options)
199
        run()
200
        return run
201
202
    def run_command(self, command_name, config_updates=None,
203
                    named_configs=(), args=(), meta_info=None):
204
        """Run the command with the given name.
205
206
        .. note:: Deprecated in Sacred 0.7
207
            run_command() will be removed in Sacred 1.0.
208
            It is replaced by run() which can now also handle command_names.
209
        """
210
        import warnings
211
        warnings.warn("run_command is deprecated. Use run instead",
212
                      DeprecationWarning)
213
        return self.run(command_name, config_updates, named_configs, meta_info,
214
                        args)
215
216
    def run_commandline(self, argv=None):
217
        """
218
        Run the command-line interface of this experiment.
219
220
        If ``argv`` is omitted it defaults to ``sys.argv``.
221
222
        Parameters
223
        ----------
224
        argv : list[str] or str, optional
225
            Command-line as string or list of strings like ``sys.argv``.
226
227
        Returns
228
        -------
229
        sacred.run.Run
230
            The Run object corresponding to the finished run.
231
232
        """
233
        if argv is None:
234
            argv = sys.argv
235
        elif isinstance(argv, basestring):
236
            argv = shlex.split(argv)
237
        else:
238
            if not isinstance(argv, (list, tuple)):
239
                raise ValueError("argv must be str or list, but was {}"
240
                                 .format(type(argv)))
241
            if not all([isinstance(a, basestring) for a in argv]):
242
                problems = [a for a in argv if not isinstance(a, basestring)]
243
                raise ValueError("argv must be list of str but contained the "
244
                                 "following elements: {}".format(problems))
245
246
        short_usage, usage = self.get_usage()
247
        args = docopt(usage, [str(a) for a in argv[1:]], help=False)
248
        commands = OrderedDict(self.gather_commands())
249
        cmd_name = args.get('COMMAND') or self.default_command
250
        config_updates, named_configs = get_config_updates(args['UPDATE'])
251
252
        if cmd_name is not None and cmd_name not in commands:
253
            print(short_usage)
254
            print('Error: Command "{}" not found. Available commands are: '
255
                  '{}'.format(cmd_name, ", ".join(commands.keys())))
256
            exit(1)
257
258
        if args['help'] or args['--help']:
259
            if args['COMMAND'] is None:
260
                print(usage)
261
            else:
262
                print(help_for_command(commands[args['COMMAND']]))
263
            exit()
264
265
        if cmd_name is None:
266
            print(short_usage)
267
            print('Error: No command found to be run. Specify a command'
268
                  ' or define main function. Available commands'
269
                  ' are: {}'.format(", ".join(commands.keys())))
270
            exit(1)
271
272
        try:
273
            return self.run(cmd_name, config_updates, named_configs, {}, args)
274
        except Exception:
275
            if not self.current_run or self.current_run.debug:
276
                raise
277
            elif self.current_run.pdb:
278
                import traceback
279
                import pdb
280
                traceback.print_exception(*sys.exc_info())
281
                pdb.post_mortem()
282
            else:
283
                print_filtered_stacktrace()
284
                exit(1)
285
286
    def open_resource(self, filename, mode='r'):
287
        """Open a file and also save it as a resource.
288
289
        Opens a file, reports it to the observers as a resource, and returns
290
        the opened file.
291
292
        In Sacred terminology a resource is a file that the experiment needed
293
        to access during a run. In case of a MongoObserver that means making
294
        sure the file is stored in the database (but avoiding duplicates) along
295
        its path and md5 sum.
296
297
        This function can only be called during a run, and just calls the
298
        :py:meth:`sacred.run.Run.open_resource` method.
299
300
        Parameters
301
        ----------
302
        filename: str
303
            name of the file that should be opened
304
        mode : str
305
            mode that file will be open
306
307
        Returns
308
        -------
309
        file
310
            the opened file-object
311
312
        """
313
        assert self.current_run is not None, "Can only be called during a run."
314
        return self.current_run.open_resource(filename, mode)
315
316
    def add_resource(self, filename):
317
        """Add a file as a resource.
318
319
        In Sacred terminology a resource is a file that the experiment needed
320
        to access during a run. In case of a MongoObserver that means making
321
        sure the file is stored in the database (but avoiding duplicates) along
322
        its path and md5 sum.
323
324
        This function can only be called during a run, and just calls the
325
        :py:meth:`sacred.run.Run.add_resource` method.
326
327
        Parameters
328
        ----------
329
        filename : str
330
            name of the file to be stored as a resource
331
        """
332
        assert self.current_run is not None, "Can only be called during a run."
333
        self.current_run.add_resource(filename)
334
335
    def add_artifact(self, filename, name=None):
336
        """Add a file as an artifact.
337
338
        In Sacred terminology an artifact is a file produced by the experiment
339
        run. In case of a MongoObserver that means storing the file in the
340
        database.
341
342
        This function can only be called during a run, and just calls the
343
        :py:meth:`sacred.run.Run.add_artifact` method.
344
345
        Parameters
346
        ----------
347
        filename : str
348
            name of the file to be stored as artifact
349
        name : str, optional
350
            optionally set the name of the artifact.
351
            Defaults to the relative file-path.
352
353
        """
354
        assert self.current_run is not None, "Can only be called during a run."
355
        self.current_run.add_artifact(filename, name)
356
357
    @property
358
    def info(self):
359
        """Access the info-dict for storing custom information.
360
361
        Only works during a run and is essentially a shortcut to:
362
363
        .. code-block:: python
364
365
            @ex.capture
366
            def my_captured_function(_run):
367
                # [...]
368
                _run.info   # == ex.info
369
        """
370
        return self.current_run.info
371
372
    def log_scalar(self, name, value, step=None):
373
        """
374
        Add a new measurement.
375
376
        The measurement will be processed by the MongoDB* observer
377
        during a heartbeat event.
378
        Other observers are not yet supported.
379
380
        :param metric_name: The name of the metric, e.g. training.loss
381
        :param value: The measured value
382
        :param step: The step number (integer), e.g. the iteration number
383
                    If not specified, an internal counter for each metric
384
                    is used, incremented by one.
385
        """
386
        # Method added in change https://github.com/chovanecm/sacred/issues/4
387
        # The same as Run.log_scalar
388
        return self.current_run.log_scalar(name, value, step)
389
390
    def gather_commands(self):
391
        """Iterator over all commands of this experiment.
392
393
        Also recursively collects all commands from ingredients.
394
395
        Yields
396
        ------
397
        (str, function)
398
            A tuple consisting of the (dotted) command-name and the
399
            corresponding captured function.
400
401
        """
402
        for cmd_name, cmd in self.commands.items():
403
            yield cmd_name, cmd
404
405
        for ingred in self.ingredients:
406
            for cmd_name, cmd in ingred.gather_commands():
407
                yield cmd_name, cmd
408
409
    def get_default_options(self):
410
        """Get a dictionary of default options as used with run.
411
412
        Returns
413
        -------
414
        dict
415
            A dictionary containing option keys of the form '--beat_interval'.
416
            Their values are boolean if the option is a flag, otherwise None or
417
            its default value.
418
419
        """
420
        all_commands = self.gather_commands()
421
        args = parse_args(["dummy"],
422
                          description=self.doc,
423
                          commands=OrderedDict(all_commands))
424
        return {k: v for k, v in args.items() if k.startswith('--')}
425
426
    # =========================== Internal Interface ==========================
427
428
    def _create_run(self, command_name=None, config_updates=None,
429
                    named_configs=(), meta_info=None, options=None):
430
        command_name = command_name or self.default_command
431
        if command_name is None:
432
            raise RuntimeError('No command found to be run. Specify a command '
433
                               'or define a main function.')
434
435
        default_options = self.get_default_options()
436
        if options:
437
            default_options.update(options)
438
        options = default_options
439
440
        # call option hooks
441
        for oh in self.option_hooks:
442
            oh(options=options)
443
444
        run = create_run(self, command_name, config_updates,
445
                         named_configs=named_configs,
446
                         force=options.get(ForceOption.get_flag(), False))
447
        run.meta_info['command'] = command_name
448
        run.meta_info['options'] = options
449
450
        if meta_info:
451
            run.meta_info.update(meta_info)
452
453
        for option in gather_command_line_options():
454
            option_value = options.get(option.get_flag(), False)
455
            if option_value:
456
                option.apply(option_value, run)
457
458
        self.current_run = run
459
        return run
460