कैलकुलेटर

हर कैलकुलेटर किसी ग्राफ़ का नोड होता है. इसमें नया कैलकुलेटर बनाने, कैलकुलेटर को शुरू करने, इसकी कैलकुलेशन करने, इनपुट और आउटपुट स्ट्रीम, टाइमस्टैंप, और विकल्पों के बारे में बताया गया है. ग्राफ़ में हर नोड को Calculator के तौर पर लागू किया जाता है. ग्राफ़ पर एक्ज़ीक्यूशन की प्रोसेस, इसके कैलकुलेटर में ही होती है. कैलकुलेटर को शून्य या उससे ज़्यादा इनपुट स्ट्रीम और/या साइड पैकेट मिल सकते हैं. साथ ही, यह शून्य या उससे ज़्यादा आउटपुट स्ट्रीम और/या साइड पैकेट पैदा कर सकता है.

CalculatorBase

एक कैलकुलेटर बनाया जाता है. इसके लिए, CalculatorBase क्लास की एक नई सब-क्लास तय की जाती है और कई तरीके लागू किए जाते हैं. साथ ही, MediaPipe के साथ नई सब-क्लास को रजिस्टर किया जाता है. कम से कम, किसी नए कैलकुलेटर को नीचे दिए गए चार तरीकों को लागू करना होगा

  • GetContract()
    • कैलकुलेटर के लेखक, GetContract() में कैलकुलेटर के इनपुट और आउटपुट के अनुमानित प्रकार बता सकते हैं.
  • Open()
    • ग्राफ़ शुरू होने के बाद, फ़्रेमवर्क Open() को कॉल करता है. इनपुट साइड पैकेट इस समय कैलकुलेटर में उपलब्ध होते हैं. Open(), नोड कॉन्फ़िगरेशन की कार्रवाइयों को समझता है (ग्राफ़ देखें). साथ ही, यह कैलकुलेटर की हर ग्राफ़-रन की स्थिति को तैयार करता है. यह फ़ंक्शन कैलकुलेटर आउटपुट के लिए पैकेट भी लिख सकता है. Open() के दौरान कोई गड़बड़ी होने पर, ग्राफ़ चलना बंद हो सकता है.
  • Process()
    • इनपुट वाले कैलकुलेटर के लिए, कम से कम एक इनपुट स्ट्रीम में पैकेट उपलब्ध होने पर, फ़्रेमवर्क Process() को बार-बार कॉल करता है. डिफ़ॉल्ट रूप से फ़्रेमवर्क इस बात की गारंटी देता है कि सभी इनपुट का टाइमस्टैंप एक ही है. ज़्यादा जानकारी के लिए, सिंक करने की सुविधा देखें. पैरलल एक्ज़ीक्यूशन चालू होने पर, एक साथ कई Process() कॉल शुरू किए जा सकते हैं. अगर Process() के दौरान कोई गड़बड़ी होती है, तो फ़्रेमवर्क कॉल Close() हो जाता है और ग्राफ़ रन करने की प्रोसेस खत्म हो जाती है.
  • Close()
    • Process() के सभी कॉल पूरे होने या सभी इनपुट स्ट्रीम बंद होने के बाद, फ़्रेमवर्क Close() को कॉल करता है. इस फ़ंक्शन को हमेशा तब कॉल किया जाता है, जब Open() को कॉल किया गया हो और पूरा हुआ हो. साथ ही, अगर किसी गड़बड़ी की वजह से ग्राफ़ खत्म हो गया हो, तब भी ऐसा किया जाता है. Close() के दौरान, किसी भी इनपुट स्ट्रीम से कोई इनपुट उपलब्ध नहीं होता. हालांकि, इसमें अब भी इनपुट साइड पैकेट का ऐक्सेस होता है. इसलिए, यह आउटपुट लिख सकता है. Close() के वापस लौटने के बाद, कैलकुलेटर को बंद नोड माना जाना चाहिए. ग्राफ़ पूरा होते ही कैलकुलेटर ऑब्जेक्ट को नष्ट कर दिया जाता है.

नीचे दिए गए कोड स्निपेट CalculatorBase.h हैं.

class CalculatorBase {
 public:
  ...

  // The subclasses of CalculatorBase must implement GetContract.
  // ...
  static absl::Status GetContract(CalculatorContract* cc);

  // Open is called before any Process() calls, on a freshly constructed
  // calculator.  Subclasses may override this method to perform necessary
  // setup, and possibly output Packets and/or set output streams' headers.
  // ...
  virtual absl::Status Open(CalculatorContext* cc) {
    return absl::OkStatus();
  }

  // Processes the incoming inputs. May call the methods on cc to access
  // inputs and produce outputs.
  // ...
  virtual absl::Status Process(CalculatorContext* cc) = 0;

  // Is called if Open() was called and succeeded.  Is called either
  // immediately after processing is complete or after a graph run has ended
  // (if an error occurred in the graph).  ...
  virtual absl::Status Close(CalculatorContext* cc) {
    return absl::OkStatus();
  }

  ...
};

कैलकुलेटर की लाइफ़

MediaPipe ग्राफ़ को शुरू करने के दौरान, फ़्रेमवर्क GetContract() स्टैटिक तरीका कॉल करता है. इससे यह तय किया जाता है कि किस तरह के पैकेट होने चाहिए.

यह फ़्रेमवर्क, हर ग्राफ़ को चलाने के लिए पूरे कैलकुलेटर को बनाता और बंद करता है (जैसे कि हर वीडियो के लिए एक बार या हर इमेज के लिए एक बार). पूरे ग्राफ़ पर एक ही तरह के महंगे या बड़े ऑब्जेक्ट को इनपुट साइड पैकेट के तौर पर दिया जाना चाहिए. इससे, एक के बाद एक बार कैलकुलेशन करने पर, एक ही डेटा को बार-बार नहीं गिना जाता.

शुरू करने के बाद, ग्राफ़ के हर रन के लिए, यह क्रम होता है:

  • Open()
  • Process() (बार-बार)
  • Close()

कैलकुलेटर शुरू करने के लिए फ़्रेमवर्क, Open() को कॉल करता है. Open() को किसी भी विकल्प के बारे में समझना चाहिए. साथ ही, हर ग्राफ़ के हिसाब से कैलकुलेटर की स्थिति सेट अप करनी चाहिए. Open() इनपुट साइड पैकेट पा सकता है और कैलकुलेटर आउटपुट के लिए पैकेट लिख सकता है. अगर सही हो, तो इनपुट स्ट्रीम की पैकेट बफ़रिंग को कम करने के लिए SetOffset() को कॉल करना चाहिए.

अगर Open() या Process() के दौरान कोई गड़बड़ी होती है (जैसा कि उनमें से किसी एक से पता चलता है कि कोई Ok स्थिति दिखाता है), तो ग्राफ़ के चलने की प्रोसेस खत्म हो जाती है. साथ ही, कैलकुलेटर के तरीकों को कोई और कॉल नहीं किया जाता. साथ ही, कैलकुलेटर को मिटा दिया जाता है.

इनपुट वाले कैलकुलेटर के लिए, जब भी कम से कम एक इनपुट में पैकेट उपलब्ध होता है, तब फ़्रेमवर्क Process() को कॉल करता है. यह फ़्रेमवर्क इस बात की गारंटी देता है कि सभी इनपुट का टाइमस्टैंप एक ही है. इससे, Process() पर किए जाने वाले हर कॉल के साथ टाइमस्टैंप बढ़ता है और सभी पैकेट डिलीवर किए जाते हैं. इस वजह से, हो सकता है कि Process() को कॉल करने पर, कुछ इनपुट में कोई पैकेट न हो. जिस इनपुट का पैकेट मौजूद नहीं होता है, उससे एक खाली पैकेट (बिना टाइमस्टैंप वाला) बनता है.

Process() पर सभी कॉल करने के बाद, फ़्रेमवर्क Close() को कॉल करता है. सभी इनपुट खत्म हो जाएंगे, लेकिन Close() के पास इनपुट साइड पैकेट का ऐक्सेस है और वह आउटपुट लिख सकता है. 'बंद करें' पर वापस जाने के बाद, कैलकुलेटर बंद हो जाता है.

बिना इनपुट वाले कैलकुलेटर को सोर्स कहा जाता है. सोर्स कैलकुलेटर तब तक Process() कॉल करता रहता है, जब तक वह Ok का स्टेटस दिखाता है. सोर्स कैलकुलेटर बताता है कि स्टॉप स्टेटस दिखाकर यह खत्म हो गया है (जैसे, mediaPipe::tool::StatusStop().).

इनपुट और आउटपुट की पहचान करना

कैलकुलेटर के सार्वजनिक इंटरफ़ेस में इनपुट स्ट्रीम और आउटपुट स्ट्रीम का सेट होता है. CalculatorGraphConfiguration में, कुछ कैलकुलेटर से मिलने वाले आउटपुट, नाम वाली स्ट्रीम का इस्तेमाल करके दूसरे कैलकुलेटर के इनपुट से कनेक्ट किए जाते हैं. स्ट्रीम के नाम आम तौर पर अंग्रेज़ी के छोटे अक्षरों में होते हैं, जबकि इनपुट और आउटपुट टैग आम तौर पर अपरकेस होते हैं. यहां दिए गए उदाहरण में, VIDEO टैग नाम वाला आउटपुट, video_stream नाम वाली स्ट्रीम का इस्तेमाल करके, टैग नाम VIDEO_IN वाले इनपुट से कनेक्ट किया गया है.

# Graph describing calculator SomeAudioVideoCalculator
node {
  calculator: "SomeAudioVideoCalculator"
  input_stream: "INPUT:combined_input"
  output_stream: "VIDEO:video_stream"
}
node {
  calculator: "SomeVideoCalculator"
  input_stream: "VIDEO_IN:video_stream"
  output_stream: "VIDEO_OUT:processed_video"
}

इनपुट और आउटपुट स्ट्रीम की पहचान इंडेक्स नंबर, टैग के नाम या टैग के नाम और इंडेक्स नंबर के कॉम्बिनेशन से की जा सकती है. यहां दिए गए उदाहरण में, इनपुट और आउटपुट आइडेंटिफ़ायर के कुछ उदाहरण दिए गए हैं. SomeAudioVideoCalculator, टैग और इंडेक्स के कॉम्बिनेशन से, टैग से वीडियो आउटपुट और ऑडियो आउटपुट की पहचान करता है. VIDEO टैग वाला इनपुट, video_stream नाम वाली स्ट्रीम से कनेक्ट है. AUDIO टैग के साथ-साथ 0 और 1 इंडेक्स वाले आउटपुट, audio_left और audio_right नाम वाली स्ट्रीम से जुड़े होते हैं. SomeAudioCalculator अपने ऑडियो इनपुट की पहचान सिर्फ़ इंडेक्स से करता है. इसके लिए टैग की ज़रूरत नहीं होती.

# Graph describing calculator SomeAudioVideoCalculator
node {
  calculator: "SomeAudioVideoCalculator"
  input_stream: "combined_input"
  output_stream: "VIDEO:video_stream"
  output_stream: "AUDIO:0:audio_left"
  output_stream: "AUDIO:1:audio_right"
}

node {
  calculator: "SomeAudioCalculator"
  input_stream: "audio_left"
  input_stream: "audio_right"
  output_stream: "audio_energy"
}

कैलकुलेटर लागू करते समय, इनपुट और आउटपुट की पहचान भी टैग के नाम और इंडेक्स नंबर से की जाती है. नीचे दिए गए फ़ंक्शन में, इनपुट और आउटपुट की पहचान की गई है:

  • इंडेक्स नंबर से: कंबाइंड इनपुट स्ट्रीम की पहचान, सिर्फ़ इंडेक्स 0 से की जाती है.
  • टैग के नाम से: वीडियो आउटपुट स्ट्रीम की पहचान "VIDEO" टैग से की जाती है.
  • टैग के नाम और इंडेक्स नंबर से: आउटपुट ऑडियो स्ट्रीम की पहचान, टैग का नाम AUDIO और इंडेक्स नंबर 0 और 1 को एक साथ करके की जाती है.
// c++ Code snippet describing the SomeAudioVideoCalculator GetContract() method
class SomeAudioVideoCalculator : public CalculatorBase {
 public:
  static absl::Status GetContract(CalculatorContract* cc) {
    cc->Inputs().Index(0).SetAny();
    // SetAny() is used to specify that whatever the type of the
    // stream is, it's acceptable.  This does not mean that any
    // packet is acceptable.  Packets in the stream still have a
    // particular type.  SetAny() has the same effect as explicitly
    // setting the type to be the stream's type.
    cc->Outputs().Tag("VIDEO").Set<ImageFrame>();
    cc->Outputs().Get("AUDIO", 0).Set<Matrix>();
    cc->Outputs().Get("AUDIO", 1).Set<Matrix>();
    return absl::OkStatus();
  }

प्रोसेस जारी है

बिना सोर्स नोड पर कॉल किए गए Process() को absl::OkStatus() दिखाना होगा, ताकि यह पता चल सके कि सब कुछ ठीक है या गड़बड़ी का सिग्नल देने वाला कोई अन्य स्टेटस कोड

अगर कोई ऐसा कैलकुलेटर है जो सोर्स नहीं है, तो tool::StatusStop() दिखता है. इसका मतलब है कि ग्राफ़ को जल्दी रद्द किया जा रहा है. इस स्थिति में, सभी सोर्स कैलकुलेटर और ग्राफ़ इनपुट स्ट्रीम बंद कर दिए जाएंगे. साथ ही, बाकी पैकेट ग्राफ़ पर लागू होंगे.

किसी ग्राफ़ में सोर्स नोड तब तक Process() को कॉल करता रहेगा, जब तक वह absl::OkStatus( दिखाता है). यह दिखाने के लिए कि जनरेट होने के लिए कोई और डेटा नहीं है tool::StatusStop(). कोई अन्य स्टेटस बताता है कि कोई गड़बड़ी हुई है.

सफलता दिखाने के लिए, Close() absl::OkStatus() दिखाता है. कोई भी अन्य स्थिति सफल न होने का संकेत देती है.

यहां बेसिक Process() फ़ंक्शन दिया गया है. यह अपने इनपुट डेटा का अनुरोध करने के लिए, Input() तरीके का इस्तेमाल करती है. इस तरीके का इस्तेमाल सिर्फ़ तब किया जा सकता है, जब कैलकुलेटर में एक ही इनपुट हो. इसके बाद, यह आउटपुट पैकेट के लिए ज़रूरी मेमोरी तय करने के लिए, std::unique_ptr का इस्तेमाल करता है और कैलकुलेशन करता है. हो जाने पर, आउटपुट स्ट्रीम में जोड़ते समय पॉइंटर रिलीज़ कर देता है.

absl::Status MyCalculator::Process() {
  const Matrix& input = Input()->Get<Matrix>();
  std::unique_ptr<Matrix> output(new Matrix(input.rows(), input.cols()));
  // do your magic here....
  //    output->row(n) =  ...
  Output()->Add(output.release(), InputTimestamp());
  return absl::OkStatus();
}

कैलकुलेटर के विकल्प

कैलकुलेटर प्रोसेस करने के पैरामीटर को (1) इनपुट स्ट्रीम पैकेट (2) इनपुट साइड पैकेट और (3) कैलकुलेटर के विकल्प के ज़रिए स्वीकार करते हैं. कैलकुलेटर के विकल्प, CalculatorGraphConfiguration.Node मैसेज के node_options फ़ील्ड में लिटरल वैल्यू के तौर पर दिखते हैं.

  node {
    calculator: "TfLiteInferenceCalculator"
    input_stream: "TENSORS:main_model_input"
    output_stream: "TENSORS:main_model_output"
    node_options: {
      [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] {
        model_path: "mediapipe/models/detection_model.tflite"
      }
    }
  }

node_options फ़ील्ड, proto3 सिंटैक्स को स्वीकार करता है. इसके अलावा, कैलकुलेटर के विकल्प options फ़ील्ड में Proto2 सिंटैक्स का इस्तेमाल करके दिए जा सकते हैं.

  node {
    calculator: "TfLiteInferenceCalculator"
    input_stream: "TENSORS:main_model_input"
    output_stream: "TENSORS:main_model_output"
    node_options: {
      [type.googleapis.com/mediapipe.TfLiteInferenceCalculatorOptions] {
        model_path: "mediapipe/models/detection_model.tflite"
      }
    }
  }

सभी कैलकुलेटर, कैलकुलेटर के विकल्प स्वीकार नहीं करते. विकल्पों को स्वीकार करने के लिए, कैलकुलेटर अपने विकल्पों, जैसे कि PacketClonerCalculatorOptions को दिखाने के लिए, आम तौर पर एक नया प्रोटोबफ़ मैसेज टाइप तय करता है. इसके बाद, कैलकुलेटर उस प्रोटोबफ़ मैसेज को CalculatorBase::Open वाले तरीके में पढ़ेगा. साथ ही, वह अपने CalculatorBase::GetContract फ़ंक्शन या CalculatorBase::Process तरीके में भी यह मैसेज पढ़ेगा. आम तौर पर, ".proto" फ़ाइल और mediapipe_proto_library() बिल्ड नियम का इस्तेमाल करके, नए प्रोटोबफ़ मैसेज टाइप को प्रोटोबफ़ स्कीमा के तौर पर परिभाषित किया जाएगा.

  mediapipe_proto_library(
      name = "packet_cloner_calculator_proto",
      srcs = ["packet_cloner_calculator.proto"],
      visibility = ["//visibility:public"],
      deps = [
          "//mediapipe/framework:calculator_options_proto",
          "//mediapipe/framework:calculator_proto",
      ],
  )

कैलकुलेटर का उदाहरण

इस सेक्शन में PacketClonerCalculator को लागू करने के बारे में बताया गया है. यह काम काफ़ी आसान है और इसे कई कैलकुलेटर ग्राफ़ में इस्तेमाल किया जाता है. मांग पर, PacketClonerCalculator सिर्फ़ हाल ही के इनपुट पैकेट की एक कॉपी बनाता है.

PacketClonerCalculator का इस्तेमाल तब किया जाता है, जब पहुंचने वाले डेटा पैकेट के टाइमस्टैंप पूरी तरह से अलाइन नहीं होते. मान लीजिए कि हमारे पास एक कमरा है, जिसमें माइक्रोफ़ोन, लाइट सेंसर, और एक वीडियो कैमरा है, जो सेंसरी डेटा इकट्ठा कर रहा है. हर सेंसर अलग-अलग काम करता है और किसी भी समय डेटा इकट्ठा करता है. मान लें कि हर सेंसर का आउटपुट होगा:

  • माइक्रोफ़ोन = कमरे में आवाज़ के डेसिबल में तेज़ आवाज़ (पूरी संख्या)
  • लाइट सेंसर = कमरे की चमक (पूरी संख्या)
  • वीडियो कैमरा = रूम का आरजीबी इमेज फ़्रेम (ImageFrame)

हमारी आसान कॉन्सेप्ट पाइपलाइन को इन तीन सेंसर से सेंसरी डेटा को इस तरह से प्रोसेस किया गया है कि किसी भी समय हमारे पास कैमरे से इमेज फ़्रेम का डेटा होता है. यह डेटा, पिछली बार इकट्ठा किए गए माइक्रोफ़ोन की आवाज़ और रोशनी सेंसर की चमक के डेटा के साथ सिंक किया जाता है. MediaPipe के साथ ऐसा करने के लिए, हमारी कॉन्सेप्ट पाइपलाइन में तीन इनपुट स्ट्रीम हैं:

  • Room_mic_signal - इस इनपुट स्ट्रीम के डेटा का हर पैकेट एक पूर्णांक डेटा है. इससे पता चलता है कि टाइमस्टैंप वाले रूम में, ऑडियो की आवाज़ कितनी तेज़ है.
  • Room_lightening_sensor - इस इनपुट स्ट्रीम में डेटा का हर पैकेट एक पूर्णांक डेटा है. इससे पता चलता है कि टाइमस्टैंप वाले कमरे में कितनी रोशनी है.
  • Room_video_tick_signal - इस इनपुट स्ट्रीम में डेटा का हर पैकेट, वीडियो डेटा का एक इमेजफ़्रेम है. यह रूम में कैमरे से इकट्ठा किए गए वीडियो को टाइमस्टैंप के साथ दिखाता है.

PacketClonerCalculator को लागू करने की जानकारी नीचे दी गई है. आपके पास GetContract(), Open(), और Process() तरीकों के साथ-साथ इंस्टेंस वैरिएबल current_ देखने का विकल्प भी है. इसमें सबसे नए इनपुट पैकेट होते हैं.

// This takes packets from N+1 streams, A_1, A_2, ..., A_N, B.
// For every packet that appears in B, outputs the most recent packet from each
// of the A_i on a separate stream.

#include <vector>

#include "absl/strings/str_cat.h"
#include "mediapipe/framework/calculator_framework.h"

namespace mediapipe {

// For every packet received on the last stream, output the latest packet
// obtained on all other streams. Therefore, if the last stream outputs at a
// higher rate than the others, this effectively clones the packets from the
// other streams to match the last.
//
// Example config:
// node {
//   calculator: "PacketClonerCalculator"
//   input_stream: "first_base_signal"
//   input_stream: "second_base_signal"
//   input_stream: "tick_signal"
//   output_stream: "cloned_first_base_signal"
//   output_stream: "cloned_second_base_signal"
// }
//
class PacketClonerCalculator : public CalculatorBase {
 public:
  static absl::Status GetContract(CalculatorContract* cc) {
    const int tick_signal_index = cc->Inputs().NumEntries() - 1;
    // cc->Inputs().NumEntries() returns the number of input streams
    // for the PacketClonerCalculator
    for (int i = 0; i < tick_signal_index; ++i) {
      cc->Inputs().Index(i).SetAny();
      // cc->Inputs().Index(i) returns the input stream pointer by index
      cc->Outputs().Index(i).SetSameAs(&cc->Inputs().Index(i));
    }
    cc->Inputs().Index(tick_signal_index).SetAny();
    return absl::OkStatus();
  }

  absl::Status Open(CalculatorContext* cc) final {
    tick_signal_index_ = cc->Inputs().NumEntries() - 1;
    current_.resize(tick_signal_index_);
    // Pass along the header for each stream if present.
    for (int i = 0; i < tick_signal_index_; ++i) {
      if (!cc->Inputs().Index(i).Header().IsEmpty()) {
        cc->Outputs().Index(i).SetHeader(cc->Inputs().Index(i).Header());
        // Sets the output stream of index i header to be the same as
        // the header for the input stream of index i
      }
    }
    return absl::OkStatus();
  }

  absl::Status Process(CalculatorContext* cc) final {
    // Store input signals.
    for (int i = 0; i < tick_signal_index_; ++i) {
      if (!cc->Inputs().Index(i).Value().IsEmpty()) {
        current_[i] = cc->Inputs().Index(i).Value();
      }
    }

    // Output if the tick signal is non-empty.
    if (!cc->Inputs().Index(tick_signal_index_).Value().IsEmpty()) {
      for (int i = 0; i < tick_signal_index_; ++i) {
        if (!current_[i].IsEmpty()) {
          cc->Outputs().Index(i).AddPacket(
              current_[i].At(cc->InputTimestamp()));
          // Add a packet to output stream of index i a packet from inputstream i
          // with timestamp common to all present inputs
        } else {
          cc->Outputs().Index(i).SetNextTimestampBound(
              cc->InputTimestamp().NextAllowedInStream());
          // if current_[i], 1 packet buffer for input stream i is empty, we will set
          // next allowed timestamp for input stream i to be current timestamp + 1
        }
      }
    }
    return absl::OkStatus();
  }

 private:
  std::vector<Packet> current_;
  int tick_signal_index_;
};

REGISTER_CALCULATOR(PacketClonerCalculator);
}  // namespace mediapipe

आम तौर पर, कैलकुलेटर में सिर्फ़ .cc फ़ाइल होती है. .h की ज़रूरत नहीं है, क्योंकि मीडिएटर रजिस्ट्रेशन का इस्तेमाल करके कैलकुलेटर का पता लगाता है. कैलकुलेटर की क्लास तय करने के बाद, उसे मैक्रो सेशन रजिस्टर करें रजिस्टर_CALCULATOR(Calculator_class_name).

यहां एक छोटा-सा MediaPipe ग्राफ़ दिया गया है, जिसमें तीन इनपुट स्ट्रीम, एक नोड (PacketClonerCalculator) और दो आउटपुट स्ट्रीम हैं.

input_stream: "room_mic_signal"
input_stream: "room_lighting_sensor"
input_stream: "room_video_tick_signal"

node {
   calculator: "PacketClonerCalculator"
   input_stream: "room_mic_signal"
   input_stream: "room_lighting_sensor"
   input_stream: "room_video_tick_signal"
   output_stream: "cloned_room_mic_signal"
   output_stream: "cloned_lighting_sensor"
 }

नीचे दिया गया डायग्राम दिखाता है कि PacketClonerCalculator, इनपुट पैकेट की सीरीज़ (टॉप) के आधार पर अपने आउटपुट पैकेट (नीचे) के बारे में कैसे बताता है.

PatetClonerCalculator का इस्तेमाल करके ग्राफ़ बनाएं
जब भी इसे TICK इनपुट स्ट्रीम पर पैकेट मिलता है, तो BoxetClonerCalculator पर हर इनपुट स्ट्रीम से सबसे नया पैकेट दिया जाता है. आउटपुट पैकेट का क्रम (नीचे), इनपुट पैकेट (ऊपर) और उनके टाइमस्टैंप के क्रम से तय होता है. टाइमस्टैंप, डायग्राम की दाईं ओर दिखाए गए हैं.