docs: def run(self, grid=None, num_of_paths=2000, seed=0, num_of_workers=CPU_COUNT, profiling=False):
          """
          implements simulation
  
          :param list(date) grid: list of Monte Carlo grid dates
          :param int num_of_paths: number of Monte Carlo paths
          :param hashable seed: seed used for rnds initialisation (additional adjustment in place)
          :param int or None num_of_workers: number of parallel workers (default: cpu_count()),
              if None no parallel processing is used
          :param bool profiling: signal whether to use profiling, True means used, else not
          :return object: final consumer state
  
          It returns a list of lists.
          The list contains per path a list produced by consumer at observation dates
          """
          self.grid = sorted(set(grid))
          self.num_of_paths = num_of_paths
          self.num_of_workers = num_of_workers
          self.seed = seed
  
          # pre processing
          self.producer.initialize(self.grid, self.num_of_paths, self.seed)
          self.consumer.initialize(self.grid, self.num_of_paths, self.seed)
  
          if num_of_workers:
              # processing
              workers = list()
              queue = Queue()
              path_per_worker = int(num_of_paths // num_of_workers)
              start_path, stop_path = 0, path_per_worker
              for i in range(num_of_workers):
                  if i == num_of_workers - 1:
                      stop_path = num_of_paths  # ensure exact num of path as required
                  name = 'worker-%d' % i
                  if profiling:
                      # display profile with `snakeviz worker-0.prof`
                      # if not installed `pip install snakeviz`
                      workers.append(Process(target=self._run_parallel_process_with_profiling,
                                             name=name,
                                             args=(start_path, stop_path, queue, name + '.prof')))
                  else:
                      workers.append(Process(target=self._run_parallel_process,
                                             name=name,
                                             args=(start_path, stop_path, queue)))
                  start_path, stop_path = stop_path, stop_path + path_per_worker
              for worker in workers:
                  worker.start()
  
              # post processing
              for _ in range(num_of_workers):
                  self.consumer.get(queue.get())
              for worker in workers:
                  worker.join()
          else:
              self._run_process(0, num_of_paths)
  
          self.consumer.finalize()
          return self.consumer.result