FairRoot bazuje na "taskach". Task to odpowiednik jakiegoś zadania - np. określonej analizy. W jednym makrze możemy mieć kilka tasków wykonywanych sekwencyjnie, co więcej jeden task może być źródłem danych dla innego tasku. Poniżej przepis jak taki task stworzyć - tu na przykładzie analizy pędu poprzecznego.

Pierwszym krokiem jest stworzenie klasy dziedziczącej po FairTask. Poniżej przykład takiej klasy:

class MyTask : public FairTask {
  UEvent* fEvent;
  TH1D* fPt;

protected:
  virtual InitStatus Init() {
    FairRootManager* mngr = FairRootManager::Instance();
    fEvent                = (UEvent*) mngr->GetObject("UEvent.");
    fPt                   = new TH1D("pt", "pt;p_{T} [GeV/c];N", 100, 0, 2);
    return kSUCCESS;
  };

public:
  MyTask() : fEvent(nullptr), fPt(nullptr) {};
  virtual void Exec(Option_t* opt) {
    for (int i = 0; i < fEvent->GetNpa(); i++) {
      UParticle* p = fEvent->GetParticle(i);
      fPt->Fill(p->GetMomentum().Pt());
    }
  }
  virtual void FinishTask() { fPt->Write(); }
  virtual ~MyTask() {};
  ClassDef(MyTask, 1)
};

Warto tu zwrócić uwagę na następujące rzeczy poza samym dziedziczeniem. W konstruktorze wskaźniki są inicjowane jako nullptr, o ile w "zwykłym makrze" nie jest to zwykle konieczne, o tyle przy próbie uruchamiania tasku przez PROOF niemal gwarantuje błąd.

Task posiada kilka metod z których najważniejsze to Exec, Init i FinishTask. Exec jest wywoływana dla każdego zderzenia/wejścia w drzewie, FinishTask wywoływana jest na końcu analizy i zwykle zapisuje tu się wyniki analizy.

Co się tu po kolei dzieje?

  • Pierwsza jest zawsze wywoływana metoda Init, ona w teorii alokuje potrzebne zasoby, ustawia wszystko to co będzie potrzebne przy "właściwej" analizie danych. W naszym przypadku musimy tutaj zrobić dwie rzeczy - "podłączyć" się do danych oraz zaalokować miejsce na histogram z wynikiem analizy. Podłączenie do danych realizujemy przez FairRootManager, to bardzo ważna klasa odpowiedzialna za niemal cały I/O w tym frameworku. Tutaj FairRootManager służy tylko do pobrania danych z zewnątrz (tutaj - pliku z danymi) ale możliwe jest też rejestrowanie własnych danych (FairRootManager::Register) i to w dwóch trybach - zapisem jedynie do pamięci RAM oraz do pliku. W wypadku zapisu do pamięci każdy kolejny task będzie widział zarejestrowane dane tak jakby się znajdowały w drzewie wejściowym, w przypadku zapisu do pliku dodatkowo dane zostaną zapisane w pliku wyjściowym. Kiedy używamy tej opcji? zwykle gdy musimy przekazać dane do innego tasku. Alokacja histogramu to z kolei  wywołanie operatora new. Na koniec zwracamy flagę kSUCCESS, która mówi że wszystko poszło ok i nasz task może być użyty tzn. że nie wywali nam całej reszty kodu.
  • Następnie dla każdego wejścia w drzewie/zarejestrowanego zderzenia wywoływane jest Exec, tutaj przeprowadzamy właściwą analizę - czyli w naszym wypadku robimy pętlę po cząstkach aby wyłuskać rozkład pędu poprzecznego
  • FinishTask jest wywoływane na końcu, tutaj zapisujemy histogram z rozkładem pędu do drzewa wyjściowego.

Makro zaś może wyglądać następująco:

void ana() {
  FairRunAna* analysis = new FairRunAna();
  TString data         = "/opt/cbmroot/cbm_nov/input/urqmd.auau.10gev.centr.root";
  analysis->SetSource(new NicaUnigenSource(data));
  analysis->SetOutputFile("out,root");

  MyTask* t = new MyTask();// w ogolności można dodać dowolną liczbę tasków
  analysis->AddTask(t);
// init musi być wywołane przed run
  analysis->Init();// pętla po zderzeniach 
  analysis->Run(100);
}

Tutaj ważna uwaga - NicaUnigenSource to klasa obecna tylko w niektórych frameworkach (CbmROOT i MpdROOT) umożliwia odczyt danych w "surowym unigenie". Zwykle używa się FairFileSource, które potrafi odczytywać jedynie drzewa wyprodukowane przez FairROOTa. Jeśli chcemy używać własnego drzewa musimy napisać własną klasę dziedziczącą po FairSource.

Zwykle nasze taski są dużo bardziej złożone niż ten tutaj i wymagają stworzenia wręcz całej biblioteki, temu zagadnieniu poświęcona jest kolejna strona.