הכלי ליצירת תרשימים של C++ הוא כלי יעיל במקרים הבאים:
- יצירת תרשימים מורכבים
- גרפים של פרמטרים (למשל, הגדרת נציג ב-
InferenceCalculator
, הפעלה/השבתה של חלקים בתרשים) - כפילויות של תרשימים (למשל, במקום גרפים ייעודיים של מעבד (CPU) ו-GPU ב-pbtxt יכול להיות קוד אחד שיוצר גרפים נדרשים, ולשתף ככל האפשר)
- תמיכה בקלט/פלט אופציונליים בתרשים
- התאמה אישית של תרשימים לפי פלטפורמה
שימוש בסיסי
בואו נראה איך אפשר להשתמש בכלי ליצירת תרשימים C++ ליצירת תרשים פשוט:
# Graph inputs.
input_stream: "input_tensors"
input_side_packet: "model"
# Graph outputs.
output_stream: "output_tensors"
node {
calculator: "InferenceCalculator"
input_stream: "TENSORS:input_tensors"
input_side_packet: "MODEL:model"
output_stream: "TENSORS:output_tensors"
options: {
[drishti.InferenceCalculatorOptions.ext] {
# Requesting GPU delegate.
delegate { gpu {} }
}
}
}
פונקציה שנועדה ליצור את ה-CalculatorGraphConfig
שלמעלה עשויה להיראות כך:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<std::vector<Tensor>> input_tensors =
graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
SidePacket<TfLiteModelPtr> model =
graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();
auto& inference_node = graph.AddNode("InferenceCalculator");
auto& inference_opts =
inference_node.GetOptions<InferenceCalculatorOptions>();
// Requesting GPU delegate.
inference_opts.mutable_delegate()->mutable_gpu();
input_tensors.ConnectTo(inference_node.In("TENSORS"));
model.ConnectTo(inference_node.SideIn("MODEL"));
Stream<std::vector<Tensor>> output_tensors =
inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
// Graph outputs.
output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
סיכום קצר:
- צריך להשתמש ב-
Graph::In/SideIn
כדי לקבל קלט מהגרף בתורStream/SidePacket
- אם משתמשים ב-
Node::Out/SideOut
כדי לקבל פלט של צמתים בתורStream/SidePacket
- באמצעות
Stream/SidePacket::ConnectTo
אפשר לחבר שידורים וחבילות צדדיות אל קלט של צמתים (Node::In/SideIn
) ופלט בתרשים (Graph::Out/SideOut
)- יש 'קיצור דרך'
>>
שאפשר להשתמש בו במקום הפונקציהConnectTo
(למשלx >> node.In("IN")
).
- יש 'קיצור דרך'
Stream/SidePacket::Cast
משמש להפעלת Cast של תוכן או חבילה צדדית שלAnyType
(למשל,Stream<AnyType> in = graph.In(0);
) לסוג מסוים- שימוש בסוגים בפועל במקום ב-
AnyType
מגדיר נתיב טוב יותר על שימוש ביכולות של הכלי ליצירת תרשימים ולשפר את התרשימים קריאה.
- שימוש בסוגים בפועל במקום ב-
שימוש מתקדם
פונקציות שירות
נחלץ את קוד בניית ההסקה לפונקציית שירות ייעודית עזרה לגבי קריאוּת ושימוש חוזר בקוד:
// Updates graph to run inference.
Stream<std::vector<Tensor>> RunInference(
Stream<std::vector<Tensor>> tensors, SidePacket<TfLiteModelPtr> model,
const InferenceCalculatorOptions::Delegate& delegate, Graph& graph) {
auto& inference_node = graph.AddNode("InferenceCalculator");
auto& inference_opts =
inference_node.GetOptions<InferenceCalculatorOptions>();
*inference_opts.mutable_delegate() = delegate;
tensors.ConnectTo(inference_node.In("TENSORS"));
model.ConnectTo(inference_node.SideIn("MODEL"));
return inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<std::vector<Tensor>> input_tensors =
graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
SidePacket<TfLiteModelPtr> model =
graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();
InferenceCalculatorOptions::Delegate delegate;
delegate.mutable_gpu();
Stream<std::vector<Tensor>> output_tensors =
RunInference(input_tensors, model, delegate, graph);
// Graph outputs.
output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));
return graph.GetConfig();
}
כתוצאה מכך, RunInference
מספק ממשק ברור שמציין מהם
קלט/פלט והסוגים שלהם.
אפשר לעשות בו שימוש חוזר, למשל. צריך שיהיו רק כמה שורות אם רוצים להריץ עוד מסקנות מהמודל:
// Run first inference.
Stream<std::vector<Tensor>> output_tensors =
RunInference(input_tensors, model, delegate, graph);
// Run second inference on the output of the first one.
Stream<std::vector<Tensor>> extra_output_tensors =
RunInference(output_tensors, extra_model, delegate, graph);
ואין צורך לשכפל שמות ותגים (InferenceCalculator
,
TENSORS
, MODEL
) או מציגים קבועים ייעודיים פה ושם – אלה
הפרטים מותאמים לשוק המקומי בפונקציה RunInference
.
סוגי שירות
כמובן שלא מדובר רק בפונקציות, במקרים מסוימים זה יכול להועיל הצגת מחלקות של שירותים, שיעזרו לכם ליצור קוד ליצירת גרף קל יותר לקריאה ויש פחות שגיאות.
ב-MediaPipe יש מחשבון PassThroughCalculator
, והוא פשוט מעביר
באמצעות מקורות הקלט שלו:
input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"
output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"
node {
calculator: "PassThroughCalculator"
input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"
# The order must be the same as for inputs (or you can use explicit indexes)
output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"
}
בואו נראה את קוד הבנייה הפשוט של C++ כדי ליצור את התרשים שלמעלה:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();
auto& pass_node = graph.AddNode("PassThroughCalculator");
float_value.ConnectTo(pass_node.In("")[0]);
int_value.ConnectTo(pass_node.In("")[1]);
bool_value.ConnectTo(pass_node.In("")[2]);
Stream<float> passed_float_value = pass_node.Out("")[0].Cast<float>();
Stream<int> passed_int_value = pass_node.Out("")[1].Cast<int>();
Stream<bool> passed_bool_value = pass_node.Out("")[2].Cast<bool>();
// Graph outputs.
passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
אבל ייצוג pbtxt
עשוי להיות מועד לשגיאות (כשאנחנו צריכים להזין הרבה קלטים
), קוד C++ נראה עוד יותר גרוע: תגים ריקים חוזרים וקריאות Cast
. קדימה
לראות איך אנחנו יכולים להשתפר בעזרת PassThroughNodeBuilder
:
class PassThroughNodeBuilder {
public:
explicit PassThroughNodeBuilder(Graph& graph)
: node_(graph.AddNode("PassThroughCalculator")) {}
template <typename T>
Stream<T> PassThrough(Stream<T> stream) {
stream.ConnectTo(node_.In(index_));
return node_.Out(index_++).Cast<T>();
}
private:
int index_ = 0;
GenericNode& node_;
};
עכשיו קוד בניית גרף יכול להיראות כך:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();
PassThroughNodeBuilder pass_node_builder(graph);
Stream<float> passed_float_value = pass_node_builder.PassThrough(float_value);
Stream<int> passed_int_value = pass_node_builder.PassThrough(int_value);
Stream<bool> passed_bool_value = pass_node_builder.PassThrough(bool_value);
// Graph outputs.
passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
מעכשיו אין אפשרות לכלול סדר שגוי או אינדקס שגוי במעבר במהלך עבודות הבנייה
ולשמור קצת על ההקלדה על ידי ניחוש הסוג של Cast
מ-PassThrough
מהקלט.
מה צריך לעשות ומה לא לעשות
אם אפשר, כדאי להגדיר את הקלט של התרשים בהתחלה מאוד
בקוד הבא:
- קשה לנחש כמה קלט הזנתם בתרשים.
- היא יכולה להיות מועדת לשגיאות באופן כללי וקשה לתחזק אותה בעתיד (למשל, האינדקס הנכון? שם? מה קורה אם מסירים קלטים מסוימים או הופכים לאופציונליים? וכו').
- השימוש החוזר של
RunSomething
מוגבל כי בתרשימים אחרים עשויים להיות הבדלים נתוני קלט
לא צריך – דוגמה לקוד פגום.
Stream<D> RunSomething(Stream<A> a, Stream<B> b, Graph& graph) {
Stream<C> c = graph.In(2).SetName("c").Cast<C>(); // Bad.
// ...
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
Stream<A> a = graph.In(0).SetName("a").Cast<A>();
// 10/100/N lines of code.
Stream<B> b = graph.In(1).SetName("b").Cast<B>() // Bad.
Stream<D> d = RunSomething(a, b, graph);
// ...
return graph.GetConfig();
}
במקום זאת, הגדירו את ערכי הקלט של התרשים בתחילת הכלי ליצירת תרשימים:
DO - דוגמה לקוד טוב.
Stream<D> RunSomething(Stream<A> a, Stream<B> b, Stream<C> c, Graph& graph) {
// ...
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).SetName("a").Cast<A>();
Stream<B> b = graph.In(1).SetName("b").Cast<B>();
Stream<C> c = graph.In(2).SetName("c").Cast<C>();
// 10/100/N lines of code.
Stream<D> d = RunSomething(a, b, c, graph);
// ...
return graph.GetConfig();
}
משתמשים ב-std::optional
אם יש זרם קלט או מנה צדדית שלא
מוגדר תמיד, וצריך להזין אותו בהתחלה:
DO - דוגמה לקוד טוב.
std::optional<Stream<A>> a;
if (needs_a) {
a = graph.In(0).SetName(a).Cast<A>();
}
הגדירו את הפלט מהתרשים בסוף
בקוד הבא:
- קשה לנחש כמה פלטים יש לך בתרשים.
- יכולה להיות נוטה לשגיאות באופן כללי וקשה לתחזק אותה בעתיד (למשל, האינדקס הנכון? שם? מה קורה אם מסירים חלק מהפלט או להפוך לאופציונליים? וכו').
- השימוש החוזר של
RunSomething
מוגבל כי בתרשימים אחרים עשויים להופיע תוצאות שונות
לא צריך – דוגמה לקוד פגום.
void RunSomething(Stream<Input> input, Graph& graph) {
// ...
node.Out("OUTPUT_F")
.SetName("output_f").ConnectTo(graph.Out(2)); // Bad.
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// 10/100/N lines of code.
node.Out("OUTPUT_D")
.SetName("output_d").ConnectTo(graph.Out(0)); // Bad.
// 10/100/N lines of code.
node.Out("OUTPUT_E")
.SetName("output_e").ConnectTo(graph.Out(1)); // Bad.
// 10/100/N lines of code.
RunSomething(input, graph);
// ...
return graph.GetConfig();
}
במקום זאת, הגדירו את הפלטים בתרשים בסוף הכלי ליצירת תרשימים:
DO - דוגמה לקוד טוב.
Stream<F> RunSomething(Stream<Input> input, Graph& graph) {
// ...
return node.Out("OUTPUT_F").Cast<F>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// 10/100/N lines of code.
Stream<D> d = node.Out("OUTPUT_D").Cast<D>();
// 10/100/N lines of code.
Stream<E> e = node.Out("OUTPUT_E").Cast<E>();
// 10/100/N lines of code.
Stream<F> f = RunSomething(input, graph);
// ...
// Outputs.
d.SetName("output_d").ConnectTo(graph.Out(0));
e.SetName("output_e").ConnectTo(graph.Out(1));
f.SetName("output_f").ConnectTo(graph.Out(2));
return graph.GetConfig();
}
שמירה על הפרדה בין הצמתים
ב-MediaPipe, סטרימינג של חבילות נתונים וחבילות צדדיות הם בעלי משמעות כמו עיבוד צמתים. דרישות קלט של צמתים ומוצרי פלט מנוסחים בצורה ברורה ובאופן עצמאי, מבחינת השידורים והמנות הצדדית שהוא צריך, יוצרת.
לא צריך – דוגמה לקוד פגום.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
auto& node2 = graph.AddNode("Calculator2");
node1.Out("OUTPUT").ConnectTo(node2.In("INPUT")); // Bad.
auto& node3 = graph.AddNode("Calculator3");
node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B")); // Bad.
node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C")); // Bad.
auto& node4 = graph.AddNode("Calculator4");
node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B")); // Bad.
node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C")); // Bad.
node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D")); // Bad.
// Outputs.
node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0)); // Bad.
node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1)); // Bad.
node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2)); // Bad.
node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3)); // Bad.
return graph.GetConfig();
}
בקוד שלמעלה:
- הצמתים מוצמדים אחד לשני, למשל
node4
יודע איפה נמצאים הקלט שמגיעה מ-(node1
,node2
,node3
) והוא מסבך את הארגון מחדש, תחזוקה ושימוש חוזר בקוד- דפוס שימוש כזה הוא שדרוג לאחור מייצוג ב-Proto, שבו צמתים הם מופרדים כברירת מחדל.
node#.Out("OUTPUT")
השיחות כפולות ויכולת הקריאה נפגעת יכול להשתמש בשמות נקיים יותר במקום זאת, ולספק סוג ממשי.
לכן, כדי לתקן את הבעיות שפורטו למעלה אפשר לכתוב את הקוד הבא של מבנה התרשים:
DO - דוגמה לקוד טוב.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
// `node1` usage is limited to 3 lines below.
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
// `node2` usage is limited to 3 lines below.
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
// `node3` usage is limited to 4 lines below.
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
// `node4` usage is limited to 5 lines below.
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
עכשיו, אם צריך, אפשר להסיר בקלות את node1
ולהפוך את b
לקלט של תרשים
נדרשים עדכונים ל-node2
, node3
, node4
(כמו בייצוג ב-Proto
דרך אגב), כי הם נפרדים זה מזה.
באופן כללי, הקוד שלמעלה משכפל את תרשים הפרוטו בצורה טובה יותר:
input_stream: "a"
node {
calculator: "Calculator1"
input_stream: "INPUT:a"
output_stream: "OUTPUT:b"
}
node {
calculator: "Calculator2"
input_stream: "INPUT:b"
output_stream: "OUTPUT:C"
}
node {
calculator: "Calculator3"
input_stream: "INPUT_B:b"
input_stream: "INPUT_C:c"
output_stream: "OUTPUT:d"
}
node {
calculator: "Calculator4"
input_stream: "INPUT_B:b"
input_stream: "INPUT_C:c"
input_stream: "INPUT_D:d"
output_stream: "OUTPUT:e"
}
output_stream: "b"
output_stream: "c"
output_stream: "d"
output_stream: "e"
בנוסף לכך, עכשיו אפשר לחלץ פונקציות עזר לשימוש חוזר בתרשימים אחרים:
DO - דוגמה לקוד טוב.
Stream<B> RunCalculator1(Stream<A> a, Graph& graph) {
auto& node = graph.AddNode("Calculator1");
a.ConnectTo(node.In("INPUT"));
return node.Out("OUTPUT").Cast<B>();
}
Stream<C> RunCalculator2(Stream<B> b, Graph& graph) {
auto& node = graph.AddNode("Calculator2");
b.ConnectTo(node.In("INPUT"));
return node.Out("OUTPUT").Cast<C>();
}
Stream<D> RunCalculator3(Stream<B> b, Stream<C> c, Graph& graph) {
auto& node = graph.AddNode("Calculator3");
b.ConnectTo(node.In("INPUT_B"));
c.ConnectTo(node.In("INPUT_C"));
return node.Out("OUTPUT").Cast<D>();
}
Stream<E> RunCalculator4(Stream<B> b, Stream<C> c, Stream<D> d, Graph& graph) {
auto& node = graph.AddNode("Calculator4");
b.ConnectTo(node.In("INPUT_B"));
c.ConnectTo(node.In("INPUT_C"));
d.ConnectTo(node.In("INPUT_D"));
return node.Out("OUTPUT").Cast<E>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
Stream<B> b = RunCalculator1(a, graph);
Stream<C> c = RunCalculator2(b, graph);
Stream<D> d = RunCalculator3(b, c, graph);
Stream<E> e = RunCalculator4(b, c, d, graph);
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
צמתים נפרדים לשיפור הקריאוּת
לא צריך – דוגמה לקוד פגום.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
בקוד שלמעלה, קשה להבין את הנקודה שבה כל צומת מתחיל מסתיים. כדי לשפר את התכונה הזו ולעזור לקוראי הקוד, אפשר פשוט להשאיר ריק שורות לפני ואחרי כל צומת:
DO - דוגמה לקוד טוב.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
בנוסף, הייצוג שלמעלה תואם את ה-Proto של CalculatorGraphConfig
שלנו טוב יותר.
אם מחלצים צמתים לפונקציות שירות, הם תחומים בתוך פונקציות וברור איפה הם מתחילים ומסתיימים, אז זה בסדר גמור יש:
DO - דוגמה לקוד טוב.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
Stream<B> b = RunCalculator1(a, graph);
Stream<C> c = RunCalculator2(b, graph);
Stream<D> d = RunCalculator3(b, c, graph);
Stream<E> e = RunCalculator4(b, c, d, graph);
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}