docs: def step(self, trace):
          """
          Returns a set of DataBlocks whose inputs were updated. This will be used by the Graph run() method to invoke
          the step() method of the next blocks in the chain (we don't invoke those call the step() method of those
          blocks directly from here, because then the stack could get quite large).
          """
          downstream_blocks = []
  
          # Restrict the set of input channels we consider to those that are active (a channel is typically deactivated
          # before it starts producing useful data, and after it has reached the end of useful data)
          active_input_channels_names = [channel_name for channel_name in self.input_channels.keys()
                                         if self.input_channels[channel_name].active]
          active_input_channels = {name: self.input_channels[name] for name in active_input_channels_names}
  
          # If this is a block that has one or more input channels but none of them are active, bail out:
          if self.input_channels and not active_input_channels:
              return downstream_blocks
  
          # 2. Pull data for earliest time. If there are no input channels, we just proceed:
          unprocessed_input_channels = {}
          if active_input_channels:
              self.update_output_channel_timestamps()
              # Get the collection of channels from which I have not already consumed data:
              unprocessed_input_channel_names = [channel_name for channel_name in active_input_channels.keys()
                                            if not active_input_channels[channel_name].consumers[self]]
              unprocessed_input_channels = {name:active_input_channels[name] for name in unprocessed_input_channel_names}
              channel_with_earliest_data = min(active_input_channels.values())
  
              if not unprocessed_input_channels:
                  # If I have some input channels but none of them are unprocessed, then there is no data to be pulled.
                  # In this case, we need to pass along the current time, and be done.
                  self.advance_self_to_latest_time_of_data_in_channels()
                  self.update_output_channel_timestamps()
                  return downstream_blocks
  
          if self.terminated():
              return downstream_blocks
  
          if unprocessed_input_channels:
              state_change = False
              for input_channel_name in unprocessed_input_channels.keys():
                  # We compare against the timestamps for all channels because even if a given channel has provided us
                  # valid data, if its timestamp is still earlier than what we believe to be the current timestamp, then
                  # we need to provide it the opportunity to potentially give even more recent data.
                  if unprocessed_input_channels[input_channel_name] <= channel_with_earliest_data:
                      new_data = unprocessed_input_channels[input_channel_name].get_value(self)  # gets value AND marks as consumed.
                      logging.debug("==> Pulling data (" + str(new_data.data) + ") from channel '" +
                                    input_channel_name + "' -- " + str(self))
                      if (not self.input_data) or \
                              (not input_channel_name in self.input_data) or \
                                      self.input_data[input_channel_name] != new_data:
                          state_change = True
                          self.input_data[input_channel_name] = new_data
              if not state_change:
                  return downstream_blocks
              self.advance_self_to_latest_time_of_pulled_data()
  
          for input_name in self.input_data.keys():
              logging.debug("# BLOCK " + str(self) + ": time=" + str(self.time) + ", " + str(input_name) + " = " +
                            str(self.input_data[input_name]))
  
          # 3. Ensure inputs satisfied (note that we want input data for all channels, not just those currently active):
          if self.input_channels:
              for input_channel_name in self.input_channels.keys():
                  if input_channel_name not in self.input_data.keys():
                      logging.debug("     Channel " + input_channel_name + " not satisfied. Bailing out.")
                      return downstream_blocks
  
          # Ensure that -- if there are any downstream channels -- they are all open.
          # Note that this has to happen after pulling data from the input channels in order to properly accommodate the
          # case in which a block consumes its own outputs.
          if self.output_channels:
              for output_channel in self.output_channels.values():
                  if not output_channel.is_open():
                      return downstream_blocks
  
          # 4. Execute user code:
          logging.debug("Executing block code for: " + str(self))
          trace.append(['execute', self.time, str(self)])
          self.block_code()  # the block_code() method is responsible for setting new values in the output channels
          # logging.debug("After executing user code, block time is: " + str(self.time))
  
          self.update_output_channel_timestamps()
          if self._after_valid_time_range():
              self.terminate()
  
          for output_channel in self.output_channels.values():
              if self._in_valid_time_range():
                  for consumer in output_channel.get_consumers():
                      # logging.debug(str(self) + " is nominating block to append to run list: " + str(consumer))
                      downstream_blocks.append(consumer)
              else:
                  output_channel.mark_consumers_hungry_for_more()
                  #if self._after_valid_time_range():
                  #    self.terminate()
  
          # Return collection of downstream neighbors:
          return downstream_blocks