Repository: pytorch/ignite

Issue:
event_to_attr should be extensible for register_event
There should be a way to extend `event_to_attr` when registering new events.


Discussion:
Yes, I agree. @Mxbonn if you would like to propose a PR, you are welcome to contribute :)

@Mxbonn thanks for bringing this up. I would like to get more information on this issue. See code below

```python
from ignite.engine import Engine
from ignite.contrib.handlers import CustomPeriodicEvent

trainer = Engine(lambda engine, batch: None)
CustomPeriodicEvent(n_epochs=10).attach(trainer)
state = trainer.run([])

state.event_to_attr
# {<Events.ITERATION_STARTED: 'iteration_started'>: 'iteration',
#  <Events.ITERATION_COMPLETED: 'iteration_completed'>: 'iteration',
#  <Events.EPOCH_STARTED: 'epoch_started'>: 'epoch',
#  <Events.EPOCH_COMPLETED: 'epoch_completed'>: 'epoch',
#  <Events.STARTED: 'started'>: 'epoch',
#  <Events.COMPLETED: 'completed'>: 'epoch',
#  <Events.EPOCHS_10_STARTED: 1>: 'epochs_10',
#  <Events.EPOCHS_10_COMPLETED: 2>: 'epochs_10'}
```

CustomPeriodicEvent handles this in some manner. Does this work for you? Would appreciate if you provide an example, if it does not. 


@anmolsjoshi I guess in my use case I could use something like `CustomPeriodicEvent`. However I believe it makes sense to add functionality to map new events to new attributes. 

In my case I have a custom Engine which uses an extra outer loop, so you have `inq_iterations`, `epochs` and `iterations`.

```python
class Inq_Events(Enum):
    """Additional INQ events.
    Additional events to indicitate the start and the end of a campaign
    """
    INQ_STARTED = "inq_started"
    INQ_ITERATION_STARTED = "inq_iteration_started"
    INQ_ITERATION_COMPLETED = "inq_iteration_completed"
    INQ_COMPLETED = "inq_completed"


class Inq_Engine(Engine):
    def __init__(self, *args):
        super(Inq_Engine, self).__init__(*args)
        self.should_terminate_single_inq_iteration = False
        self.register_events(*Inq_Events)

    def ternimate_inq_iteration(self):
        self._logger.info("Terminate inq iteration is signaled. "
                          "Current inq iteration will stop after the current epoch is finished.")
        self.should_terminate_single_inq_iteration = True

    def run(self, data, max_epochs=1, inq_iterations=1):
        """Runs the process_function over the passed data.
                Args:
                    data (Iterable): Collection of batches allowing repeated iteration (e.g., list or `DataLoader`).
                    max_epochs (int, optional): max epochs to run for (default: 1).
                    inq_iterations (int, optional): number of incremental quantization iterations
                Returns:
                    State: output state.
                """

        self.state = State(dataloader=data, epoch=0, max_epochs=max_epochs, metrics={}, inq_iteration=0)

        try:
            self._logger.info("Engine run starting with max_epochs={}.".format(max_epochs))
            start_time = time.time()
            self._fire_event(Events.STARTED)
            self._fire_event(Inq_Events.INQ_STARTED)
            for inq_iter in range(inq_iterations):
                self.state.inq_iteration += 1
                self._fire_event(Inq_Events.INQ_ITERATION_STARTED)
                while self.state.epoch < (max_epochs * self.state.inq_iteration) and not self.should_terminate:
                    self.state.epoch += 1
                    self._fire_event(Events.EPOCH_STARTED)
                    hours, mins, secs = self._run_once_on_dataset()
                    self._logger.info("Epoch[%s], INQ Iteration[%s] Complete. Time taken: %02d:%02d:%02d",
                                      self.state.epoch, self.state.inq_iteration, hours, mins, secs)
                    if self.should_terminate_single_inq_iteration:
                        break
                    self._fire_event(Events.EPOCH_COMPLETED)
                self._fire_event(Inq_Events.INQ_ITERATION_COMPLETED)
                if self.should_terminate:
                    break

            self._fire_event(Inq_Events.INQ_COMPLETED)
            self._fire_event(Events.COMPLETED)
            time_taken = time.time() - start_time
            hours, mins, secs = _to_hours_mins_secs(time_taken)
            self._logger.info("Engine run complete. Time taken %02d:%02d:%02d" % (hours, mins, secs))

        except BaseException as e:
            self._logger.error("Engine run is terminating due to exception: %s.", str(e))
            self._handle_exception(e)

        return self.state
```

I think it would make sense to add an optional event_to_attrib dictionary mapping to the `register_events` method
@Mxbonn yes, that makes sense. We talked with @anmolsjoshi and supposed that this issue is about to include some optional `event_to_attrib` mapping to `register_events`. So, your example confirms our assumption.

Resolving patch:
diff --git a/ignite/engine/engine.py b/ignite/engine/engine.py
--- a/ignite/engine/engine.py
+++ b/ignite/engine/engine.py
@@ -34,12 +34,14 @@ class State(object):
     }
 
     def __init__(self, **kwargs):
-        self.iteration = 0
         self.output = None
         self.batch = None
         for k, v in kwargs.items():
             setattr(self, k, v)
 
+        for value in self.event_to_attr.values():
+            setattr(self, value, 0)
+
     def get_event_attrib_value(self, event_name):
         if event_name not in State.event_to_attr:
             raise RuntimeError("Unknown event name '{}'".format(event_name))
@@ -89,18 +91,19 @@ def __init__(self, process_function):
 
         self._check_signature(process_function, 'process_function', None)
 
-    def register_events(self, *event_names):
+    def register_events(self, *event_names, **kwargs):
         """Add events that can be fired.
 
         Registering an event will let the user fire these events at any point.
         This opens the door to make the :meth:`~ignite.engine.Engine.run` loop even more
         configurable.
 
-        By default, the events from :class:`~ignite.engine.Events` are registerd.
+        By default, the events from :class:`~ignite.engine.Events` are registered.
 
         Args:
             *event_names: An object (ideally a string or int) to define the
                 name of the event being supported.
+            event_to_attr (dict): A dictionary to map an event to a state attribute.
 
         Example usage:
 
@@ -115,9 +118,34 @@ class Custom_Events(Enum):
             engine = Engine(process_function)
             engine.register_events(*Custom_Events)
 
+
+        Example with State Attribute:
+
+        .. code-block:: python
+
+            from enum import Enum
+
+            class TBPTT_Events(Enum):
+                TIME_ITERATION_STARTED = "time_iteration_started"
+                TIME_ITERATION_COMPLETED = "time_iteration_completed"
+
+            TBPTT_event_to_attr = {TBPTT_Events.TIME_ITERATION_STARTED: 'time_iteration',
+                                   TBPTT_Events.TIME_ITERATION_STARTED: 'time_iteration'}
+
+            engine = Engine(process_function)
+            engine.register_events(*TBPTT_Events, event_to_attr=TBPTT_event_to_attr)
+            engine.run(data)
+            # engine.state contains an attribute time_iteration, which can be accessed using engine.state.time_iteration
         """
+        event_to_attr = kwargs.get('event_to_attr', None)
+        if event_to_attr:
+            if not isinstance(event_to_attr, dict):
+                raise ValueError('Expected event_to_attr to be dictionary. Got {}.'.format(type(event_to_attr)))
+
         for name in event_names:
             self._allowed_events.append(name)
+            if event_to_attr:
+                State.event_to_attr[name] = event_to_attr[name]
 
     def add_event_handler(self, event_name, handler, *args, **kwargs):
         """Add an event handler to be executed when the specified event is fired.
@@ -334,7 +362,7 @@ def run(self, data, max_epochs=1):
             State: output state.
         """
 
-        self.state = State(dataloader=data, epoch=0, max_epochs=max_epochs, metrics={})
+        self.state = State(dataloader=data, max_epochs=max_epochs, metrics={})
 
         try:
             self._logger.info("Engine run starting with max_epochs={}.".format(max_epochs))


Test patch:
diff --git a/tests/ignite/engine/test_engine.py b/tests/ignite/engine/test_engine.py
--- a/tests/ignite/engine/test_engine.py
+++ b/tests/ignite/engine/test_engine.py
@@ -231,6 +231,43 @@ def process_func(engine, batch):
     assert handle.called
 
 
+def test_custom_events_with_event_to_attr():
+    class Custom_Events(Enum):
+        TEST_EVENT = "test_event"
+
+    custom_event_to_attr = {Custom_Events.TEST_EVENT: 'test_event'}
+
+    # Dummy engine
+    engine = Engine(lambda engine, batch: 0)
+    engine.register_events(*Custom_Events, event_to_attr=custom_event_to_attr)
+
+    # Handle is never called
+    handle = MagicMock()
+    engine.add_event_handler(Custom_Events.TEST_EVENT, handle)
+    engine.run(range(1))
+    assert hasattr(engine.state, 'test_event')
+    assert engine.state.test_event == 0
+
+    # Advanced engine
+    def process_func(engine, batch):
+        engine.fire_event(Custom_Events.TEST_EVENT)
+
+    engine = Engine(process_func)
+    engine.register_events(*Custom_Events, event_to_attr=custom_event_to_attr)
+
+    def handle(engine):
+        engine.state.test_event += 1
+
+    engine.add_event_handler(Custom_Events.TEST_EVENT, handle)
+    engine.run(range(25))
+    assert engine.state.test_event == 25
+
+    custom_event_to_attr = 'a'
+    engine = Engine(lambda engine, batch: 0)
+    with pytest.raises(ValueError):
+        engine.register_events(*Custom_Events, event_to_attr=custom_event_to_attr)
+
+
 def test_on_decorator_raises_with_invalid_event():
     engine = DummyEngine()
     with pytest.raises(ValueError):