4386 lines
172 KiB
MQL5
4386 lines
172 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| OMV Panel.mqh |
| |||
//| Copyright 2026, Allan Munene Mutiiria. |
| |||
//| https://t.me/Forex_Algo_Trader |
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "Copyright 2026, Allan Munene Mutiiria."
| |||
#property link "https://t.me/Forex_Algo_Trader"
| |||
| |||
#ifndef OMV_PANEL_MQH
| |||
#define OMV_PANEL_MQH
| |||
| |||
#include "OMV Canvas.mqh"
| |||
#include "OMV Model.mqh"
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Panel size bounds |
| |||
//+------------------------------------------------------------------+
| |||
#define OP_W 660 // Default panel width
| |||
#define OP_H 440 // Default panel height
| |||
#define OP_W_MIN 480 // Narrowest the panel may be dragged
| |||
#define OP_W_MAX 1100 // Widest the panel may be dragged
| |||
#define OP_H_MIN 320 // Shortest the panel may be dragged
| |||
#define OP_H_MAX 800 // Tallest the panel may be dragged
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Band heights |
| |||
//+------------------------------------------------------------------+
| |||
#define OP_HEADER_H 44 // Header band holding the title and icons
| |||
#define OP_STAT_H 48 // Statistics strip under the header
| |||
#define OP_FOOT_H 34 // Footer band holding the view controls
| |||
#define OP_GRIP 14 // Side of the square resize grip
| |||
#define OP_ICON 24 // Square a header or footer icon occupies
| |||
#define OP_GLYPH 0.26 // Reach of a diagonal glyph across its icon
| |||
#define OP_BAR_R 0.28 // Reach of a straight glyph across its icon
| |||
#define OP_FRAME_MS 16 // Shortest gap between two repaints
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Inspector and scrollbars |
| |||
//+------------------------------------------------------------------+
| |||
#define OP_INSPECT_W 230 // Width the inspector occupies when open
| |||
#define OP_NAME_W 72 // Width of the name column beside a value
| |||
#define OP_VAL_PAD 5 // Padding inside a value box
| |||
#define OP_VAL_H 20 // Height of a single line value box
| |||
#define OP_VAL_GAP 3 // Space between two value boxes
| |||
#define OP_MAX_ROWS 64 // Value rows the inspector may hold
| |||
#define OP_MAX_LINES 400 // Lines one row may add when it is opened
| |||
#define OP_WRAP_MAX 4 // Lines a single value may wrap onto
| |||
#define OP_BAR 8 // Thickness of a scrollbar
| |||
#define OP_WHEEL 40 // Pixels one wheel notch moves the graph
| |||
#define OP_RADIUS 10 // Corner radius of the panel itself
| |||
#define OP_BTN_R 6 // Corner radius of a button
| |||
#define OP_PAD 14 // Breathing room around the graph inside its area
| |||
#define OP_ZOOM_MIN 0.30 // Smallest scale the graph is drawn at
| |||
#define OP_ZOOM_MAX 2.00 // Largest scale the graph is drawn at
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Hit targets |
| |||
//+------------------------------------------------------------------+
| |||
enum OMV_HIT
| |||
{
| |||
OH_NONE = 0, // Pointer is over nothing clickable
| |||
OH_HEADER, // Header band, starts a move
| |||
OH_THEME, // Light and dark toggle
| |||
OH_MIN, // Collapse to the header band
| |||
OH_RESTORE, // Expand back to the full panel
| |||
OH_CLOSE, // Remove the panel
| |||
OH_FIT, // Scale the graph to the canvas
| |||
OH_VIEW, // Swap between the graph and the neurons
| |||
OH_ZOOM_OUT, // Step the zoom down
| |||
OH_ZOOM_IN, // Step the zoom up
| |||
OH_INSPECT_X, // Close the inspector column
| |||
OH_GRIP, // Bottom right grip, starts a resize
| |||
OH_BAR_V, // Vertical scrollbar thumb
| |||
OH_BAR_H, // Horizontal scrollbar thumb
| |||
OH_CANVAS, // Graph area, starts a pan
| |||
OH_NODE, // A drawn node box
| |||
OH_ROW, // A tensor row inside a node box
| |||
OH_LINK, // A drawn connection
| |||
OH_INFO, // An expandable row of the inspector
| |||
OH_INFO_BAR // The inspector scrollbar thumb
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| One laid out row of the inspector |
| |||
//+------------------------------------------------------------------+
| |||
struct OmvRow
| |||
{
| |||
// Name the row carries on its left
| |||
string name;
| |||
// Print the value inside the box
| |||
string value;
| |||
// Print these lines once the row is expanded
| |||
string extra[OP_MAX_LINES];
| |||
// Count the lines the row adds when opened
| |||
int extraCount;
| |||
// Count the lines the value itself wrapped onto
| |||
int wraps;
| |||
// Mark a row that can be expanded
| |||
bool canOpen;
| |||
// Mark a row that is currently expanded
| |||
bool isOpen;
| |||
// Mark a heading rather than a value
| |||
bool isHead;
| |||
// Name the value in the graph this row points at
| |||
string linkName;
| |||
// Place the box top edge
| |||
int y;
| |||
// Measure the box height
| |||
int h;
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| What the inspector is describing |
| |||
//+------------------------------------------------------------------+
| |||
enum OMV_PICK
| |||
{
| |||
OP_PICK_NONE = 0, // Nothing chosen
| |||
OP_PICK_NODE, // A layer of the graph
| |||
OP_PICK_TENSOR, // A stored weight tensor
| |||
OP_PICK_LINK, // A connection between two layers
| |||
OP_PICK_GRAPH, // The graph as a whole
| |||
OP_PICK_MODEL // The file as a whole
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Display mode |
| |||
//+------------------------------------------------------------------+
| |||
enum OMV_MODE
| |||
{
| |||
OM_FULL = 0, // Whole panel
| |||
OM_MIN // Header band only
| |||
};
| |||
//+------------------------------------------------------------------+
| |||
//| Resolved theme colors |
| |||
//+------------------------------------------------------------------+
| |||
struct OmvTheme
| |||
{
| |||
// Color the panel body
| |||
uint panel;
| |||
// Color the header and footer bands
| |||
uint chrome;
| |||
// Color the panel outline
| |||
uint border;
| |||
// Color the band separators
| |||
uint divider;
| |||
// Color the faint canvas grid
| |||
uint grid;
| |||
// Color the primary text
| |||
uint text;
| |||
// Color the secondary text
| |||
uint muted;
| |||
// Color the accent used for the active control
| |||
uint accent;
| |||
// Color the compute node fill
| |||
uint computeFill;
| |||
// Color the compute node edge
| |||
uint computeEdge;
| |||
// Color the activation node fill
| |||
uint activateFill;
| |||
// Color the activation node edge
| |||
uint activateEdge;
| |||
// Color the structural node fill
| |||
uint shapeFill;
| |||
// Color the structural node edge
| |||
uint shapeEdge;
| |||
// Color the output node fill
| |||
uint outputFill;
| |||
// Color the output node edge
| |||
uint outputEdge;
| |||
// Color a connection carrying a positive weight
| |||
uint linkUp;
| |||
// Color a connection carrying a negative weight
| |||
uint linkDown;
| |||
// Color a neuron circle
| |||
uint cell;
| |||
// Color the plain node fill
| |||
uint plainFill;
| |||
// Color the plain node edge
| |||
uint plainEdge;
| |||
// Color a button face
| |||
uint btnFace;
| |||
// Color a button edge
| |||
uint btnEdge;
| |||
// Color a hovered button face
| |||
uint hoverFace;
| |||
// Color a hovered button edge
| |||
uint hoverEdge;
| |||
| |||
void Apply(const bool dark);
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Resolve every color for the requested theme |
| |||
//+------------------------------------------------------------------+
| |||
void OmvTheme::Apply(const bool dark)
| |||
{
| |||
//--- Resolve the dark palette
| |||
if(dark)
| |||
{
| |||
panel = ColorToARGB(C'24,24,27', 255);
| |||
chrome = ColorToARGB(C'32,32,36', 255);
| |||
border = ColorToARGB(C'58,58,64', 255);
| |||
divider = ColorToARGB(C'48,48,53', 255);
| |||
grid = ColorToARGB(C'38,38,42', 255);
| |||
text = ColorToARGB(C'232,232,236', 255);
| |||
muted = ColorToARGB(C'138,138,146', 255);
| |||
accent = ColorToARGB(C'163,155,242', 255);
| |||
computeFill = ColorToARGB(C'22,44,68', 255);
| |||
computeEdge = ColorToARGB(C'88,150,214', 255);
| |||
activateFill = ColorToARGB(C'62,32,22', 255);
| |||
activateEdge = ColorToARGB(C'214,120,88', 255);
| |||
shapeFill = ColorToARGB(C'62,28,44', 255);
| |||
shapeEdge = ColorToARGB(C'214,110,150', 255);
| |||
outputFill = ColorToARGB(C'20,54,44', 255);
| |||
outputEdge = ColorToARGB(C'80,190,152', 255);
| |||
linkUp = ColorToARGB(C'80,190,152', 255);
| |||
linkDown = ColorToARGB(C'232,110,110', 255);
| |||
cell = ColorToARGB(C'150,170,195', 255);
| |||
plainFill = ColorToARGB(C'42,42,46', 255);
| |||
plainEdge = ColorToARGB(C'110,110,118', 255);
| |||
btnFace = ColorToARGB(C'40,40,45', 255);
| |||
btnEdge = ColorToARGB(C'62,62,68', 255);
| |||
hoverFace = ColorToARGB(C'52,52,58', 255);
| |||
hoverEdge = ColorToARGB(C'92,92,100', 255);
| |||
return;
| |||
}
| |||
//--- Resolve the light palette
| |||
panel = ColorToARGB(C'255,255,255', 255);
| |||
chrome = ColorToARGB(C'247,246,243', 255);
| |||
border = ColorToARGB(C'208,206,196', 255);
| |||
divider = ColorToARGB(C'228,226,219', 255);
| |||
grid = ColorToARGB(C'241,239,232', 255);
| |||
text = ColorToARGB(C'44,44,42', 255);
| |||
muted = ColorToARGB(C'126,124,118', 255);
| |||
accent = ColorToARGB(C'127,119,221', 255);
| |||
computeFill = ColorToARGB(C'230,241,251', 255);
| |||
computeEdge = ColorToARGB(C'55,138,221', 255);
| |||
activateFill = ColorToARGB(C'250,236,231', 255);
| |||
activateEdge = ColorToARGB(C'216,90,48', 255);
| |||
shapeFill = ColorToARGB(C'251,234,240', 255);
| |||
shapeEdge = ColorToARGB(C'212,83,126', 255);
| |||
outputFill = ColorToARGB(C'225,245,238', 255);
| |||
outputEdge = ColorToARGB(C'29,158,117', 255);
| |||
linkUp = ColorToARGB(C'29,158,117', 255);
| |||
linkDown = ColorToARGB(C'201,58,58', 255);
| |||
cell = ColorToARGB(C'92,110,132', 255);
| |||
plainFill = ColorToARGB(C'241,239,232', 255);
| |||
plainEdge = ColorToARGB(C'180,178,169', 255);
| |||
btnFace = ColorToARGB(C'255,255,255', 255);
| |||
btnEdge = ColorToARGB(C'211,209,199', 255);
| |||
hoverFace = ColorToARGB(C'247,246,243', 255);
| |||
hoverEdge = ColorToARGB(C'180,178,169', 255);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Box geometry |
| |||
//+------------------------------------------------------------------+
| |||
#define OL_BOX_W 76 // Width of one node box
| |||
#define OL_HEAD_H 17 // Height of the header strip naming the operator
| |||
#define OL_ROW_H 13 // Height of one listed weight row
| |||
#define OL_ROW_PAD 3 // Margin around the listed rows on every side
| |||
#define OL_BOX_S 19 // Height of a node box carrying a title only
| |||
#define OL_MAX_ROWS 16 // Weight rows one box may list
| |||
#define OL_GAP_X 22 // Horizontal space between neighbors on a rank
| |||
#define OL_GAP_Y 26 // Vertical space where a connection carries a label
| |||
#define OL_GAP_S 14 // Vertical space where the connections carry none
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Node roles used for coloring |
| |||
//+------------------------------------------------------------------+
| |||
#define OL_ROLE_COMPUTE 0 // Layers holding weights
| |||
#define OL_ROLE_ACTIVATE 1 // Elementwise activations
| |||
#define OL_ROLE_SHAPE 2 // Structural operators
| |||
#define OL_ROLE_OUTPUT 3 // Terminal operators
| |||
#define OL_ROLE_PLAIN 4 // Anything else
| |||
#define OL_ROLE_PORT 5 // A graph input or output
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Placed graph node |
| |||
//+------------------------------------------------------------------+
| |||
struct OmvBox
| |||
{
| |||
// Index the node this box was placed for, or a negative for a port
| |||
int node;
| |||
// Name a port box, empty on a layer box
| |||
string portName;
| |||
// Place the left edge in graph space
| |||
int x;
| |||
// Place the top edge in graph space
| |||
int y;
| |||
// Measure the box width
| |||
int w;
| |||
// Measure the box height
| |||
int h;
| |||
// Depth this box sits at
| |||
int rank;
| |||
// Role that decides the box coloring
| |||
int role;
| |||
// Letter the operator calls each listed input
| |||
string rowLabel[OL_MAX_ROWS];
| |||
// Shape text drawn beside each listed input
| |||
string rowText[OL_MAX_ROWS];
| |||
// Index the tensor each listed input names
| |||
int rowTensor[OL_MAX_ROWS];
| |||
// Name the value each listed input reads
| |||
string rowName[OL_MAX_ROWS];
| |||
// Count the rows this box lists
| |||
int rowCount;
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Drawn connection between two boxes |
| |||
//+------------------------------------------------------------------+
| |||
struct OmvEdge
| |||
{
| |||
// Index the box the edge leaves
| |||
int from;
| |||
// Index the box the edge enters
| |||
int to;
| |||
// Name the value the connection carries
| |||
string label;
| |||
// Shape text drawn beside the connection
| |||
string shape;
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Neuron view limits |
| |||
//+------------------------------------------------------------------+
| |||
#define ON_MAX_BANDS 32 // Neuron rows the view may stack
| |||
#define ON_MAX_NEURONS 4096 // Circles the view may draw in total
| |||
#define ON_MAX_LINES 20000 // Connections the view may draw before thinning
| |||
#define ON_BAND_GAP 88 // Vertical space between one row and the next
| |||
#define ON_DOT 11 // Diameter of one neuron at full zoom
| |||
#define ON_DOT_GAP 8 // Space between two neurons on the same row
| |||
#define ON_TOP_PAD 24 // Room above the first row for its caption
| |||
#define ON_RANK_GAP 52 // Space between two rows sharing a depth
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| One row of neurons the view draws |
| |||
//+------------------------------------------------------------------+
| |||
struct OmvBand
| |||
{
| |||
// Count the neurons standing on this row
| |||
int count;
| |||
// Name what produced the row and what was applied to it
| |||
string label;
| |||
// Place the row across the arrangement
| |||
int x;
| |||
// Place the row down the arrangement
| |||
int y;
| |||
// Measure how wide the row of circles runs
| |||
int w;
| |||
// Record how far the row sits from the graph input
| |||
int rank;
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| One run of connections between two rows |
| |||
//+------------------------------------------------------------------+
| |||
struct OmvWeb
| |||
{
| |||
// Name the row the connections leave
| |||
int from;
| |||
// Name the row the connections arrive at
| |||
int to;
| |||
// Index the tensor holding the weights, or a negative when structural
| |||
int tensor;
| |||
// Offset the arriving row when several runs share it
| |||
int offset;
| |||
// Mark a weight matrix the operator stores the other way round
| |||
bool transposed;
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Neuron arrangement built from the dense layers |
| |||
//+------------------------------------------------------------------+
| |||
class COmvNeurons
| |||
{
| |||
private:
| |||
// Store the rows of neurons
| |||
OmvBand m_band[];
| |||
// Store the runs of connections
| |||
OmvWeb m_web[];
| |||
// Count the rows held
| |||
int m_bands;
| |||
// Count the runs held
| |||
int m_webs;
| |||
// Measure the arrangement width
| |||
int m_width;
| |||
// Measure the arrangement height
| |||
int m_height;
| |||
// Explain why the view cannot be drawn
| |||
string m_reason;
| |||
// Report whether the arrangement is drawable
| |||
bool m_ready;
| |||
| |||
int BandOf(const string name, const string &names[], const int &bands[],
| |||
const int count) const;
| |||
int WidthOf(COmvModel &model, const string name) const;
| |||
int TensorNamed(COmvModel &model, const string name) const;
| |||
bool IsDense(const string op) const;
| |||
bool IsUnary(const string op) const;
| |||
bool IsBinary(const string op) const;
| |||
bool IsShape(const string op) const;
| |||
int AddBand(const int count, const string label);
| |||
| |||
public:
| |||
COmvNeurons(void);
| |||
bool Build(COmvModel &model);
| |||
void Clear(void);
| |||
| |||
int Bands(void) const { return m_bands; }
| |||
int Webs(void) const { return m_webs; }
| |||
int Width(void) const { return m_width; }
| |||
int Height(void) const { return m_height; }
| |||
string Reason(void) const { return m_reason; }
| |||
bool Ready(void) const { return m_ready; }
| |||
| |||
void BandAt(const int i, OmvBand &out) const { if(i >= 0 && i < m_bands) out = m_band[i]; }
| |||
void WebAt(const int i, OmvWeb &out) const { if(i >= 0 && i < m_webs) out = m_web[i]; }
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Arranges a parsed model into boxes and edges |
| |||
//+------------------------------------------------------------------+
| |||
class COmvLayout
| |||
{
| |||
private:
| |||
// Store the placed boxes
| |||
OmvBox m_box[];
| |||
// Store the drawn edges
| |||
OmvEdge m_edge[];
| |||
// Count the boxes placed
| |||
int m_boxes;
| |||
// Count the edges drawn
| |||
int m_edges;
| |||
// Measure the arrangement width
| |||
int m_width;
| |||
// Measure the arrangement height
| |||
int m_height;
| |||
| |||
int RoleOf(const string op) const;
| |||
int ProducerOf(COmvModel &model, const string name) const;
| |||
bool HasNodeParent(COmvModel &model, const OmvNode &node) const;
| |||
int ShallowestConsumer(COmvModel &model, const int index, const int &rank[]) const;
| |||
bool IsWeight(COmvModel &model, const string name) const;
| |||
void RowsOf(COmvModel &model, const OmvNode &node, OmvBox &box) const;
| |||
void AddPortBox(const string name, const int rank);
| |||
void AddEdge(const int from, const int to, const string label,
| |||
const string shape);
| |||
string ShapeOf(COmvModel &model, const string name) const;
| |||
| |||
public:
| |||
COmvLayout(void);
| |||
void Clear(void);
| |||
bool Build(COmvModel &model);
| |||
| |||
int Boxes(void) const { return m_boxes; }
| |||
int Edges(void) const { return m_edges; }
| |||
int Width(void) const { return m_width; }
| |||
int Height(void) const { return m_height; }
| |||
void BoxAt(const int i, OmvBox &out) const { if(i >= 0 && i < m_boxes) out = m_box[i]; }
| |||
void EdgeAt(const int i, OmvEdge &out) const { if(i >= 0 && i < m_edges) out = m_edge[i]; }
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Construct an empty neuron arrangement |
| |||
//+------------------------------------------------------------------+
| |||
COmvNeurons::COmvNeurons(void)
| |||
{
| |||
//--- Start with nothing placed
| |||
Clear();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Drop the arrangement built for a previous model |
| |||
//+------------------------------------------------------------------+
| |||
void COmvNeurons::Clear(void)
| |||
{
| |||
//--- Zero the counts that bound both arrays
| |||
m_bands = 0;
| |||
m_webs = 0;
| |||
//--- Release the stored rows and runs
| |||
ArrayResize(m_band, 0);
| |||
ArrayResize(m_web, 0);
| |||
//--- Clear the measurements and the verdict
| |||
m_width = 0;
| |||
m_height = 0;
| |||
m_reason = "";
| |||
m_ready = false;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Report whether an operator multiplies by a weight matrix |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvNeurons::IsDense(const string op) const
| |||
{
| |||
//--- Only these two carry a two dimensional weight between neuron rows
| |||
return (op == "Gemm" || op == "MatMul");
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Report whether an operator works on one value in place |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvNeurons::IsUnary(const string op) const
| |||
{
| |||
//--- These change the values on a row without changing how many there are
| |||
return (op == "Relu" || op == "Tanh" || op == "Sigmoid" || op == "Softmax" ||
| |||
op == "LeakyRelu" || op == "Elu" || op == "Sqrt" || op == "Abs" ||
| |||
op == "Cast" || op == "Identity" || op == "Clip" || op == "Erf");
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Report whether an operator pairs a value with a constant |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvNeurons::IsBinary(const string op) const
| |||
{
| |||
//--- These scale or shift a row, leaving its neuron count alone
| |||
return (op == "Mul" || op == "Add" || op == "Sub" || op == "Div" || op == "Pow");
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Report whether an operator only rearranges a value |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvNeurons::IsShape(const string op) const
| |||
{
| |||
//--- These move the numbers around without creating or destroying any
| |||
return (op == "Reshape" || op == "Transpose" || op == "Flatten" ||
| |||
op == "Squeeze" || op == "Unsqueeze");
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the row a named value stands on |
| |||
//+------------------------------------------------------------------+
| |||
int COmvNeurons::BandOf(const string name, const string &names[], const int &bands[],
| |||
const int count) const
| |||
{
| |||
//--- Scan the names already placed for a match
| |||
for(int i = 0; i < count; i++)
| |||
if(names[i] == name)
| |||
return bands[i];
| |||
//--- Report that the value stands on no row
| |||
return -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Take how many numbers a named value carries |
| |||
//+------------------------------------------------------------------+
| |||
int COmvNeurons::WidthOf(COmvModel &model, const string name) const
| |||
{
| |||
//--- A graph input announces its own shape
| |||
for(int i = 0; i < model.Inputs(); i++)
| |||
{
| |||
//--- Fetch this graph input
| |||
OmvPort port;
| |||
model.InputAt(i, port);
| |||
//--- Take the last axis, which is the count of numbers
| |||
if(port.name == name && port.dims > 0)
| |||
return port.dim[port.dims - 1];
| |||
}
| |||
//--- A value passing between layers announces its shape when the file says so
| |||
for(int i = 0; i < model.Inners(); i++)
| |||
{
| |||
//--- Fetch this inner value
| |||
OmvPort port;
| |||
model.InnerAt(i, port);
| |||
//--- Take the last axis, which is the count of numbers
| |||
if(port.name == name && port.dims > 0)
| |||
return port.dim[port.dims - 1];
| |||
}
| |||
//--- A graph output announces its own shape
| |||
for(int i = 0; i < model.Outputs(); i++)
| |||
{
| |||
//--- Fetch this graph output
| |||
OmvPort port;
| |||
model.OutputAt(i, port);
| |||
//--- Take the last axis, which is the count of numbers
| |||
if(port.name == name && port.dims > 0)
| |||
return port.dim[port.dims - 1];
| |||
}
| |||
//--- Report nothing for a value the file never shapes
| |||
return 0;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the tensor a name belongs to |
| |||
//+------------------------------------------------------------------+
| |||
int COmvNeurons::TensorNamed(COmvModel &model, const string name) const
| |||
{
| |||
//--- Scan every stored tensor for a matching name
| |||
for(int i = 0; i < model.Tensors(); i++)
| |||
{
| |||
//--- Fetch this tensor
| |||
OmvTensor tensor;
| |||
model.TensorAt(i, tensor);
| |||
//--- Report the one that carries the name
| |||
if(tensor.name == name)
| |||
return i;
| |||
}
| |||
//--- Report that no tensor carries it
| |||
return -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Open a new row of neurons |
| |||
//+------------------------------------------------------------------+
| |||
int COmvNeurons::AddBand(const int count, const string label)
| |||
{
| |||
//--- Refuse a row beyond what the view may stack
| |||
if(m_bands >= ON_MAX_BANDS || count <= 0)
| |||
return -1;
| |||
//--- Describe the row
| |||
OmvBand band;
| |||
band.count = count;
| |||
band.label = label;
| |||
band.x = 0;
| |||
band.y = 0;
| |||
band.w = 0;
| |||
band.rank = 0;
| |||
//--- Append it to the arrangement
| |||
ArrayResize(m_band, m_bands + 1);
| |||
m_band[m_bands++] = band;
| |||
return m_bands - 1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Follow the value names into rows of neurons |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvNeurons::Build(COmvModel &model)
| |||
{
| |||
//--- Drop whatever a previous model left behind
| |||
Clear();
| |||
//--- Refuse a graph that exposes nothing to start from
| |||
if(model.Inputs() <= 0)
| |||
{
| |||
m_reason = "the graph exposes no input";
| |||
return false;
| |||
}
| |||
//--- Track which row each value name stands on
| |||
string names[];
| |||
int bands[];
| |||
int named = 0;
| |||
ArrayResize(names, OMV_MAX_NODES * 2);
| |||
ArrayResize(bands, OMV_MAX_NODES * 2);
| |||
//--- Open the first row from the graph input
| |||
OmvPort first;
| |||
model.InputAt(0, first);
| |||
const int firstWide = WidthOf(model, first.name);
| |||
if(firstWide <= 0)
| |||
{
| |||
m_reason = "the graph input carries no shape";
| |||
return false;
| |||
}
| |||
names[named] = first.name;
| |||
bands[named] = AddBand(firstWide, first.name);
| |||
named++;
| |||
//--- Walk the layers in file order, opening a row wherever weights appear
| |||
for(int i = 0; i < model.Nodes(); i++)
| |||
{
| |||
//--- Fetch this layer
| |||
OmvNode node;
| |||
model.NodeAt(i, node);
| |||
//--- Find which of its inputs already stand on a row
| |||
int fed[8];
| |||
int feds = 0;
| |||
for(int j = 0; j < node.feedCount && feds < 8; j++)
| |||
{
| |||
//--- Keep only the names a previous layer produced
| |||
const int b = BandOf(node.feeds[j], names, bands, named);
| |||
if(b >= 0)
| |||
fed[feds++] = b;
| |||
}
| |||
//--- Find a two dimensional weight among its inputs
| |||
int weight = -1;
| |||
for(int j = 0; j < node.feedCount; j++)
| |||
{
| |||
//--- Take the first stored tensor carrying two axes
| |||
const int k = TensorNamed(model, node.feeds[j]);
| |||
if(k < 0)
| |||
continue;
| |||
OmvTensor probe;
| |||
model.TensorAt(k, probe);
| |||
if(probe.dims == 2)
| |||
{
| |||
weight = k;
| |||
break;
| |||
}
| |||
}
| |||
//--- A dense layer opens a row and connects to it through its weights
| |||
if(IsDense(node.op) && feds > 0 && weight >= 0)
| |||
{
| |||
//--- Take whether the operator stores its weights the other way round
| |||
bool flipped = false;
| |||
for(int a = 0; a < node.attrCount; a++)
| |||
if(node.attrName[a] == "transB" && node.attrValue[a] == "1")
| |||
flipped = true;
| |||
//--- Take the outgoing width from whichever axis the storage implies
| |||
OmvTensor w;
| |||
model.TensorAt(weight, w);
| |||
const int made = AddBand(flipped ? w.dim[0] : w.dim[1], node.op);
| |||
if(made < 0)
| |||
{
| |||
m_reason = "the model has more layers than the view can stack";
| |||
return false;
| |||
}
| |||
//--- Record the run of connections the weight describes
| |||
OmvWeb web;
| |||
web.from = fed[0];
| |||
web.to = made;
| |||
web.tensor = weight;
| |||
web.offset = 0;
| |||
web.transposed = flipped;
| |||
ArrayResize(m_web, m_webs + 1);
| |||
m_web[m_webs++] = web;
| |||
//--- Stand the produced name on the new row
| |||
if(node.emitCount > 0 && named < ArraySize(names))
| |||
{
| |||
names[named] = node.emits[0];
| |||
bands[named] = made;
| |||
named++;
| |||
}
| |||
continue;
| |||
}
| |||
//--- A joining layer opens a row that its sources feed positionally
| |||
if(node.op == "Concat" && feds >= 2)
| |||
{
| |||
//--- Add the widths of every row arriving here
| |||
int total = 0;
| |||
for(int j = 0; j < feds; j++)
| |||
total += m_band[fed[j]].count;
| |||
const int made = AddBand(total, "Concat");
| |||
if(made < 0)
| |||
{
| |||
m_reason = "the model has more layers than the view can stack";
| |||
return false;
| |||
}
| |||
//--- Record one structural run per source, each at its own offset
| |||
int at = 0;
| |||
for(int j = 0; j < feds; j++)
| |||
{
| |||
//--- Note where this source lands on the joined row
| |||
OmvWeb web;
| |||
web.from = fed[j];
| |||
web.to = made;
| |||
web.tensor = -1;
| |||
web.offset = at;
| |||
web.transposed = false;
| |||
ArrayResize(m_web, m_webs + 1);
| |||
m_web[m_webs++] = web;
| |||
at += m_band[fed[j]].count;
| |||
}
| |||
//--- Stand the produced name on the new row
| |||
if(node.emitCount > 0 && named < ArraySize(names))
| |||
{
| |||
names[named] = node.emits[0];
| |||
bands[named] = made;
| |||
named++;
| |||
}
| |||
continue;
| |||
}
| |||
//--- These change the numbers on a row without changing how many there are
| |||
if((IsUnary(node.op) || IsBinary(node.op) || IsShape(node.op)) && feds > 0)
| |||
{
| |||
//--- Stand the produced name on the same row and note what was applied
| |||
if(node.emitCount > 0 && named < ArraySize(names))
| |||
{
| |||
names[named] = node.emits[0];
| |||
bands[named] = fed[0];
| |||
named++;
| |||
}
| |||
if(IsUnary(node.op) || IsBinary(node.op))
| |||
m_band[fed[0]].label += " + " + node.op;
| |||
continue;
| |||
}
| |||
//--- A layer reading only stored tensors touches no row at all
| |||
if(feds == 0)
| |||
continue;
| |||
//--- A dense layer whose weights are computed rather than stored
| |||
if(IsDense(node.op))
| |||
{
| |||
m_reason = node.op + " takes its weights from another layer, not from a stored tensor";
| |||
return false;
| |||
}
| |||
//--- Anything else has no neurons to draw, so the view cannot be honest
| |||
m_reason = "the model uses " + node.op + ", which has no neuron pairs to draw";
| |||
return false;
| |||
}
| |||
//--- Refuse a graph that produced no weighted connection at all
| |||
if(m_webs <= 0 || m_bands < 2)
| |||
{
| |||
m_reason = "the model has no dense layers";
| |||
return false;
| |||
}
| |||
//--- Count what the arrangement would cost to draw
| |||
int neurons = 0;
| |||
for(int i = 0; i < m_bands; i++)
| |||
neurons += m_band[i].count;
| |||
if(neurons > ON_MAX_NEURONS)
| |||
{
| |||
m_reason = "the model has " + IntegerToString(neurons) + " neurons to draw";
| |||
return false;
| |||
}
| |||
//--- Take how deep each row sits by walking the runs until nothing moves
| |||
for(int pass = 0; pass < m_bands; pass++)
| |||
{
| |||
//--- Push every target one step deeper than the source that feeds it
| |||
bool moved = false;
| |||
for(int i = 0; i < m_webs; i++)
| |||
{
| |||
//--- Deepen the target only when the source now sits deeper
| |||
const int want = m_band[m_web[i].from].rank + 1;
| |||
if(m_band[m_web[i].to].rank < want)
| |||
{
| |||
m_band[m_web[i].to].rank = want;
| |||
moved = true;
| |||
}
| |||
}
| |||
//--- Stop as soon as a whole pass changes nothing
| |||
if(!moved)
| |||
break;
| |||
}
| |||
//--- Measure every row and find how deep the arrangement runs
| |||
int deepest = 0;
| |||
for(int i = 0; i < m_bands; i++)
| |||
{
| |||
//--- Take the width the circles occupy on this row
| |||
m_band[i].w = m_band[i].count * ON_DOT + (m_band[i].count - 1) * ON_DOT_GAP;
| |||
if(m_band[i].rank > deepest)
| |||
deepest = m_band[i].rank;
| |||
}
| |||
//--- Lay every rank across, so rows sharing a depth sit side by side
| |||
int widest = 0;
| |||
for(int r = 0; r <= deepest; r++)
| |||
{
| |||
//--- Add the widths of every row at this depth
| |||
int across = 0, seen = 0;
| |||
for(int i = 0; i < m_bands; i++)
| |||
if(m_band[i].rank == r)
| |||
{
| |||
across += m_band[i].w;
| |||
seen++;
| |||
}
| |||
if(seen > 1)
| |||
across += (seen - 1) * ON_RANK_GAP;
| |||
if(across > widest)
| |||
widest = across;
| |||
}
| |||
//--- Place each row against the widest rank, centered as a group
| |||
for(int r = 0; r <= deepest; r++)
| |||
{
| |||
//--- Measure this rank again to know where the group starts
| |||
int across = 0, seen = 0;
| |||
for(int i = 0; i < m_bands; i++)
| |||
if(m_band[i].rank == r)
| |||
{
| |||
across += m_band[i].w;
| |||
seen++;
| |||
}
| |||
if(seen > 1)
| |||
across += (seen - 1) * ON_RANK_GAP;
| |||
//--- Walk the rank left to right from that starting point
| |||
int at = (widest - across) / 2;
| |||
for(int i = 0; i < m_bands; i++)
| |||
{
| |||
//--- Place only the rows sitting at this depth
| |||
if(m_band[i].rank != r)
| |||
continue;
| |||
m_band[i].x = at;
| |||
m_band[i].y = ON_TOP_PAD + r * ON_BAND_GAP;
| |||
at += m_band[i].w + ON_RANK_GAP;
| |||
}
| |||
}
| |||
m_height = ON_TOP_PAD + deepest * ON_BAND_GAP + ON_DOT;
| |||
//--- Publish the measurements the panel scales against
| |||
m_width = widest;
| |||
m_ready = true;
| |||
//--- Report the arrangement as built
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Construct an empty layout |
| |||
//+------------------------------------------------------------------+
| |||
COmvLayout::COmvLayout(void)
| |||
{
| |||
//--- Clear every field so a query before building reports empty
| |||
Clear();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Drop everything the previous build produced |
| |||
//+------------------------------------------------------------------+
| |||
void COmvLayout::Clear(void)
| |||
{
| |||
//--- Zero the counts that bound both arrays
| |||
m_boxes = 0;
| |||
m_edges = 0;
| |||
//--- Release the stored geometry
| |||
ArrayResize(m_box, 0);
| |||
ArrayResize(m_edge, 0);
| |||
//--- Zero the overall size
| |||
m_width = 0;
| |||
m_height = 0;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Classify an operator into a coloring role |
| |||
//+------------------------------------------------------------------+
| |||
int COmvLayout::RoleOf(const string op) const
| |||
{
| |||
//--- Group the operators that carry weight matrices
| |||
if(op == "Gemm" || op == "MatMul" || op == "Conv" || op == "ConvTranspose")
| |||
return OL_ROLE_COMPUTE;
| |||
//--- Group the elementwise activations
| |||
if(op == "Tanh" || op == "Relu" || op == "Sigmoid" || op == "LeakyRelu" || op == "Elu")
| |||
return OL_ROLE_ACTIVATE;
| |||
//--- Group the operators that rearrange rather than compute
| |||
if(op == "Concat" || op == "Reshape" || op == "Transpose" || op == "Split" || op == "Slice")
| |||
return OL_ROLE_SHAPE;
| |||
//--- Group the operators that normally end a graph
| |||
if(op == "Softmax" || op == "LogSoftmax" || op == "Sigmoid")
| |||
return OL_ROLE_OUTPUT;
| |||
//--- Leave everything else plain
| |||
return OL_ROLE_PLAIN;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the node that emits a given name |
| |||
//+------------------------------------------------------------------+
| |||
int COmvLayout::ProducerOf(COmvModel &model, const string name) const
| |||
{
| |||
//--- Scan every node for one that emits this name
| |||
for(int i = 0; i < model.Nodes(); i++)
| |||
{
| |||
//--- Fetch this node
| |||
OmvNode node;
| |||
model.NodeAt(i, node);
| |||
//--- Compare every name it produces
| |||
for(int j = 0; j < node.emitCount; j++)
| |||
if(node.emits[j] == name)
| |||
return i;
| |||
}
| |||
//--- Report that no node produces it
| |||
return -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Report whether a name belongs to a stored weight tensor |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvLayout::IsWeight(COmvModel &model, const string name) const
| |||
{
| |||
//--- Scan every tensor for a matching name
| |||
for(int i = 0; i < model.Tensors(); i++)
| |||
{
| |||
//--- Fetch this tensor
| |||
OmvTensor tensor;
| |||
model.TensorAt(i, tensor);
| |||
//--- Report a match
| |||
if(tensor.name == name)
| |||
return true;
| |||
}
| |||
//--- Report that no tensor carries this name
| |||
return false;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Report whether any name feeding a node comes from a node |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvLayout::HasNodeParent(COmvModel &model, const OmvNode &node) const
| |||
{
| |||
//--- Test every name feeding this node
| |||
for(int j = 0; j < node.feedCount; j++)
| |||
{
| |||
//--- Skip names that are weights rather than node outputs
| |||
if(IsWeight(model, node.feeds[j]))
| |||
continue;
| |||
//--- Report a name that another node produces
| |||
if(ProducerOf(model, node.feeds[j]) >= 0)
| |||
return true;
| |||
}
| |||
//--- Report that nothing upstream feeds this node
| |||
return false;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the shallowest rank among a node's consumers |
| |||
//+------------------------------------------------------------------+
| |||
int COmvLayout::ShallowestConsumer(COmvModel &model, const int index, const int &rank[]) const
| |||
{
| |||
//--- Fetch the node whose consumers are wanted
| |||
OmvNode source;
| |||
model.NodeAt(index, source);
| |||
//--- Start above every possible rank
| |||
int shallowest = -1;
| |||
//--- Scan every node for one that reads a name this node emits
| |||
for(int k = 0; k < model.Nodes(); k++)
| |||
{
| |||
//--- Fetch the candidate consumer
| |||
OmvNode target;
| |||
model.NodeAt(k, target);
| |||
//--- Compare every name feeding the candidate
| |||
for(int j = 0; j < target.feedCount; j++)
| |||
{
| |||
//--- Skip names that are weights rather than node outputs
| |||
if(IsWeight(model, target.feeds[j]))
| |||
continue;
| |||
//--- Skip names this node does not emit
| |||
if(ProducerOf(model, target.feeds[j]) != index)
| |||
continue;
| |||
//--- Track the shallowest consumer found so far
| |||
if(shallowest < 0 || rank[k] < shallowest)
| |||
shallowest = rank[k];
| |||
}
| |||
}
| |||
//--- Report the shallowest rank, or a negative when nothing consumes it
| |||
return shallowest;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Report the shape a named value carries |
| |||
//+------------------------------------------------------------------+
| |||
string COmvLayout::ShapeOf(COmvModel &model, const string name) const
| |||
{
| |||
//--- A graph input announces its own shape
| |||
for(int i = 0; i < model.Inputs(); i++)
| |||
{
| |||
//--- Fetch this graph input
| |||
OmvPort port;
| |||
model.InputAt(i, port);
| |||
//--- Report the shape when the name matches
| |||
if(port.name == name)
| |||
return model.DimsText(port.dim, port.dims);
| |||
}
| |||
//--- A graph output announces its own shape
| |||
for(int i = 0; i < model.Outputs(); i++)
| |||
{
| |||
//--- Fetch this graph output
| |||
OmvPort port;
| |||
model.OutputAt(i, port);
| |||
//--- Report the shape when the name matches
| |||
if(port.name == name)
| |||
return model.DimsText(port.dim, port.dims);
| |||
}
| |||
//--- A value passing between layers announces its shape when the file says so
| |||
for(int i = 0; i < model.Inners(); i++)
| |||
{
| |||
//--- Fetch this inner value
| |||
OmvPort port;
| |||
model.InnerAt(i, port);
| |||
//--- Report the shape when the name matches
| |||
if(port.name == name)
| |||
return model.DimsText(port.dim, port.dims);
| |||
}
| |||
//--- A stored tensor announces its own shape
| |||
for(int i = 0; i < model.Tensors(); i++)
| |||
{
| |||
//--- Fetch this tensor
| |||
OmvTensor tensor;
| |||
model.TensorAt(i, tensor);
| |||
//--- Report the shape when the name matches
| |||
if(tensor.name == name)
| |||
return model.DimsText(tensor.dim, tensor.dims);
| |||
}
| |||
//--- Report nothing for a value the file never shapes
| |||
return "";
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Place a box standing for a value the graph exposes |
| |||
//+------------------------------------------------------------------+
| |||
void COmvLayout::AddPortBox(const string name, const int rank)
| |||
{
| |||
//--- Refuse a box beyond what the arrangement may hold
| |||
if(m_boxes >= OMV_MAX_NODES)
| |||
return;
| |||
//--- Describe the box, which carries a name rather than a layer
| |||
OmvBox box;
| |||
box.node = -1;
| |||
box.portName = name;
| |||
box.rank = rank;
| |||
box.role = OL_ROLE_PORT;
| |||
box.rowCount = 0;
| |||
box.w = OL_BOX_W;
| |||
box.h = OL_BOX_S;
| |||
box.x = (m_width - OL_BOX_W) / 2;
| |||
box.y = 0;
| |||
//--- Append it to the arrangement
| |||
ArrayResize(m_box, m_boxes + 1);
| |||
m_box[m_boxes++] = box;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Record one connection between two placed boxes |
| |||
//+------------------------------------------------------------------+
| |||
void COmvLayout::AddEdge(const int from, const int to, const string label, const string shape)
| |||
{
| |||
//--- Refuse a connection between boxes that were never placed
| |||
if(from < 0 || to < 0 || from >= m_boxes || to >= m_boxes)
| |||
return;
| |||
//--- Describe the connection and the value it carries
| |||
OmvEdge edge;
| |||
edge.from = from;
| |||
edge.to = to;
| |||
edge.label = label;
| |||
edge.shape = shape;
| |||
//--- Append it to the arrangement
| |||
ArrayResize(m_edge, m_edges + 1);
| |||
m_edge[m_edges++] = edge;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| List every stored weight an operator reads |
| |||
//+------------------------------------------------------------------+
| |||
void COmvLayout::RowsOf(COmvModel &model, const OmvNode &node, OmvBox &box) const
| |||
{
| |||
//--- Start with an empty listing
| |||
box.rowCount = 0;
| |||
//--- The operator calls its inputs A, B, C and so on in order
| |||
const string letters = "ABCDEFGH";
| |||
//--- Walk the names feeding this node
| |||
for(int j = 0; j < node.feedCount && box.rowCount < OL_MAX_ROWS; j++)
| |||
{
| |||
//--- Look for a stored tensor carrying this name
| |||
for(int i = 0; i < model.Tensors(); i++)
| |||
{
| |||
//--- Fetch this tensor
| |||
OmvTensor tensor;
| |||
model.TensorAt(i, tensor);
| |||
//--- Skip anything that is not the name being looked up
| |||
if(tensor.name != node.feeds[j])
| |||
continue;
| |||
//--- Name the input by its position on the operator
| |||
box.rowLabel[box.rowCount] = StringSubstr(letters, j, 1);
| |||
//--- Print the shape the way the operator reports it
| |||
box.rowText[box.rowCount] = model.DimsText(tensor.dim, tensor.dims);
| |||
box.rowTensor[box.rowCount] = i;
| |||
box.rowName[box.rowCount] = tensor.name;
| |||
box.rowCount++;
| |||
break;
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Arrange every node into ranks and connect them |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvLayout::Build(COmvModel &model)
| |||
{
| |||
//--- Drop any arrangement held from a previous model
| |||
Clear();
| |||
//--- Abort when the model holds no nodes
| |||
const int total = model.Nodes();
| |||
if(total <= 0)
| |||
return false;
| |||
//--- Start every node at an unknown depth
| |||
int rank[];
| |||
ArrayResize(rank, total);
| |||
ArrayInitialize(rank, -1);
| |||
//--- Settle depths by repeated passes so parents always come first
| |||
for(int pass = 0; pass < total; pass++)
| |||
{
| |||
//--- Track whether this pass moved anything
| |||
bool moved = false;
| |||
//--- Visit every node
| |||
for(int i = 0; i < total; i++)
| |||
{
| |||
//--- Fetch this node
| |||
OmvNode node;
| |||
model.NodeAt(i, node);
| |||
//--- Take the deepest producer feeding it
| |||
int deepest = -1;
| |||
for(int j = 0; j < node.feedCount; j++)
| |||
{
| |||
//--- Skip names that are weights rather than node outputs
| |||
if(IsWeight(model, node.feeds[j]))
| |||
continue;
| |||
//--- Find the node emitting this name
| |||
const int producer = ProducerOf(model, node.feeds[j]);
| |||
//--- Track the deepest one settled so far
| |||
if(producer >= 0 && rank[producer] > deepest)
| |||
deepest = rank[producer];
| |||
}
| |||
//--- Sit one level below the deepest producer
| |||
const int want = deepest + 1;
| |||
//--- Push the node down when this pass found it deeper
| |||
if(want > rank[i])
| |||
{
| |||
rank[i] = want;
| |||
moved = true;
| |||
}
| |||
}
| |||
//--- Stop once a pass changes nothing
| |||
if(!moved)
| |||
break;
| |||
}
| |||
//--- Sink a node with no upstream parent to just above its consumer
| |||
for(int i = 0; i < total; i++)
| |||
{
| |||
//--- Fetch this node
| |||
OmvNode node;
| |||
model.NodeAt(i, node);
| |||
//--- Leave a node that something upstream already feeds
| |||
if(HasNodeParent(model, node))
| |||
continue;
| |||
//--- Find where its shallowest consumer sits
| |||
const int consumer = ShallowestConsumer(model, i, rank);
| |||
//--- Drop the node to one rank above that consumer
| |||
if(consumer > 0 && consumer - 1 > rank[i])
| |||
rank[i] = consumer - 1;
| |||
}
| |||
//--- Count how many nodes sit on each rank
| |||
int depth = 0;
| |||
for(int i = 0; i < total; i++)
| |||
if(rank[i] > depth)
| |||
depth = rank[i];
| |||
int perRank[];
| |||
ArrayResize(perRank, depth + 1);
| |||
ArrayInitialize(perRank, 0);
| |||
for(int i = 0; i < total; i++)
| |||
perRank[rank[i]]++;
| |||
//--- Find the widest rank so every rank can be centered against it
| |||
int widest = 1;
| |||
for(int r = 0; r <= depth; r++)
| |||
if(perRank[r] > widest)
| |||
widest = perRank[r];
| |||
m_width = widest * OL_BOX_W + (widest - 1) * OL_GAP_X;
| |||
//--- Track how many boxes each rank has already placed
| |||
int placed[];
| |||
ArrayResize(placed, depth + 1);
| |||
ArrayInitialize(placed, 0);
| |||
//--- Find which ranks are entered by a connection that carries a label
| |||
bool wideGap[];
| |||
ArrayResize(wideGap, depth + 2);
| |||
ArrayInitialize(wideGap, false);
| |||
for(int i = 0; i < total; i++)
| |||
{
| |||
//--- Fetch this node to read the names it takes in
| |||
OmvNode probe;
| |||
model.NodeAt(i, probe);
| |||
//--- Test every name that is not a stored weight
| |||
for(int j = 0; j < probe.feedCount; j++)
| |||
{
| |||
//--- Skip a name the file never shaped, it draws no label
| |||
if(IsWeight(model, probe.feeds[j]))
| |||
continue;
| |||
if(StringLen(ShapeOf(model, probe.feeds[j])) == 0)
| |||
continue;
| |||
//--- Widen the gap above the rank this node sits on
| |||
wideGap[rank[i]] = true;
| |||
}
| |||
}
| |||
//--- A graph output is always entered by a labelled connection
| |||
if(model.Outputs() > 0)
| |||
wideGap[depth + 1] = true;
| |||
//--- Measure the tallest box on every rank so ranks never overlap
| |||
int rowHigh[];
| |||
ArrayResize(rowHigh, depth + 1);
| |||
ArrayInitialize(rowHigh, OL_BOX_S);
| |||
for(int i = 0; i < total; i++)
| |||
{
| |||
//--- Fetch this node to size the box it will get
| |||
OmvNode probe;
| |||
model.NodeAt(i, probe);
| |||
OmvBox sizing;
| |||
RowsOf(model, probe, sizing);
| |||
//--- Take the taller of what this rank already holds and this box
| |||
const int high = (sizing.rowCount > 0)
| |||
? OL_HEAD_H + OL_ROW_PAD * 2 + sizing.rowCount * OL_ROW_H : OL_BOX_S;
| |||
if(high > rowHigh[rank[i]])
| |||
rowHigh[rank[i]] = high;
| |||
}
| |||
//--- Place one box per node
| |||
ArrayResize(m_box, total);
| |||
for(int i = 0; i < total; i++)
| |||
{
| |||
//--- Fetch this node
| |||
OmvNode node;
| |||
model.NodeAt(i, node);
| |||
//--- Describe the box
| |||
OmvBox box;
| |||
box.node = i;
| |||
box.rank = rank[i];
| |||
box.role = RoleOf(node.op);
| |||
RowsOf(model, node, box);
| |||
box.w = OL_BOX_W;
| |||
//--- Size the box to the header plus however many rows it lists
| |||
box.h = (box.rowCount > 0)
| |||
? OL_HEAD_H + OL_ROW_PAD * 2 + box.rowCount * OL_ROW_H : OL_BOX_S;
| |||
//--- Center this rank inside the widest one
| |||
const int count = perRank[rank[i]];
| |||
const int span = count * OL_BOX_W + (count - 1) * OL_GAP_X;
| |||
const int left = (m_width - span) / 2;
| |||
//--- Step across the rank by however many boxes it already holds
| |||
box.x = left + placed[rank[i]] * (OL_BOX_W + OL_GAP_X);
| |||
box.y = 0;
| |||
//--- Stack this rank below every taller rank above it
| |||
for(int r = 0; r < rank[i]; r++)
| |||
box.y += rowHigh[r] + (wideGap[r + 1] ? OL_GAP_Y : OL_GAP_S);
| |||
placed[rank[i]]++;
| |||
//--- Store the placed box
| |||
m_box[m_boxes++] = box;
| |||
}
| |||
//--- Give every graph input a box on a rank of its own above the graph
| |||
for(int p = 0; p < model.Inputs(); p++)
| |||
{
| |||
//--- Fetch this graph input
| |||
OmvPort port;
| |||
model.InputAt(p, port);
| |||
//--- Skip a name that a stored tensor already covers
| |||
if(IsWeight(model, port.name))
| |||
continue;
| |||
AddPortBox(port.name, -1);
| |||
}
| |||
//--- Give every graph output a box on a rank of its own below the graph
| |||
for(int p = 0; p < model.Outputs(); p++)
| |||
{
| |||
//--- Fetch this graph output
| |||
OmvPort port;
| |||
model.OutputAt(p, port);
| |||
AddPortBox(port.name, depth + 1);
| |||
}
| |||
//--- Shift every layer box down to leave the input rank its room
| |||
const int inputGap = wideGap[0] ? OL_GAP_Y : OL_GAP_S;
| |||
for(int i = 0; i < m_boxes; i++)
| |||
if(m_box[i].rank >= 0)
| |||
m_box[i].y += OL_BOX_S + inputGap;
| |||
//--- Place the output boxes under the deepest rank
| |||
int deepestY = 0;
| |||
for(int i = 0; i < m_boxes; i++)
| |||
if(m_box[i].rank >= 0 && m_box[i].y + m_box[i].h > deepestY)
| |||
deepestY = m_box[i].y + m_box[i].h;
| |||
const int outputGap = wideGap[depth + 1] ? OL_GAP_Y : OL_GAP_S;
| |||
for(int i = 0; i < m_boxes; i++)
| |||
if(m_box[i].rank > depth)
| |||
m_box[i].y = deepestY + outputGap;
| |||
//--- Measure the whole arrangement from the boxes it actually holds
| |||
m_height = 0;
| |||
for(int i = 0; i < m_boxes; i++)
| |||
if(m_box[i].y + m_box[i].h > m_height)
| |||
m_height = m_box[i].y + m_box[i].h;
| |||
//--- Connect every graph input box to the layers that read it
| |||
for(int i = 0; i < m_boxes; i++)
| |||
{
| |||
//--- Take only the boxes standing for a graph input
| |||
if(m_box[i].rank != -1)
| |||
continue;
| |||
//--- Find every layer reading that name
| |||
for(int k = 0; k < total; k++)
| |||
{
| |||
//--- Fetch the candidate layer
| |||
OmvNode target;
| |||
model.NodeAt(k, target);
| |||
//--- Compare every name feeding it
| |||
for(int j = 0; j < target.feedCount; j++)
| |||
{
| |||
//--- Skip a name that is not this port
| |||
if(target.feeds[j] != m_box[i].portName)
| |||
continue;
| |||
//--- Find the box the layer was placed in
| |||
for(int b = 0; b < m_boxes; b++)
| |||
{
| |||
//--- Store the connection once the box is found
| |||
if(m_box[b].node != k)
| |||
continue;
| |||
AddEdge(i, b, m_box[i].portName, ShapeOf(model, m_box[i].portName));
| |||
break;
| |||
}
| |||
}
| |||
}
| |||
}
| |||
//--- Connect the layer that writes each graph output to its box
| |||
for(int i = 0; i < m_boxes; i++)
| |||
{
| |||
//--- Take only the boxes standing for a graph output
| |||
if(m_box[i].rank <= depth)
| |||
continue;
| |||
//--- Find the layer emitting that name
| |||
const int producer = ProducerOf(model, m_box[i].portName);
| |||
if(producer < 0)
| |||
continue;
| |||
//--- Find the box the layer was placed in
| |||
for(int b = 0; b < m_boxes; b++)
| |||
{
| |||
//--- Store the connection once the box is found
| |||
if(m_box[b].node != producer)
| |||
continue;
| |||
AddEdge(b, i, m_box[i].portName, ShapeOf(model, m_box[i].portName));
| |||
break;
| |||
}
| |||
}
| |||
//--- Connect every node to the nodes feeding it
| |||
for(int i = 0; i < total; i++)
| |||
{
| |||
//--- Fetch this node
| |||
OmvNode node;
| |||
model.NodeAt(i, node);
| |||
//--- Draw one edge per producing name
| |||
for(int j = 0; j < node.feedCount; j++)
| |||
{
| |||
//--- Skip names that are weights rather than node outputs
| |||
if(IsWeight(model, node.feeds[j]))
| |||
continue;
| |||
//--- Find the node emitting this name
| |||
const int producer = ProducerOf(model, node.feeds[j]);
| |||
//--- Skip a name no node produces
| |||
if(producer < 0)
| |||
continue;
| |||
//--- Find the boxes the two layers were placed in
| |||
int fromBox = -1, toBox = -1;
| |||
for(int b = 0; b < m_boxes; b++)
| |||
{
| |||
//--- Match each end to its box
| |||
if(m_box[b].node == producer)
| |||
fromBox = b;
| |||
if(m_box[b].node == i)
| |||
toBox = b;
| |||
}
| |||
//--- Skip a pair that was never placed
| |||
if(fromBox < 0 || toBox < 0)
| |||
continue;
| |||
//--- Store the connection and the name it carries
| |||
AddEdge(fromBox, toBox, node.feeds[j], ShapeOf(model, node.feeds[j]));
| |||
}
| |||
}
| |||
//--- Report success
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Chart panel showing a parsed model |
| |||
//+------------------------------------------------------------------+
| |||
class COmvPanel
| |||
{
| |||
private:
| |||
// Draw everything through one canvas
| |||
COmvCanvas m_cv;
| |||
// Hold the parsed model
| |||
COmvModel m_model;
| |||
// Hold the arranged geometry
| |||
COmvLayout m_layout;
| |||
// Hold the neuron arrangement
| |||
COmvNeurons m_cells;
| |||
// Show the neuron view in place of the graph
| |||
bool m_neuronView;
| |||
// Bound the control that switches between the two views
| |||
int m_rView[4];
| |||
// Resolve the active theme colors
| |||
OmvTheme m_th;
| |||
| |||
// Identify the chart the panel lives on
| |||
long m_chartId;
| |||
// Name the bitmap object
| |||
string m_name;
| |||
// Name the running program
| |||
string m_title;
| |||
// Track whether the canvas exists
| |||
bool m_created;
| |||
// Track whether teardown has run
| |||
bool m_destroyed;
| |||
| |||
// Place the panel left edge
| |||
int m_x;
| |||
// Place the panel top edge
| |||
int m_y;
| |||
// Measure the panel width
| |||
int m_w;
| |||
// Measure the panel height
| |||
int m_h;
| |||
// Remember the height before collapsing
| |||
int m_fullH;
| |||
// Select the full or collapsed mode
| |||
int m_mode;
| |||
// Track whether the dark theme is active
| |||
bool m_dark;
| |||
// Track whether the inspector column is open
| |||
bool m_inspect;
| |||
| |||
// Scale the graph inside its canvas
| |||
double m_zoom;
| |||
// Offset the graph horizontally inside its canvas
| |||
int m_panX;
| |||
// Offset the graph vertically inside its canvas
| |||
int m_panY;
| |||
// Index the node the inspector describes
| |||
int m_selected;
| |||
// Index the node under the pointer
| |||
int m_hoverNode;
| |||
// Name what kind of thing the inspector describes
| |||
int m_pickKind;
| |||
// Index the tensor the inspector describes
| |||
int m_pickTensor;
| |||
// Index the connection the inspector describes
| |||
int m_pickLink;
| |||
// Index the tensor row under the pointer
| |||
int m_hoverRow;
| |||
// Index the connection under the pointer
| |||
int m_hoverLink;
| |||
// Bound every drawn tensor row
| |||
int m_rowRect[][4];
| |||
// Name the tensor each drawn row belongs to
| |||
int m_rowTensor[];
| |||
// Count the rows drawn this frame
| |||
int m_rows;
| |||
// Remember the route every drawn connection took
| |||
int m_linkPts[][12];
| |||
// Count the points each drawn route holds
| |||
int m_linkCount[];
| |||
// Name the connection each drawn route belongs to
| |||
int m_linkEdge[];
| |||
// Count the connections drawn this frame
| |||
int m_links;
| |||
// Hold the rows the inspector lays out
| |||
OmvRow m_info[];
| |||
// Count the rows the inspector holds
| |||
int m_infos;
| |||
// Remember which row names the user has opened
| |||
string m_opened[];
| |||
// Count the names held open
| |||
int m_openCount;
| |||
// Offset the inspector when its rows run past the column
| |||
int m_infoScroll;
| |||
// Measure how tall the laid out rows are
| |||
int m_infoHigh;
| |||
// Index the row under the pointer
| |||
int m_infoHover;
| |||
// Name the value the pointer is lighting up in the graph
| |||
string m_litName;
| |||
// Track whether the pointer sits on a row's expander rather than its body
| |||
bool m_onExpander;
| |||
// Bound the inspector scrollbar thumb
| |||
int m_rInfoBar[4];
| |||
| |||
// Identify the control under the pointer
| |||
int m_hover;
| |||
// Identify the control the press began on
| |||
int m_press;
| |||
// Track a header drag
| |||
bool m_dragging;
| |||
// Track a grip drag
| |||
bool m_resizing;
| |||
// Track a canvas drag
| |||
bool m_panning;
| |||
// Track a scrollbar drag
| |||
int m_barDrag;
| |||
// Track whether the vertical bar is showing
| |||
bool m_barV;
| |||
// Track whether the horizontal bar is showing
| |||
bool m_barH;
| |||
// Remember the previous mouse button state
| |||
bool m_lmbPrev;
| |||
// Track whether chart scrolling is suppressed
| |||
bool m_scrollLocked;
| |||
// Remember whether the chart reported pointer moves before the panel asked
| |||
bool m_hadMouse;
| |||
// Remember whether the chart reported wheel turns before the panel asked
| |||
bool m_hadWheel;
| |||
// Offset the pointer inside the header at press
| |||
int m_dragOffX;
| |||
// Offset the pointer inside the header at press
| |||
int m_dragOffY;
| |||
// Offset the grip from the panel corner at press
| |||
int m_gripOffX;
| |||
// Offset the grip from the panel corner at press
| |||
int m_gripOffY;
| |||
// Remember where a canvas press began, to tell a click from a pan
| |||
int m_pressX;
| |||
// Remember where a canvas press began, to tell a click from a pan
| |||
int m_pressY;
| |||
// Remember the pointer position at a drag press
| |||
int m_fromX;
| |||
// Remember the pointer position at a drag press
| |||
int m_fromY;
| |||
// Remember the pan offset at a scrollbar press
| |||
int m_barFrom;
| |||
// Stamp the last repaint for the frame cap
| |||
uint m_lastFrameMs;
| |||
// Hold the chrome that does not change between frames
| |||
uint m_bg[];
| |||
// Track whether the cached chrome still matches the panel
| |||
bool m_bgValid;
| |||
// Keep the drawn view so a hover does not redraw every connection
| |||
uint m_scene[];
| |||
// Remember what the kept scene was drawn for
| |||
string m_sceneSig;
| |||
// Track whether the kept scene still matches the view
| |||
bool m_sceneValid;
| |||
// Track a frame the cap skipped
| |||
bool m_dirty;
| |||
// Describe everything the cached chrome depends on
| |||
string m_bgSig;
| |||
// Remember which tooltip the object currently carries
| |||
string m_tipShown;
| |||
| |||
// Bound the theme button
| |||
int m_rTheme[4];
| |||
// Bound the minimize button
| |||
int m_rMin[4];
| |||
// Bound the close button
| |||
int m_rClose[4];
| |||
// Bound the fit button
| |||
int m_rFit[4];
| |||
// Bound the zoom out button
| |||
int m_rZoomOut[4];
| |||
// Bound the zoom in button
| |||
int m_rZoomIn[4];
| |||
// Bound the inspector close button
| |||
int m_rInspectX[4];
| |||
// Bound the resize grip
| |||
int m_rGrip[4];
| |||
// Bound the vertical scrollbar thumb
| |||
int m_rBarV[4];
| |||
// Bound the horizontal scrollbar thumb
| |||
int m_rBarH[4];
| |||
| |||
void InitDefaults(void);
| |||
void ComputeLayout(void);
| |||
bool CreateCanvas(void);
| |||
bool ApplyMode(void);
| |||
void LockChartScroll(const bool lock);
| |||
int CanvasLeft(void) const { return 1; }
| |||
int CanvasTop(void) const { return OP_HEADER_H + OP_STAT_H + 1; }
| |||
int CanvasRight(void) const { return m_inspect ? m_w - OP_INSPECT_W : m_w - 1; }
| |||
int CanvasBottom(void) const { return m_h - OP_FOOT_H; }
| |||
int ViewW(void) const { return CanvasRight() - CanvasLeft() - (m_barV ? OP_BAR : 0); }
| |||
int ViewH(void) const { return CanvasBottom() - CanvasTop() - (m_barH ? OP_BAR : 0); }
| |||
int ViewWide(void) const { return m_neuronView ? m_cells.Width() : m_layout.Width(); }
| |||
int ViewHigh(void) const { return m_neuronView ? m_cells.Height() : m_layout.Height(); }
| |||
int ScaledW(void) const { return (int)(ViewWide() * m_zoom); }
| |||
int ScaledH(void) const { return (int)(ViewHigh() * m_zoom); }
| |||
bool InRect(const int &r[], const int mx, const int my) const
| |||
{ return (r[2] > 0 && mx >= r[0] && mx < r[0] + r[2] && my >= r[1] && my < r[1] + r[3]); }
| |||
int HitTest(const int mx, const int my);
| |||
int NodeAtPoint(const int mx, const int my);
| |||
int RowAtPoint(const int mx, const int my);
| |||
int LinkAtPoint(const int mx, const int my);
| |||
int TensorNamed(const string name) const;
| |||
string TensorNameAt(const int index) const;
| |||
string Measure(const double value) const;
| |||
string ArgIn(const string op, const int slot) const;
| |||
string ArgOut(const string op) const;
| |||
void AddEndpoint(const string side, const int boxIndex);
| |||
void FitGraph(void);
| |||
void SetZoom(const double zoom);
| |||
void OnWheel(const int mx, const int my, const int delta);
| |||
void ClampPan(void);
| |||
uint FillForRole(const int role) const;
| |||
uint EdgeForRole(const int role) const;
| |||
string TipFor(const int id) const;
| |||
void SetTooltip(const string words);
| |||
| |||
string ChromeSignature(void);
| |||
string SceneSignature(void);
| |||
void DrawChrome(void);
| |||
void DrawShell(void);
| |||
void DrawHeader(void);
| |||
void DrawStats(void);
| |||
void DrawGraph(void);
| |||
void DrawNeurons(void);
| |||
void SizeBars(void);
| |||
void DrawBars(void);
| |||
void AddRow(const string name, const string value,
| |||
const bool isHead, const string linkName = "");
| |||
void AddLine(const string line);
| |||
void AddValueLines(const string name);
| |||
void AddTensorValues(const OmvTensor &tensor);
| |||
void BuildInfo(void);
| |||
void ToggleRow(const string key);
| |||
int InfoAtPoint(const int mx, const int my);
| |||
bool OnExpander(const int mx, const int row) const;
| |||
int LinkNamed(const string name) const;
| |||
void DrawInspector(void);
| |||
void DrawFooterChrome(void);
| |||
void DrawFooter(void);
| |||
void DrawViewSwitch(void);
| |||
void DrawButton(const int &r[], const int id, const string label);
| |||
void DrawIcon(const int &r[], const int id, const int which);
| |||
void PaintCapped(void);
| |||
| |||
public:
| |||
COmvPanel(void) { m_created = false; m_destroyed = false; m_scrollLocked = false; m_hadMouse = false; m_hadWheel = false; }
| |||
bool Init(const long chartId, const string path);
| |||
void Destroy(void);
| |||
void Render(void);
| |||
void OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Reset every member to a safe starting value |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::InitDefaults(void)
| |||
{
| |||
//--- Clear the chart identity
| |||
m_chartId = 0;
| |||
m_name = "OMV_Panel";
| |||
m_title = "";
| |||
m_created = false;
| |||
m_destroyed = false;
| |||
//--- Anchor the panel and size it for full mode
| |||
m_x = 20;
| |||
m_y = 34;
| |||
m_w = OP_W;
| |||
m_h = OP_H;
| |||
m_fullH = OP_H;
| |||
m_mode = OM_FULL;
| |||
//--- Start light with the inspector closed until a node is picked
| |||
m_dark = false;
| |||
m_inspect = false;
| |||
//--- Start the graph unscaled and unshifted
| |||
m_zoom = 1.0;
| |||
m_panX = 0;
| |||
m_panY = 0;
| |||
m_selected = -1;
| |||
m_hoverNode = -1;
| |||
m_neuronView = false;
| |||
m_pickKind = OP_PICK_NONE;
| |||
m_pickTensor = -1;
| |||
m_pickLink = -1;
| |||
m_hoverRow = -1;
| |||
m_hoverLink = -1;
| |||
m_rows = 0;
| |||
m_links = 0;
| |||
m_infos = 0;
| |||
m_openCount = 0;
| |||
m_litName = "";
| |||
m_onExpander = false;
| |||
m_infoScroll = 0;
| |||
m_infoHigh = 0;
| |||
m_infoHover = -1;
| |||
//--- Clear the interaction state
| |||
m_hover = OH_NONE;
| |||
m_press = OH_NONE;
| |||
m_dragging = false;
| |||
m_resizing = false;
| |||
m_panning = false;
| |||
m_barDrag = OH_NONE;
| |||
m_barV = false;
| |||
m_barH = false;
| |||
m_lmbPrev = false;
| |||
m_scrollLocked = false;
| |||
m_dragOffX = 0;
| |||
m_dragOffY = 0;
| |||
m_gripOffX = 0;
| |||
m_gripOffY = 0;
| |||
m_pressX = 0;
| |||
m_pressY = 0;
| |||
m_fromX = 0;
| |||
m_fromY = 0;
| |||
m_barFrom = 0;
| |||
m_lastFrameMs = 0;
| |||
m_bgValid = false;
| |||
m_dirty = false;
| |||
m_bgSig = "";
| |||
m_sceneValid = false;
| |||
m_sceneSig = "";
| |||
m_tipShown = "";
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Place every control against the current panel size |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::ComputeLayout(void)
| |||
{
| |||
//--- Sit the three header icons against the right edge
| |||
const int iconY = (OP_HEADER_H - OP_ICON) / 2;
| |||
m_rClose[0] = m_w - 40;
| |||
m_rClose[1] = iconY;
| |||
m_rClose[2] = OP_ICON;
| |||
m_rClose[3] = OP_ICON;
| |||
m_rMin[0] = m_w - 72;
| |||
m_rMin[1] = iconY;
| |||
m_rMin[2] = OP_ICON;
| |||
m_rMin[3] = OP_ICON;
| |||
m_rTheme[0] = m_w - 104;
| |||
m_rTheme[1] = iconY;
| |||
m_rTheme[2] = OP_ICON;
| |||
m_rTheme[3] = OP_ICON;
| |||
//--- Sit the view controls along the footer
| |||
const int footY = m_h - OP_FOOT_H + (OP_FOOT_H - 22) / 2;
| |||
m_rFit[0] = 14;
| |||
m_rFit[1] = footY;
| |||
m_rFit[2] = 36;
| |||
m_rFit[3] = 22;
| |||
m_rZoomOut[0] = 58;
| |||
m_rZoomOut[1] = footY;
| |||
m_rZoomOut[2] = 22;
| |||
m_rZoomOut[3] = 22;
| |||
m_rZoomIn[0] = 84;
| |||
m_rZoomIn[1] = footY;
| |||
m_rZoomIn[2] = 22;
| |||
m_rZoomIn[3] = 22;
| |||
//--- Sit the view switch to the right of the zoom readout
| |||
m_rView[0] = 152;
| |||
m_rView[1] = footY;
| |||
m_rView[2] = 62;
| |||
m_rView[3] = 22;
| |||
//--- Sit the inspector close button in its top right corner
| |||
m_rInspectX[0] = m_inspect ? m_w - 30 : 0;
| |||
m_rInspectX[1] = CanvasTop() + 8;
| |||
m_rInspectX[2] = m_inspect ? OP_ICON : 0;
| |||
m_rInspectX[3] = OP_ICON;
| |||
//--- Sit the grip in the bottom right corner
| |||
m_rGrip[0] = m_w - OP_GRIP - 2;
| |||
m_rGrip[1] = m_h - OP_GRIP - 2;
| |||
m_rGrip[2] = OP_GRIP;
| |||
m_rGrip[3] = OP_GRIP;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Create the bitmap the panel draws into |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvPanel::CreateCanvas(void)
| |||
{
| |||
//--- Build the bitmap once at its largest, so its size never changes again
| |||
if(!m_cv.CreateBitmapLabel(m_chartId, 0, m_name, m_x, m_y,
| |||
OP_W_MAX, OP_H_MAX, COLOR_FORMAT_ARGB_NORMALIZE))
| |||
return false;
| |||
//--- Pin it to the top left corner of the chart
| |||
ObjectSetInteger(m_chartId, m_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(m_chartId, m_name, OBJPROP_BACK, false);
| |||
ObjectSetInteger(m_chartId, m_name, OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(m_chartId, m_name, OBJPROP_HIDDEN, true);
| |||
ObjectSetInteger(m_chartId, m_name, OBJPROP_ZORDER, 0);
| |||
ObjectSetInteger(m_chartId, m_name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
| |||
ObjectSetString(m_chartId, m_name, OBJPROP_TOOLTIP, "\n");
| |||
//--- Size the chrome cache to the bitmap, which never changes again
| |||
ArrayResize(m_bg, m_cv.PixelCount());
| |||
ArrayResize(m_scene, m_cv.PixelCount());
| |||
m_bgValid = false;
| |||
m_sceneValid = false;
| |||
//--- Report that the canvas exists
| |||
m_created = true;
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Resize the bitmap to match the current mode |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvPanel::ApplyMode(void)
| |||
{
| |||
//--- Take the height the mode calls for, the bitmap itself never changes
| |||
m_h = (m_mode == OM_MIN) ? OP_HEADER_H : m_fullH;
| |||
ComputeLayout();
| |||
m_bgValid = false;
| |||
//--- Report success
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Suppress chart scrolling while the pointer works the panel |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::LockChartScroll(const bool lock)
| |||
{
| |||
//--- Skip the call when the state already matches
| |||
if(lock == m_scrollLocked)
| |||
return;
| |||
//--- Toggle scrolling and remember the new state
| |||
ChartSetInteger(m_chartId, CHART_MOUSE_SCROLL, !lock);
| |||
m_scrollLocked = lock;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Pick the fill color for a node role |
| |||
//+------------------------------------------------------------------+
| |||
uint COmvPanel::FillForRole(const int role) const
| |||
{
| |||
//--- Match the role to its fill
| |||
switch(role)
| |||
{
| |||
case OL_ROLE_COMPUTE:
| |||
return m_th.computeFill;
| |||
case OL_ROLE_ACTIVATE:
| |||
return m_th.activateFill;
| |||
case OL_ROLE_SHAPE:
| |||
return m_th.shapeFill;
| |||
case OL_ROLE_OUTPUT:
| |||
return m_th.outputFill;
| |||
case OL_ROLE_PORT:
| |||
return m_th.chrome;
| |||
}
| |||
//--- Fall back to the plain fill
| |||
return m_th.plainFill;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Pick the edge color for a node role |
| |||
//+------------------------------------------------------------------+
| |||
uint COmvPanel::EdgeForRole(const int role) const
| |||
{
| |||
//--- Match the role to its edge
| |||
switch(role)
| |||
{
| |||
case OL_ROLE_COMPUTE:
| |||
return m_th.computeEdge;
| |||
case OL_ROLE_ACTIVATE:
| |||
return m_th.activateEdge;
| |||
case OL_ROLE_SHAPE:
| |||
return m_th.shapeEdge;
| |||
case OL_ROLE_OUTPUT:
| |||
return m_th.outputEdge;
| |||
case OL_ROLE_PORT:
| |||
return m_th.text;
| |||
}
| |||
//--- Fall back to the plain edge
| |||
return m_th.plainEdge;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Name what a control does for its tooltip |
| |||
//+------------------------------------------------------------------+
| |||
string COmvPanel::TipFor(const int id) const
| |||
{
| |||
//--- Match the control to the words describing it
| |||
switch(id)
| |||
{
| |||
case OH_HEADER:
| |||
return "Drag to move";
| |||
case OH_THEME:
| |||
return m_dark ? "Switch to light" : "Switch to dark";
| |||
case OH_MIN:
| |||
return "Collapse";
| |||
case OH_RESTORE:
| |||
return "Expand";
| |||
case OH_CLOSE:
| |||
return "Remove panel";
| |||
case OH_FIT:
| |||
return m_neuronView ? "Fit neurons" : "Fit graph";
| |||
case OH_VIEW:
| |||
//--- Say why the control cannot be used rather than leaving it silent
| |||
if(!m_neuronView && !m_cells.Ready())
| |||
return "Neuron view unavailable: " + m_cells.Reason();
| |||
return m_neuronView ? "Back to the graph" : "Show the neurons";
| |||
case OH_ZOOM_OUT:
| |||
return "Zoom out";
| |||
case OH_ZOOM_IN:
| |||
return "Zoom in";
| |||
case OH_INSPECT_X:
| |||
return "Close inspector";
| |||
case OH_GRIP:
| |||
return "Drag to resize";
| |||
case OH_BAR_V:
| |||
return "Scroll vertically";
| |||
case OH_BAR_H:
| |||
return "Scroll horizontally";
| |||
case OH_CANVAS:
| |||
return "Drag to pan, click for model properties";
| |||
case OH_ROW:
| |||
return "Tensor properties";
| |||
case OH_LINK:
| |||
return "Connection properties";
| |||
case OH_INFO:
| |||
//--- Name the half of the row the pointer is actually on
| |||
if(m_infoHover < 0 || m_infoHover >= m_infos)
| |||
return "";
| |||
if(m_onExpander)
| |||
return m_info[m_infoHover].isOpen ? "Click to collapse" : "Click to expand";
| |||
if(StringLen(m_info[m_infoHover].linkName) > 0)
| |||
return "Show in graph";
| |||
return m_info[m_infoHover].canOpen ? "Click to expand" : "";
| |||
case OH_INFO_BAR:
| |||
return "Scroll properties";
| |||
case OH_NODE:
| |||
return "Node properties";
| |||
}
| |||
//--- Report nothing for anything without a tooltip
| |||
return "";
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Publish the tooltip the terminal shows over the panel |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::SetTooltip(const string words)
| |||
{
| |||
//--- A single newline is how the terminal is told to show nothing
| |||
const string shown = (StringLen(words) > 0) ? words : "\n";
| |||
//--- Skip the call when the object already carries this text
| |||
if(shown == m_tipShown)
| |||
return;
| |||
//--- Hand the text to the object the terminal draws the tooltip for
| |||
ObjectSetString(m_chartId, m_name, OBJPROP_TOOLTIP, shown);
| |||
m_tipShown = shown;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Hold the pan offsets inside what the graph allows |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::ClampPan(void)
| |||
{
| |||
//--- Settle which bars are showing before the view is measured
| |||
SizeBars();
| |||
//--- Measure the view and the scaled graph
| |||
const int viewW = ViewW(), viewH = ViewH();
| |||
const int fullW = ScaledW(), fullH = ScaledH();
| |||
//--- Center the graph when it fits, otherwise hold it against its margin
| |||
if(fullW <= viewW - OP_PAD * 2)
| |||
m_panX = (viewW - fullW) / 2;
| |||
else
| |||
{
| |||
//--- Never pull the graph past the margin on either side
| |||
if(m_panX > OP_PAD)
| |||
m_panX = OP_PAD;
| |||
if(m_panX < viewW - fullW - OP_PAD)
| |||
m_panX = viewW - fullW - OP_PAD;
| |||
}
| |||
//--- Apply the same rule vertically
| |||
if(fullH <= viewH - OP_PAD * 2)
| |||
m_panY = (viewH - fullH) / 2;
| |||
else
| |||
{
| |||
//--- Never pull the graph past the margin above or below
| |||
if(m_panY > OP_PAD)
| |||
m_panY = OP_PAD;
| |||
if(m_panY < viewH - fullH - OP_PAD)
| |||
m_panY = viewH - fullH - OP_PAD;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Scale and center the graph inside the canvas |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::FitGraph(void)
| |||
{
| |||
//--- Measure the room the canvas offers once the margin is taken out
| |||
const int roomW = ViewW() - OP_PAD * 2;
| |||
const int roomH = ViewH() - OP_PAD * 2;
| |||
//--- Abort when the canvas or the graph has no size
| |||
if(roomW <= 0 || roomH <= 0 || ViewWide() <= 0 || ViewHigh() <= 0)
| |||
return;
| |||
//--- Take the tighter of the two ratios so nothing overflows
| |||
const double byWidth = (double)roomW / ViewWide();
| |||
const double byHeight = (double)roomH / ViewHigh();
| |||
double zoom = MathMin(byWidth, byHeight);
| |||
//--- Let the neurons fill the canvas, but never enlarge the lettered boxes
| |||
const double ceiling = m_neuronView ? OP_ZOOM_MAX : 1.0;
| |||
if(zoom > ceiling)
| |||
zoom = ceiling;
| |||
if(zoom < OP_ZOOM_MIN)
| |||
zoom = OP_ZOOM_MIN;
| |||
m_zoom = zoom;
| |||
//--- Center what the zoom produced
| |||
ClampPan();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Apply a zoom level within the allowed range |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::SetZoom(const double zoom)
| |||
{
| |||
//--- Remember where the view center sits in graph space
| |||
const double cx = (ViewW() * 0.5 - m_panX) / m_zoom;
| |||
const double cy = (ViewH() * 0.5 - m_panY) / m_zoom;
| |||
//--- Clamp the request to the readable range
| |||
double next = zoom;
| |||
if(next < OP_ZOOM_MIN)
| |||
next = OP_ZOOM_MIN;
| |||
if(next > OP_ZOOM_MAX)
| |||
next = OP_ZOOM_MAX;
| |||
m_zoom = next;
| |||
//--- Put the same graph point back under the view center
| |||
m_panX = (int)(ViewW() * 0.5 - cx * m_zoom);
| |||
m_panY = (int)(ViewH() * 0.5 - cy * m_zoom);
| |||
ClampPan();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Handle a wheel turn over the graph |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::OnWheel(const int mx, const int my, const int delta)
| |||
{
| |||
//--- Scroll the inspector when the pointer sits over its column
| |||
if(m_inspect && mx > CanvasRight() && my >= CanvasTop() && my < CanvasBottom())
| |||
{
| |||
//--- Step the listing by one notch
| |||
m_infoScroll += (delta > 0) ? -OP_WHEEL : OP_WHEEL;
| |||
//--- Hold the new offset inside the listing before it is drawn
| |||
if(m_infoScroll < 0)
| |||
m_infoScroll = 0;
| |||
PaintCapped();
| |||
return;
| |||
}
| |||
//--- Ignore a turn that lands outside the graph area
| |||
if(mx < CanvasLeft() || mx >= CanvasRight() || my < CanvasTop() || my >= CanvasBottom())
| |||
return;
| |||
//--- Step one notch per event, taking only the sign so fine wheels still work
| |||
const int step = (delta > 0) ? OP_WHEEL : -OP_WHEEL;
| |||
//--- Scroll the axis that actually has somewhere to go
| |||
if(m_barV)
| |||
m_panY += step;
| |||
else
| |||
m_panX += step;
| |||
ClampPan();
| |||
PaintCapped();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Write the endpoint line a connection reports |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::AddEndpoint(const string side, const int boxIndex)
| |||
{
| |||
//--- Refuse an index no box answers to
| |||
if(boxIndex < 0 || boxIndex >= m_layout.Boxes())
| |||
return;
| |||
//--- Fetch the box the connection reaches
| |||
OmvBox box;
| |||
m_layout.BoxAt(boxIndex, box);
| |||
//--- A graph port is a value, so it reports its own name and nothing more
| |||
if(box.node < 0)
| |||
{
| |||
AddRow(side, "name: " + box.portName, false, box.portName);
| |||
return;
| |||
}
| |||
//--- A layer reports the operator, then the name it was given
| |||
OmvNode node;
| |||
m_model.NodeAt(box.node, node);
| |||
AddRow(side, "node: " + node.op, false);
| |||
AddLine("name: " + node.name);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Print a number without letting a small one read as zero |
| |||
//+------------------------------------------------------------------+
| |||
string COmvPanel::Measure(const double value) const
| |||
{
| |||
//--- Print an exact zero plainly
| |||
if(value == 0.0)
| |||
return "0";
| |||
//--- Fall back to scientific notation where fixed digits would round to zero
| |||
if(MathAbs(value) < 0.0000001)
| |||
return DoubleToString(value, -8);
| |||
//--- Print anything larger at a width the column can hold
| |||
return DoubleToString(value, 8);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Name the input an operator gives a position |
| |||
//+------------------------------------------------------------------+
| |||
string COmvPanel::ArgIn(const string op, const int slot) const
| |||
{
| |||
//--- The general matrix operator names its three operands
| |||
if(op == "Gemm" || op == "MatMul")
| |||
return StringSubstr("ABC", slot, 1);
| |||
//--- These operators name their single operand X
| |||
if(op == "Relu" || op == "Sigmoid" || op == "LeakyRelu" || op == "Elu" || op == "Sqrt")
| |||
return "X";
| |||
//--- Elementwise pairs name their two operands A and B
| |||
if(op == "Mul" || op == "Add" || op == "Sub" || op == "Div" || op == "Pow")
| |||
return StringSubstr("AB", slot, 1);
| |||
//--- Concat takes a list rather than numbered operands
| |||
if(op == "Concat")
| |||
return "inputs";
| |||
//--- Everything else names its operand for what it is
| |||
return "input";
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Name the output an operator gives a position |
| |||
//+------------------------------------------------------------------+
| |||
string COmvPanel::ArgOut(const string op) const
| |||
{
| |||
//--- The general matrix operator names its result Y
| |||
if(op == "Gemm" || op == "MatMul" || op == "Relu" || op == "Sigmoid" ||
| |||
op == "LeakyRelu" || op == "Elu" || op == "Sqrt")
| |||
return "Y";
| |||
//--- Elementwise pairs name their result C
| |||
if(op == "Mul" || op == "Add" || op == "Sub" || op == "Div" || op == "Pow")
| |||
return "C";
| |||
//--- Concat names its result for what it built
| |||
if(op == "Concat")
| |||
return "concat_result";
| |||
//--- Everything else names its result for what it is
| |||
return "output";
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Name the tensor an index points at |
| |||
//+------------------------------------------------------------------+
| |||
string COmvPanel::TensorNameAt(const int index) const
| |||
{
| |||
//--- Refuse an index no tensor answers to
| |||
if(index < 0 || index >= m_model.Tensors())
| |||
return "";
| |||
//--- Fetch the tensor and report its name
| |||
OmvTensor tensor;
| |||
m_model.TensorAt(index, tensor);
| |||
return tensor.name;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Report whether a point sits on a row's expander |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvPanel::OnExpander(const int mx, const int row) const
| |||
{
| |||
//--- Refuse a row that carries no expander
| |||
if(row < 0 || row >= m_infos || !m_info[row].canOpen)
| |||
return false;
| |||
//--- The expander occupies the right end of the value box
| |||
const int right = m_w - 16 - ((m_infoHigh > (CanvasBottom() - 8) - (CanvasTop() + 44))
| |||
? OP_BAR + 2 : 0);
| |||
return (mx >= right - 22);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the first connection carrying a named value |
| |||
//+------------------------------------------------------------------+
| |||
int COmvPanel::LinkNamed(const string name) const
| |||
{
| |||
//--- Scan every connection for the one carrying this value
| |||
for(int i = 0; i < m_layout.Edges(); i++)
| |||
{
| |||
//--- Fetch this connection
| |||
OmvEdge edge;
| |||
m_layout.EdgeAt(i, edge);
| |||
//--- Report it once the name matches
| |||
if(edge.label == name)
| |||
return i;
| |||
}
| |||
//--- Report that nothing carries it
| |||
return -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the tensor a name belongs to |
| |||
//+------------------------------------------------------------------+
| |||
int COmvPanel::TensorNamed(const string name) const
| |||
{
| |||
//--- Scan every stored tensor for a matching name
| |||
for(int i = 0; i < m_model.Tensors(); i++)
| |||
{
| |||
//--- Fetch this tensor
| |||
OmvTensor tensor;
| |||
m_model.TensorAt(i, tensor);
| |||
//--- Report the one that carries the name
| |||
if(tensor.name == name)
| |||
return i;
| |||
}
| |||
//--- Report that no tensor carries it
| |||
return -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the tensor row under a panel point |
| |||
//+------------------------------------------------------------------+
| |||
//| Find the tensor row under a panel point |
| |||
//+------------------------------------------------------------------+
| |||
int COmvPanel::RowAtPoint(const int mx, const int my)
| |||
{
| |||
//--- Test every row drawn on the last frame
| |||
for(int i = 0; i < m_rows; i++)
| |||
if(mx >= m_rowRect[i][0] && mx < m_rowRect[i][0] + m_rowRect[i][2] &&
| |||
my >= m_rowRect[i][1] && my < m_rowRect[i][1] + m_rowRect[i][3])
| |||
return i;
| |||
//--- Report that no row holds the point
| |||
return -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the connection under a panel point |
| |||
//+------------------------------------------------------------------+
| |||
int COmvPanel::LinkAtPoint(const int mx, const int my)
| |||
{
| |||
//--- Take the closest route, so crossing connections cannot be confused
| |||
int best = -1;
| |||
double nearest = 6.0;
| |||
//--- Measure the pointer against every route drawn on the last frame
| |||
for(int i = 0; i < m_links; i++)
| |||
{
| |||
//--- Walk the segments the route was built from
| |||
for(int s = 0; s + 1 < m_linkCount[i]; s++)
| |||
{
| |||
//--- Take this segment
| |||
const double ax = m_linkPts[i][s * 2], ay = m_linkPts[i][s * 2 + 1];
| |||
const double bx = m_linkPts[i][s * 2 + 2], by = m_linkPts[i][s * 2 + 3];
| |||
//--- Project the pointer onto it
| |||
const double dx = bx - ax, dy = by - ay;
| |||
const double len2 = dx * dx + dy * dy;
| |||
double along = (len2 > 1e-9) ? ((mx - ax) * dx + (my - ay) * dy) / len2 : 0.0;
| |||
//--- Clamp the projection to the segment ends
| |||
if(along < 0.0)
| |||
along = 0.0;
| |||
if(along > 1.0)
| |||
along = 1.0;
| |||
//--- Measure how far the pointer sits from the segment
| |||
const double ox = mx - (ax + dx * along), oy = my - (ay + dy * along);
| |||
const double dist = MathSqrt(ox * ox + oy * oy);
| |||
//--- Keep the nearest route within reach
| |||
if(dist < nearest)
| |||
{
| |||
nearest = dist;
| |||
best = i;
| |||
}
| |||
}
| |||
}
| |||
//--- Report the connection the closest route belongs to
| |||
return (best >= 0) ? m_linkEdge[best] : -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the node box under a panel point |
| |||
//+------------------------------------------------------------------+
| |||
int COmvPanel::NodeAtPoint(const int mx, const int my)
| |||
{
| |||
//--- Convert the point out of canvas space into graph space
| |||
const double gx = (mx - CanvasLeft() - m_panX) / m_zoom;
| |||
const double gy = (my - CanvasTop() - m_panY) / m_zoom;
| |||
//--- Test every placed box
| |||
for(int i = 0; i < m_layout.Boxes(); i++)
| |||
{
| |||
//--- Fetch this box
| |||
OmvBox box;
| |||
m_layout.BoxAt(i, box);
| |||
//--- Report the box that contains the point
| |||
if(gx >= box.x && gx < box.x + box.w && gy >= box.y && gy < box.y + box.h)
| |||
return i;
| |||
}
| |||
//--- Report that no box holds the point
| |||
return -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Identify the control under a panel point |
| |||
//+------------------------------------------------------------------+
| |||
int COmvPanel::HitTest(const int mx, const int my)
| |||
{
| |||
//--- Test the header icons before the band behind them
| |||
if(InRect(m_rTheme, mx, my))
| |||
return OH_THEME;
| |||
if(InRect(m_rMin, mx, my))
| |||
return (m_mode == OM_MIN) ? OH_RESTORE : OH_MIN;
| |||
if(InRect(m_rClose, mx, my))
| |||
return OH_CLOSE;
| |||
//--- Report the header band itself
| |||
if(my < OP_HEADER_H)
| |||
return OH_HEADER;
| |||
//--- Report nothing else while collapsed
| |||
if(m_mode == OM_MIN)
| |||
return OH_NONE;
| |||
//--- Test the grip before the footer behind it
| |||
if(InRect(m_rGrip, mx, my))
| |||
return OH_GRIP;
| |||
//--- Test the footer controls
| |||
if(InRect(m_rFit, mx, my))
| |||
return OH_FIT;
| |||
if(InRect(m_rView, mx, my))
| |||
return OH_VIEW;
| |||
if(InRect(m_rZoomOut, mx, my))
| |||
return OH_ZOOM_OUT;
| |||
if(InRect(m_rZoomIn, mx, my))
| |||
return OH_ZOOM_IN;
| |||
//--- Test the inspector close button
| |||
if(InRect(m_rInspectX, mx, my))
| |||
return OH_INSPECT_X;
| |||
//--- Test the inspector scrollbar before the rows behind it
| |||
if(InRect(m_rInfoBar, mx, my))
| |||
return OH_INFO_BAR;
| |||
//--- Test the inspector rows, ignoring any that lead nowhere
| |||
const int infoRow = InfoAtPoint(mx, my);
| |||
if(infoRow >= 0 && (m_info[infoRow].canOpen || StringLen(m_info[infoRow].linkName) > 0))
| |||
return OH_INFO;
| |||
//--- Test both scrollbar thumbs
| |||
if(InRect(m_rBarV, mx, my))
| |||
return OH_BAR_V;
| |||
if(InRect(m_rBarH, mx, my))
| |||
return OH_BAR_H;
| |||
//--- Test the graph area last, smallest target first
| |||
if(mx >= CanvasLeft() && mx < CanvasRight() && my >= CanvasTop() && my < CanvasBottom())
| |||
{
| |||
//--- The neuron view holds no boxes, rows or connections to pick
| |||
if(m_neuronView)
| |||
return OH_CANVAS;
| |||
//--- A tensor row sits inside a node, so it wins over the node
| |||
if(RowAtPoint(mx, my) >= 0)
| |||
return OH_ROW;
| |||
//--- A node wins over the empty canvas
| |||
if(NodeAtPoint(mx, my) >= 0)
| |||
return OH_NODE;
| |||
//--- A connection is the last thing that can be grabbed
| |||
if(LinkAtPoint(mx, my) >= 0)
| |||
return OH_LINK;
| |||
return OH_CANVAS;
| |||
}
| |||
//--- Report nothing clickable
| |||
return OH_NONE;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw a labeled button |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawButton(const int &r[], const int id, const string label)
| |||
{
| |||
//--- Light the button when the pointer sits over it
| |||
const bool lit = (m_hover == id || m_press == id);
| |||
//--- Fill and outline the button face
| |||
m_cv.RoundFill(r[0], r[1], r[2], r[3], OP_BTN_R, lit ? m_th.hoverFace : m_th.btnFace);
| |||
m_cv.RoundStroke(r[0], r[1], r[2], r[3], OP_BTN_R, 1, lit ? m_th.hoverEdge : m_th.btnEdge);
| |||
//--- Measure the label, then center it inside the button
| |||
m_cv.Font(8);
| |||
const int labelH = m_cv.TextHigh(label);
| |||
m_cv.TxtC(r[0] + r[2] / 2, r[1] + (r[3] - labelH) / 2, label, m_th.text);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw an icon button |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawIcon(const int &r[], const int id, const int which)
| |||
{
| |||
//--- Skip a control the current mode hides
| |||
if(r[2] <= 0)
| |||
return;
| |||
//--- Light the button when the pointer sits over it
| |||
const bool lit = (m_hover == id || m_press == id);
| |||
//--- Fill the face only while lit so the chrome stays clean
| |||
if(lit)
| |||
{
| |||
m_cv.RoundFill(r[0], r[1], r[2], r[3], OP_BTN_R, m_th.hoverFace);
| |||
m_cv.RoundStroke(r[0], r[1], r[2], r[3], OP_BTN_R, 1, m_th.hoverEdge);
| |||
}
| |||
//--- Find the middle of the button and the box the glyph fits
| |||
const int cx = r[0] + r[2] / 2;
| |||
const int cy = r[1] + r[3] / 2;
| |||
const double sz = (double)MathMin(r[2], r[3]) - 4.0;
| |||
const uint ink = lit ? m_th.text : m_th.muted;
| |||
//--- Draw the sun that offers the light theme
| |||
if(which == 0)
| |||
{
| |||
//--- Fill the core then lay eight spokes around it
| |||
m_cv.DiscAA(cx, cy, (int)MathRound(sz * 0.24), ink);
| |||
for(int i = 0; i < 8; i++)
| |||
{
| |||
//--- Step around the circle one eighth at a time
| |||
const double ang = i * (M_PI / 4.0);
| |||
const double cs = MathCos(ang), sn = MathSin(ang);
| |||
m_cv.StrokeAA(cx + cs * sz * 0.36, cy + sn * sz * 0.36,
| |||
cx + cs * sz * 0.52, cy + sn * sz * 0.52, 2, ink);
| |||
}
| |||
return;
| |||
}
| |||
//--- Draw the moon that offers the dark theme
| |||
if(which == 7)
| |||
{
| |||
//--- Draw the crescent as one shape so no background bleeds into its rim
| |||
const double moonR = sz * 0.42;
| |||
m_cv.CrescentAA(cx, cy, moonR, moonR * 0.58, -moonR * 0.44, moonR * 0.90, ink);
| |||
return;
| |||
}
| |||
//--- Draw the bar that collapses the panel or steps the zoom down
| |||
if(which == 1 || which == 3)
| |||
{
| |||
m_cv.StrokeAA(cx - sz * OP_BAR_R, cy, cx + sz * OP_BAR_R, cy, 2, ink);
| |||
return;
| |||
}
| |||
//--- Draw the cross that removes the panel or closes the inspector
| |||
if(which == 2 || which == 5)
| |||
{
| |||
m_cv.StrokeAA(cx - sz * OP_GLYPH, cy - sz * OP_GLYPH,
| |||
cx + sz * OP_GLYPH, cy + sz * OP_GLYPH, 2, ink);
| |||
m_cv.StrokeAA(cx + sz * OP_GLYPH, cy - sz * OP_GLYPH,
| |||
cx - sz * OP_GLYPH, cy + sz * OP_GLYPH, 2, ink);
| |||
return;
| |||
}
| |||
//--- Draw the plus that steps the zoom up
| |||
if(which == 4)
| |||
{
| |||
m_cv.StrokeAA(cx - sz * OP_BAR_R, cy, cx + sz * OP_BAR_R, cy, 2, ink);
| |||
m_cv.StrokeAA(cx, cy - sz * OP_BAR_R, cx, cy + sz * OP_BAR_R, 2, ink);
| |||
return;
| |||
}
| |||
//--- Draw the box that restores the panel
| |||
m_cv.RoundStroke(cx - (int)(sz * OP_GLYPH), cy - (int)(sz * 0.22),
| |||
(int)(sz * OP_GLYPH * 2), (int)(sz * 0.44), 2, 1, ink);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Name what the kept scene was drawn for |
| |||
//+------------------------------------------------------------------+
| |||
string COmvPanel::SceneSignature(void)
| |||
{
| |||
//--- Any change here means the drawn view no longer matches the panel
| |||
return StringFormat("%s|%d|%d|%d|%d|%d|%d|%d|%d|%d|%s", ChromeSignature(),
| |||
m_panX, m_panY, (int)(m_zoom * 1000),
| |||
m_selected, m_pickKind, m_hoverNode, m_hoverRow,
| |||
m_pickTensor, m_pickLink, m_litName);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Describe everything the cached chrome depends on |
| |||
//+------------------------------------------------------------------+
| |||
string COmvPanel::ChromeSignature(void)
| |||
{
| |||
//--- Any change here means the cached chrome no longer matches the panel
| |||
return StringFormat("%d|%d|%d|%d|%d|%d|%d|%d|%d", m_w, m_h, m_mode,
| |||
(int)m_dark, (int)m_inspect, (int)m_neuronView,
| |||
m_model.Nodes(), m_model.Tensors(), m_model.Parameters());
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw every band that does not change between frames |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawChrome(void)
| |||
{
| |||
//--- Lay the body down first
| |||
DrawShell();
| |||
//--- Draw the bands that never move
| |||
if(m_mode == OM_FULL)
| |||
{
| |||
DrawStats();
| |||
DrawFooterChrome();
| |||
}
| |||
//--- Fill the header band under the icons
| |||
m_cv.RoundFillSel(0, 0, m_w, OP_HEADER_H, OP_RADIUS, m_th.chrome,
| |||
true, true, m_mode == OM_MIN, m_mode == OM_MIN);
| |||
//--- Rule the band off only when something sits below it
| |||
if(m_mode != OM_MIN)
| |||
m_cv.HRule(1, m_w - 2, OP_HEADER_H - 1, m_th.divider);
| |||
//--- Outline the whole panel so no band overwrites the edge
| |||
m_cv.RoundStroke(0, 0, m_w, m_h, OP_RADIUS, 1, m_th.border);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the panel body and its outline |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawShell(void)
| |||
{
| |||
//--- Clear the whole bitmap, so whatever the panel no longer covers is clear
| |||
m_cv.NoClip();
| |||
m_cv.Erase(0);
| |||
//--- Fill the body with every corner smoothed
| |||
m_cv.RoundFill(0, 0, m_w, m_h, OP_RADIUS, m_th.panel);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the header title and icons |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawHeader(void)
| |||
{
| |||
//--- Draw the eye mark that identifies the viewer
| |||
m_cv.DiscAA(30, 22, 8, m_th.accent);
| |||
m_cv.DiscAA(30, 22, 6, m_th.chrome);
| |||
m_cv.DiscAA(30, 22, 3, m_th.accent);
| |||
//--- Name the program
| |||
m_cv.Font(11, true);
| |||
const int titleRoom = m_rTheme[0] - 48 - 12;
| |||
string head[];
| |||
if(titleRoom > 0 && m_cv.WrapText(m_title, titleRoom, head, 1) > 0)
| |||
m_cv.TxtL(48, 10, head[0], m_th.text);
| |||
//--- Describe the loaded file under the name
| |||
m_cv.Font(8);
| |||
string subtitle = m_model.GraphName();
| |||
if(StringLen(subtitle) == 0)
| |||
subtitle = "no graph";
| |||
string tail[];
| |||
const string line = subtitle + " opset " + IntegerToString(m_model.Opset());
| |||
if(titleRoom > 0 && m_cv.WrapText(line, titleRoom, tail, 1) > 0)
| |||
m_cv.TxtL(48, 27, tail[0], m_th.muted);
| |||
//--- Draw the three header icons
| |||
DrawIcon(m_rTheme, OH_THEME, m_dark ? 0 : 7);
| |||
DrawIcon(m_rMin, (m_mode == OM_MIN) ? OH_RESTORE : OH_MIN, (m_mode == OM_MIN) ? 6 : 1);
| |||
DrawIcon(m_rClose, OH_CLOSE, 2);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the statistics strip |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawStats(void)
| |||
{
| |||
//--- Fill the strip inside the outline so the graph cannot show through
| |||
m_cv.FillRect(1, OP_HEADER_H, m_w - 2, OP_HEADER_H + OP_STAT_H, m_th.panel);
| |||
m_cv.HRule(1, m_w - 2, OP_HEADER_H + OP_STAT_H, m_th.divider);
| |||
//--- Split the strip into five equal columns
| |||
const int columns = 5;
| |||
const int span = m_w / columns;
| |||
const int top = OP_HEADER_H;
| |||
//--- Name every column
| |||
string label[5];
| |||
label[0] = "nodes";
| |||
label[1] = "parameters";
| |||
label[2] = "tensors";
| |||
label[3] = "input";
| |||
label[4] = "output";
| |||
//--- Read the value for every column
| |||
string shown[5];
| |||
shown[0] = IntegerToString(m_model.Nodes());
| |||
shown[1] = IntegerToString(m_model.Parameters());
| |||
shown[2] = IntegerToString(m_model.Tensors());
| |||
//--- Take the first graph input shape
| |||
OmvPort port;
| |||
m_model.InputAt(0, port);
| |||
shown[3] = (m_model.Inputs() > 0) ? m_model.DimsText(port.dim, port.dims) : "-";
| |||
//--- Take the first graph output shape
| |||
m_model.OutputAt(0, port);
| |||
shown[4] = (m_model.Outputs() > 0) ? m_model.DimsText(port.dim, port.dims) : "-";
| |||
//--- Draw every column and the rule between them
| |||
for(int i = 0; i < columns; i++)
| |||
{
| |||
//--- Center the caption and the value in this column
| |||
const int cx = span * i + span / 2;
| |||
m_cv.Font(8);
| |||
m_cv.TxtC(cx, top + 10, label[i], m_th.muted);
| |||
m_cv.Font(12, true);
| |||
m_cv.TxtC(cx, top + 24, shown[i], m_th.text);
| |||
//--- Rule between this column and the previous one
| |||
if(i > 0)
| |||
m_cv.VRule(span * i, top + 10, top + OP_STAT_H - 10, m_th.divider);
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the neurons and the weights joining them |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawNeurons(void)
| |||
{
| |||
//--- Measure the area the arrangement is drawn inside
| |||
const int left = CanvasLeft(), top = CanvasTop();
| |||
const int right = CanvasRight(), bottom = CanvasBottom();
| |||
//--- Hold every later stroke inside that area less its margin
| |||
m_cv.SetClip(left + OP_PAD, top + OP_PAD,
| |||
right - 1 - (m_barV ? OP_BAR : 0) - OP_PAD,
| |||
bottom - 1 - (m_barH ? OP_BAR : 0) - OP_PAD);
| |||
//--- Say why the view is empty when the model cannot be drawn
| |||
if(!m_cells.Ready())
| |||
{
| |||
//--- Print the reason in the middle of the area
| |||
m_cv.Font(9);
| |||
string why[];
| |||
const int room = right - left - OP_PAD * 4;
| |||
if(room > 0 && m_cv.WrapText("Neuron view unavailable: " + m_cells.Reason(),
| |||
room, why, 1) > 0)
| |||
m_cv.TxtM((left + right) / 2, (top + bottom) / 2, why[0], m_th.muted);
| |||
m_cv.NoClip();
| |||
return;
| |||
}
| |||
//--- Take how far one neuron sits from the next at this zoom
| |||
const double dot = MathMax(2.0, ON_DOT * m_zoom);
| |||
const double pitch = (ON_DOT + ON_DOT_GAP) * m_zoom;
| |||
//--- Draw every run of connections under the neurons themselves
| |||
for(int i = 0; i < m_cells.Webs(); i++)
| |||
{
| |||
//--- Fetch this run and the two rows it joins
| |||
OmvWeb web;
| |||
m_cells.WebAt(i, web);
| |||
OmvBand a, b;
| |||
m_cells.BandAt(web.from, a);
| |||
m_cells.BandAt(web.to, b);
| |||
//--- Place both rows against the current pan and zoom
| |||
const double ay = top + m_panY + (a.y + ON_DOT) * m_zoom;
| |||
const double by = top + m_panY + b.y * m_zoom;
| |||
const double ax = left + m_panX + a.x * m_zoom + dot * 0.5;
| |||
const double bx = left + m_panX + b.x * m_zoom + dot * 0.5;
| |||
//--- Draw a structural run as one line per neuron, carrying no weight
| |||
if(web.tensor < 0)
| |||
{
| |||
//--- Join each source neuron to the place it lands on the joined row
| |||
for(int j = 0; j < a.count; j++)
| |||
m_cv.WireAA(ax + j * pitch, ay,
| |||
bx + (web.offset + j) * pitch, by, m_th.divider);
| |||
continue;
| |||
}
| |||
//--- Fetch the weights this run draws
| |||
OmvTensor w;
| |||
m_model.TensorAt(web.tensor, w);
| |||
//--- Scale the color and thickness against the largest weight held
| |||
const double span = MathMax(MathAbs(w.minValue), MathAbs(w.maxValue));
| |||
if(span <= 0.0 || w.kept <= 0)
| |||
continue;
| |||
//--- Thin the run when a full matrix would be more lines than we can draw
| |||
const int lines = a.count * b.count;
| |||
const double keep = (lines > ON_MAX_LINES) ? (double)ON_MAX_LINES / lines : 1.0;
| |||
const double floor = (keep < 1.0) ? span * (1.0 - keep) : 0.0;
| |||
//--- Draw one line per weight, from each source neuron to each target
| |||
for(int r = 0; r < a.count; r++)
| |||
for(int c = 0; c < b.count; c++)
| |||
{
| |||
//--- Take the weight joining this pair, whichever way it is stored
| |||
const int at = web.transposed ? c * a.count + r : r * b.count + c;
| |||
if(at >= w.kept)
| |||
continue;
| |||
const double v = w.val[at];
| |||
//--- Drop the weakest connections when the run had to be thinned
| |||
if(MathAbs(v) < floor)
| |||
continue;
| |||
//--- Fade the line by how strong the weight is against the largest
| |||
const double strength = MathAbs(v) / span;
| |||
const uint ink = (v >= 0.0) ? m_th.linkUp : m_th.linkDown;
| |||
const uchar alpha = (uchar)(40.0 + 175.0 * strength);
| |||
//--- Join the two neurons at the color the weight implies
| |||
m_cv.WireAA(ax + r * pitch, ay, bx + c * pitch, by,
| |||
(ink & 0x00FFFFFF) | ((uint)alpha << 24));
| |||
}
| |||
}
| |||
//--- Draw every neuron over the connections that reach it
| |||
for(int i = 0; i < m_cells.Bands(); i++)
| |||
{
| |||
//--- Fetch this row
| |||
OmvBand band;
| |||
m_cells.BandAt(i, band);
| |||
//--- Place it against the current pan and zoom
| |||
const double y = top + m_panY + band.y * m_zoom;
| |||
const double x = left + m_panX + band.x * m_zoom;
| |||
//--- Draw one filled circle per neuron
| |||
for(int j = 0; j < band.count; j++)
| |||
m_cv.DiscAA((int)(x + j * pitch + dot * 0.5), (int)(y + dot * 0.5),
| |||
(int)MathMax(1.0, dot * 0.5), m_th.cell);
| |||
//--- Name the row over its own centre, so two rows never collide
| |||
m_cv.Font((int)MathMax(1.0, MathRound(8 * m_zoom)));
| |||
const string caption = band.label + " " + IntegerToString(band.count);
| |||
const int high = m_cv.TextHigh(caption);
| |||
const int wide = m_cv.TextWide(caption);
| |||
const int padX = (int)MathMax(2.0, 4 * m_zoom);
| |||
//--- Centre the plate on the row it names
| |||
const int plateW = wide + padX * 2;
| |||
const int plateX = (int)(x + (band.w * m_zoom - plateW) / 2);
| |||
//--- Sit the caption above the row, or below it where there is no room
| |||
int capY = (int)(y - high - padX * 2);
| |||
if(capY - padX < CanvasTop() + OP_PAD)
| |||
capY = (int)(y + dot + padX * 2);
| |||
//--- Draw the caption while its own plate still sits inside the view
| |||
if(m_cv.InClip(plateX, capY - padX / 2, plateX + plateW, capY + high + padX / 2))
| |||
{
| |||
//--- Tint the plate rather than hiding what runs behind it
| |||
const int radius = (int)MathMax(1.0, 3 * m_zoom);
| |||
m_cv.RoundFill(plateX, capY - padX / 2, plateW, high + padX, radius,
| |||
(m_th.panel & 0x00FFFFFF) | 0x80000000);
| |||
m_cv.RoundStroke(plateX, capY - padX / 2, plateW, high + padX, radius, 1, m_th.divider);
| |||
m_cv.TxtM(plateX + plateW / 2, capY + high / 2, caption, m_th.text);
| |||
}
| |||
}
| |||
//--- Open the clip again for whatever draws next
| |||
m_cv.NoClip();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the graph inside its canvas area |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawGraph(void)
| |||
{
| |||
//--- Measure the area the graph may occupy
| |||
const int left = CanvasLeft();
| |||
const int top = CanvasTop();
| |||
const int right = CanvasRight();
| |||
const int bottom = CanvasBottom();
| |||
//--- Hold every later stroke inside the area less its margin
| |||
m_cv.SetClip(left + OP_PAD, top + OP_PAD,
| |||
right - 1 - (m_barV ? OP_BAR : 0) - OP_PAD,
| |||
bottom - 1 - (m_barH ? OP_BAR : 0) - OP_PAD);
| |||
//--- Start the pickable lists empty for this frame
| |||
m_rows = 0;
| |||
m_links = 0;
| |||
//--- Report an empty graph rather than drawing nothing
| |||
if(m_layout.Boxes() <= 0)
| |||
{
| |||
m_cv.Font(9);
| |||
m_cv.TxtC((left + right) / 2, (top + bottom) / 2, "no graph to show", m_th.muted);
| |||
return;
| |||
}
| |||
//--- Draw every edge under the boxes it connects
| |||
for(int i = 0; i < m_layout.Edges(); i++)
| |||
{
| |||
//--- Fetch this edge
| |||
OmvEdge edge;
| |||
m_layout.EdgeAt(i, edge);
| |||
//--- Fetch the two boxes it joins
| |||
OmvBox a, b;
| |||
m_layout.BoxAt(edge.from, a);
| |||
m_layout.BoxAt(edge.to, b);
| |||
//--- Count how many edges enter the target and which one this is
| |||
int arrives = 0, ordinal = 0;
| |||
for(int k = 0; k < m_layout.Edges(); k++)
| |||
{
| |||
//--- Fetch the candidate edge
| |||
OmvEdge other;
| |||
m_layout.EdgeAt(k, other);
| |||
//--- Count only the ones landing on the same box
| |||
if(other.to != edge.to)
| |||
continue;
| |||
if(k == i)
| |||
ordinal = arrives;
| |||
arrives++;
| |||
}
| |||
//--- Count how many edges leave the source and which one this is
| |||
int departs = 0, place = 0;
| |||
for(int k = 0; k < m_layout.Edges(); k++)
| |||
{
| |||
//--- Fetch the candidate edge
| |||
OmvEdge other;
| |||
m_layout.EdgeAt(k, other);
| |||
//--- Count only the ones leaving the same box
| |||
if(other.from != edge.from)
| |||
continue;
| |||
if(k == i)
| |||
place = departs;
| |||
departs++;
| |||
}
| |||
//--- Spread both ends so no two routes can share a point
| |||
const double leaveAt = (double)(place + 1) / (departs + 1);
| |||
const double share = (double)(ordinal + 1) / (arrives + 1);
| |||
//--- Leave the bottom of the source box at this edge's own share of it
| |||
const double x1 = left + m_panX + (a.x + a.w * leaveAt) * m_zoom;
| |||
const double y1 = top + m_panY + (a.y + a.h) * m_zoom;
| |||
//--- Enter the top of the target box at this edge's own share of it
| |||
const double x2 = left + m_panX + (b.x + b.w * share) * m_zoom;
| |||
const double y2 = top + m_panY + b.y * m_zoom;
| |||
//--- Skip an edge that falls wholly outside the visible area
| |||
if(MathMax(y1, y2) < top || MathMin(y1, y2) > bottom)
| |||
continue;
| |||
if(MathMax(x1, x2) < left || MathMin(x1, x2) > right)
| |||
continue;
| |||
//--- Measure the room each entry point has before the head is sized
| |||
const double gap = (b.w * m_zoom) / (arrives + 1);
| |||
//--- Scale the head with the zoom, yielding only where entry points crowd
| |||
const double head = MathMin(5.0 * m_zoom, gap - 1.0);
| |||
//--- Only the bend follows the zoom, so the route keeps its shape
| |||
const double bend = MathMax(4.0, 10.0 * m_zoom);
| |||
//--- Light every connection carrying the value the pointer is on
| |||
bool hot = (StringLen(m_litName) > 0 && edge.label == m_litName);
| |||
//--- Light every connection carrying the value the inspector describes
| |||
if(!hot && m_pickKind == OP_PICK_LINK && m_pickLink >= 0)
| |||
{
| |||
//--- Fetch the chosen connection to read the value it carries
| |||
OmvEdge chosen;
| |||
m_layout.EdgeAt(m_pickLink, chosen);
| |||
hot = (edge.label == chosen.label);
| |||
}
| |||
//--- Draw a bent connector so crossings read cleanly
| |||
m_cv.Elbow(x1, y1, x2, y2, bend, head, hot ? m_th.accent : m_th.muted);
| |||
//--- Remember the route itself so the pointer can pick the right one
| |||
ArrayResize(m_linkPts, m_links + 1);
| |||
ArrayResize(m_linkCount, m_links + 1);
| |||
ArrayResize(m_linkEdge, m_links + 1);
| |||
//--- Tie the route to its connection, since skipped edges shift the slots
| |||
m_linkEdge[m_links] = i;
| |||
const double midY = (y1 + (y2 - head)) * 0.5;
| |||
m_linkPts[m_links][0] = (int)x1;
| |||
m_linkPts[m_links][1] = (int)y1;
| |||
m_linkPts[m_links][2] = (int)x1;
| |||
m_linkPts[m_links][3] = (int)midY;
| |||
m_linkPts[m_links][4] = (int)x2;
| |||
m_linkPts[m_links][5] = (int)midY;
| |||
m_linkPts[m_links][6] = (int)x2;
| |||
m_linkPts[m_links][7] = (int)y2;
| |||
m_linkCount[m_links] = 4;
| |||
m_links++;
| |||
//--- Print the shape the connection carries beside its drop
| |||
if(StringLen(edge.shape) == 0)
| |||
continue;
| |||
//--- Give the label the same room a weight row gets, so both scale alike
| |||
const int labelH = (int)MathMax(1.0, OL_ROW_H * m_zoom) - 1;
| |||
const int labelW = (int)MathMax(1.0, OL_BOX_W * m_zoom);
| |||
m_cv.FitFont(edge.shape, labelW, labelH, 8, 1, false);
| |||
const int lw = m_cv.TextWide(edge.shape);
| |||
const int lh = m_cv.TextHigh(edge.shape);
| |||
const int lx = (int)x1 + 5;
| |||
const int ly = (int)(y1 + (y2 - y1) * 0.25);
| |||
//--- Draw it while its own glyphs still sit inside the view
| |||
if(m_cv.InClip(lx, ly - lh / 2, lx + lw, ly + lh / 2))
| |||
m_cv.TxtL(lx, ly - lh / 2, edge.shape, m_th.muted);
| |||
}
| |||
//--- Draw every box over the edges
| |||
for(int i = 0; i < m_layout.Boxes(); i++)
| |||
{
| |||
//--- Fetch this box
| |||
OmvBox box;
| |||
m_layout.BoxAt(i, box);
| |||
//--- Convert it out of graph space into canvas space
| |||
const int bx = left + m_panX + (int)(box.x * m_zoom);
| |||
const int by = top + m_panY + (int)(box.y * m_zoom);
| |||
const int bw = (int)(box.w * m_zoom);
| |||
const int bh = (int)(box.h * m_zoom);
| |||
//--- Skip a box that falls outside the visible area
| |||
if(bx + bw < left || bx > right || by + bh < top || by > bottom)
| |||
continue;
| |||
//--- Take the caption from the node, or from the port for a port box
| |||
OmvNode node;
| |||
string caption = box.portName;
| |||
if(box.node >= 0)
| |||
{
| |||
m_model.NodeAt(box.node, node);
| |||
caption = node.op;
| |||
}
| |||
//--- Fill the box in its role color
| |||
m_cv.RoundFill(bx, by, bw, bh, 4, FillForRole(box.role));
| |||
//--- Give a node holding weights a header strip over a listing body
| |||
const int headH = (box.rowCount > 0) ? (int)(OL_HEAD_H * m_zoom) : bh;
| |||
if(box.rowCount > 0)
| |||
m_cv.RoundFillSel(bx, by, bw, headH, 4, EdgeForRole(box.role), true, true, false, false);
| |||
//--- A box reads as chosen only while the inspector describes a box
| |||
const bool chosen = (i == m_selected) &&
| |||
(m_pickKind == OP_PICK_NODE || m_pickKind == OP_PICK_GRAPH);
| |||
//--- Thicken the outline instead of ringing a marked box
| |||
const bool marked = (chosen || i == m_hoverNode);
| |||
m_cv.RoundStroke(bx, by, bw, bh, 4, marked ? 2 : 1,
| |||
marked ? m_th.accent : EdgeForRole(box.role));
| |||
//--- Pick the largest type the header can hold
| |||
m_cv.FitFont(caption, bw - 6, headH - 2, 11, 1, true);
| |||
const int titleH = m_cv.TextHigh(caption);
| |||
//--- Name the operator, light on the strip or in the role color without one
| |||
const uint titleInk = (box.rowCount > 0) ? FillForRole(box.role) : EdgeForRole(box.role);
| |||
const int midY = by + headH / 2;
| |||
const int halfW = m_cv.TextWide(caption) / 2 + 1;
| |||
//--- Draw the title while its own glyphs still sit inside the view
| |||
if(m_cv.InClip(bx + bw / 2 - halfW, midY - titleH / 2, bx + bw / 2 + halfW, midY + titleH / 2))
| |||
m_cv.TxtM(bx + bw / 2, midY, caption, titleInk);
| |||
//--- List every weight the node reads, one row each
| |||
const int rowH = (int)(OL_ROW_H * m_zoom);
| |||
const int pad = (int)MathMax(1.0, OL_ROW_PAD * m_zoom);
| |||
for(int r = 0; r < box.rowCount && rowH >= 1; r++)
| |||
{
| |||
//--- Place this row inside the body, clear of the header and the floor
| |||
const int ry = by + headH + pad + r * rowH;
| |||
if(ry + rowH > by + bh)
| |||
break;
| |||
//--- Inset the row by the same margin on every side
| |||
const int rowX = bx + pad;
| |||
const int rowW = bw - pad * 2;
| |||
//--- Remember it so the pointer can pick it
| |||
ArrayResize(m_rowRect, m_rows + 1);
| |||
ArrayResize(m_rowTensor, m_rows + 1);
| |||
m_rowRect[m_rows][0] = rowX;
| |||
m_rowRect[m_rows][1] = ry;
| |||
m_rowRect[m_rows][2] = rowW;
| |||
m_rowRect[m_rows][3] = rowH;
| |||
m_rowTensor[m_rows] = box.rowTensor[r];
| |||
//--- Mark a row the pointer sits on or the inspector describes
| |||
const bool hot = (m_hoverRow == m_rows) ||
| |||
(m_pickKind == OP_PICK_TENSOR && m_pickTensor == box.rowTensor[r]) ||
| |||
(StringLen(m_litName) > 0 && box.rowName[r] == m_litName);
| |||
m_rows++;
| |||
if(hot)
| |||
m_cv.RoundStroke(rowX, ry, rowW, rowH, 3, 1, m_th.accent);
| |||
//--- Print the letter the operator calls this input
| |||
m_cv.FitFont(box.rowText[r], rowW - 8, rowH - 1, 8, 1, false);
| |||
const int rowTH = m_cv.TextHigh(box.rowText[r]);
| |||
//--- Draw the row while its own glyphs still sit inside the view
| |||
if(m_cv.InClip(rowX, ry, rowX + rowW, ry + rowH))
| |||
{
| |||
m_cv.TxtL(rowX + 4, ry + (rowH - rowTH) / 2, box.rowLabel[r], m_th.text);
| |||
m_cv.TxtR(rowX + rowW - 4, ry + (rowH - rowTH) / 2, box.rowText[r], m_th.muted);
| |||
}
| |||
}
| |||
}
| |||
//--- Open the clip again for whatever draws next
| |||
m_cv.NoClip();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Decide which scrollbars the graph currently needs |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::SizeBars(void)
| |||
{
| |||
//--- Measure the view as though neither bar were present
| |||
const int roomW = CanvasRight() - CanvasLeft();
| |||
const int roomH = CanvasBottom() - CanvasTop();
| |||
//--- Take the graph at the current zoom
| |||
const int fullW = ScaledW(), fullH = ScaledH();
| |||
//--- Decide each bar, allowing for the gutter the other one takes
| |||
m_barV = (fullH > roomH - OP_PAD * 2);
| |||
m_barH = (fullW > roomW - OP_PAD * 2 - (m_barV ? OP_BAR : 0));
| |||
m_barV = (fullH > roomH - OP_PAD * 2 - (m_barH ? OP_BAR : 0));
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw whichever scrollbars the graph needs |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawBars(void)
| |||
{
| |||
//--- Measure the view and the scaled graph
| |||
const int viewW = ViewW(), viewH = ViewH();
| |||
const int fullW = ScaledW(), fullH = ScaledH();
| |||
const int left = CanvasLeft(), top = CanvasTop();
| |||
//--- Draw the vertical bar only when the graph is taller than the view
| |||
if(!m_barV || viewH <= 0)
| |||
{
| |||
m_rBarV[2] = 0;
| |||
m_rBarV[3] = 0;
| |||
}
| |||
else
| |||
{
| |||
//--- Lay the track down the right edge of the view
| |||
const int trackX = CanvasRight() - OP_BAR;
| |||
m_cv.RoundFill(trackX, top, OP_BAR, viewH, OP_BAR / 2, m_th.grid);
| |||
//--- Size the thumb by how much of the graph is visible
| |||
int thumbH = (int)((double)viewH * viewH / fullH);
| |||
if(thumbH < 20)
| |||
thumbH = 20;
| |||
//--- Map the whole pan range, margin included, onto the track
| |||
const int span = fullH - viewH + OP_PAD * 2;
| |||
double frac = (span > 0) ? (double)(OP_PAD - m_panY) / span : 0.0;
| |||
if(frac < 0.0)
| |||
frac = 0.0;
| |||
if(frac > 1.0)
| |||
frac = 1.0;
| |||
//--- Place the thumb inside the track it was given
| |||
const int thumbY = top + (int)((viewH - thumbH) * frac);
| |||
//--- Store the thumb so the pointer can grab it
| |||
m_rBarV[0] = trackX;
| |||
m_rBarV[1] = thumbY;
| |||
m_rBarV[2] = OP_BAR;
| |||
m_rBarV[3] = thumbH;
| |||
//--- Draw the thumb, lit while the pointer works it
| |||
const bool lit = (m_hover == OH_BAR_V || m_barDrag == OH_BAR_V);
| |||
m_cv.RoundFill(trackX, thumbY, OP_BAR, thumbH, OP_BAR / 2, lit ? m_th.accent : m_th.muted);
| |||
}
| |||
//--- Draw the horizontal bar only when the graph is wider than the view
| |||
if(!m_barH || viewW <= 0)
| |||
{
| |||
m_rBarH[2] = 0;
| |||
m_rBarH[3] = 0;
| |||
return;
| |||
}
| |||
//--- Lay the track along the bottom edge of the view
| |||
const int trackY = CanvasBottom() - OP_BAR;
| |||
m_cv.RoundFill(left, trackY, viewW, OP_BAR, OP_BAR / 2, m_th.grid);
| |||
//--- Size the thumb by how much of the graph is visible
| |||
int thumbW = (int)((double)viewW * viewW / fullW);
| |||
if(thumbW < 20)
| |||
thumbW = 20;
| |||
//--- Map the whole pan range, margin included, onto the track
| |||
const int span = fullW - viewW + OP_PAD * 2;
| |||
double frac = (span > 0) ? (double)(OP_PAD - m_panX) / span : 0.0;
| |||
if(frac < 0.0)
| |||
frac = 0.0;
| |||
if(frac > 1.0)
| |||
frac = 1.0;
| |||
//--- Place the thumb inside the track it was given
| |||
const int thumbX = left + (int)((viewW - thumbW) * frac);
| |||
//--- Store the thumb so the pointer can grab it
| |||
m_rBarH[0] = thumbX;
| |||
m_rBarH[1] = trackY;
| |||
m_rBarH[2] = thumbW;
| |||
m_rBarH[3] = OP_BAR;
| |||
//--- Draw the thumb, lit while the pointer works it
| |||
const bool lit = (m_hover == OH_BAR_H || m_barDrag == OH_BAR_H);
| |||
m_cv.RoundFill(thumbX, trackY, thumbW, OP_BAR, OP_BAR / 2, lit ? m_th.accent : m_th.muted);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Remember or forget that a row is open |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::ToggleRow(const string key)
| |||
{
| |||
//--- Forget the key when it is already held open
| |||
for(int i = 0; i < m_openCount; i++)
| |||
{
| |||
//--- Drop it by pulling the last entry into its place
| |||
if(m_opened[i] != key)
| |||
continue;
| |||
m_opened[i] = m_opened[m_openCount - 1];
| |||
m_openCount--;
| |||
ArrayResize(m_opened, m_openCount);
| |||
return;
| |||
}
| |||
//--- Hold the key open when it was not already
| |||
ArrayResize(m_opened, m_openCount + 1);
| |||
m_opened[m_openCount++] = key;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Append one row to the inspector listing |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::AddRow(const string name, const string value,
| |||
const bool isHead, const string linkName)
| |||
{
| |||
//--- Refuse a row beyond what the listing may hold
| |||
if(m_infos >= OP_MAX_ROWS)
| |||
return;
| |||
//--- Keep a row open across a rebuild by looking its name up
| |||
bool wasOpen = false;
| |||
for(int i = 0; i < m_openCount; i++)
| |||
if(m_opened[i] == name + "|" + value)
| |||
{
| |||
wasOpen = true;
| |||
break;
| |||
}
| |||
//--- Describe the row
| |||
ArrayResize(m_info, m_infos + 1);
| |||
m_info[m_infos].name = name;
| |||
m_info[m_infos].value = value;
| |||
m_info[m_infos].extraCount = 0;
| |||
m_info[m_infos].wraps = 1;
| |||
m_info[m_infos].canOpen = false;
| |||
m_info[m_infos].isOpen = wasOpen;
| |||
m_info[m_infos].isHead = isHead;
| |||
m_info[m_infos].linkName = linkName;
| |||
m_info[m_infos].y = 0;
| |||
m_info[m_infos].h = 0;
| |||
m_infos++;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Add one line to the row most recently started |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::AddLine(const string line)
| |||
{
| |||
//--- Refuse a line before any row has been started
| |||
if(m_infos <= 0)
| |||
return;
| |||
//--- Refuse a line beyond what the row may hold
| |||
const int last = m_infos - 1;
| |||
if(m_info[last].extraCount >= OP_MAX_LINES)
| |||
return;
| |||
//--- Append the line and mark the row as something that opens
| |||
m_info[last].extra[m_info[last].extraCount++] = line;
| |||
m_info[last].canOpen = true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Add the lines a named value opens to |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::AddValueLines(const string name)
| |||
{
| |||
//--- A stored weight reports its category before anything else
| |||
const int k = TensorNamed(name);
| |||
if(k >= 0)
| |||
{
| |||
//--- Fetch the tensor being reported
| |||
OmvTensor tensor;
| |||
m_model.TensorAt(k, tensor);
| |||
AddLine("category: " + tensor.category);
| |||
AddLine("tensor: " + m_model.TypeName(tensor.dataType) +
| |||
m_model.DimsList(tensor.dim, tensor.dims));
| |||
//--- Say so when the values live outside the file rather than showing none
| |||
if(tensor.location != "default")
| |||
AddLine("location: " + tensor.location);
| |||
//--- Print the weights themselves, wrapped on the last axis
| |||
AddTensorValues(tensor);
| |||
return;
| |||
}
| |||
//--- A graph port reports the type the graph declares for it
| |||
for(int p = 0; p < m_model.Inputs(); p++)
| |||
{
| |||
//--- Fetch this graph input
| |||
OmvPort port;
| |||
m_model.InputAt(p, port);
| |||
//--- Report its type once the name matches
| |||
if(port.name != name)
| |||
continue;
| |||
AddLine("tensor: " + m_model.TypeName(port.elemType) +
| |||
m_model.DimsList(port.dim, port.dims));
| |||
return;
| |||
}
| |||
//--- A value passing between layers reports its type when the file says so
| |||
for(int p = 0; p < m_model.Inners(); p++)
| |||
{
| |||
//--- Fetch this inner value
| |||
OmvPort port;
| |||
m_model.InnerAt(p, port);
| |||
//--- Report its type once the name matches
| |||
if(port.name != name)
| |||
continue;
| |||
AddLine("tensor: " + m_model.TypeName(port.elemType) +
| |||
m_model.DimsList(port.dim, port.dims));
| |||
return;
| |||
}
| |||
//--- A graph output reports its type the same way
| |||
for(int p = 0; p < m_model.Outputs(); p++)
| |||
{
| |||
//--- Fetch this graph output
| |||
OmvPort port;
| |||
m_model.OutputAt(p, port);
| |||
//--- Report its type once the name matches
| |||
if(port.name != name)
| |||
continue;
| |||
AddLine("tensor: " + m_model.TypeName(port.elemType) +
| |||
m_model.DimsList(port.dim, port.dims));
| |||
return;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Print a tensor's weights, wrapped on its last axis |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::AddTensorValues(const OmvTensor &tensor)
| |||
{
| |||
//--- Print the strings when the tensor holds text rather than numbers
| |||
if(tensor.texts > 0)
| |||
{
| |||
//--- Open the bracket and list one string to a line
| |||
AddLine("[");
| |||
for(int s = 0; s < tensor.texts; s++)
| |||
AddLine(" " + tensor.text[s] + ((s + 1 < tensor.texts) ? "," : ""));
| |||
AddLine("]");
| |||
return;
| |||
}
| |||
//--- Nothing to print when the file carried no weights
| |||
if(tensor.kept <= 0)
| |||
return;
| |||
//--- Take the last axis as the length of an innermost run
| |||
const int inner = (tensor.dims > 0) ? tensor.dim[tensor.dims - 1] : tensor.kept;
| |||
if(inner <= 0)
| |||
return;
| |||
//--- Open the outermost bracket
| |||
AddLine("[");
| |||
//--- A one axis tensor is a single run with no nesting
| |||
const bool nested = (tensor.dims > 1);
| |||
const string pad = nested ? " " : " ";
| |||
//--- Walk the weights, one to a line, closing each run as it ends
| |||
for(int i = 0; i < tensor.kept; i++)
| |||
{
| |||
//--- Open a run whenever one begins
| |||
if(nested && i % inner == 0)
| |||
AddLine(" [");
| |||
//--- Print this weight, with a comma unless it ends its run
| |||
const bool lastInRun = ((i + 1) % inner == 0) || (i + 1 == tensor.kept);
| |||
AddLine(pad + DoubleToString(tensor.val[i], 8) + (lastInRun ? "" : ","));
| |||
//--- Close a run whenever one ends
| |||
if(nested && lastInRun)
| |||
AddLine((i + 1 == tensor.kept) ? " ]" : " ],");
| |||
}
| |||
//--- Say so when the tensor held more than the display keeps
| |||
if(tensor.values > tensor.kept)
| |||
AddLine(" ... " + IntegerToString(tensor.values - tensor.kept) + " more");
| |||
//--- Close the outermost bracket
| |||
AddLine("]");
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Add the lines a named value opens to |
| |||
//+------------------------------------------------------------------+
| |||
//| Turn the current selection into inspector rows |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::BuildInfo(void)
| |||
{
| |||
//--- Start a fresh listing
| |||
const int held = m_infos;
| |||
m_infos = 0;
| |||
//--- Describe a chosen weight tensor
| |||
if(m_pickKind == OP_PICK_TENSOR && m_pickTensor >= 0)
| |||
{
| |||
//--- Fetch the tensor being described
| |||
OmvTensor tensor;
| |||
m_model.TensorAt(m_pickTensor, tensor);
| |||
AddRow("name", tensor.name, false);
| |||
AddRow("category", tensor.category, false);
| |||
AddRow("type", m_model.TypeName(tensor.dataType), false);
| |||
AddRow("shape", m_model.DimsBare(tensor.dim, tensor.dims), false);
| |||
//--- Say so when the file keeps the values somewhere else
| |||
if(tensor.location != "default")
| |||
AddRow("location", tensor.location, false);
| |||
//--- Say so when the file stored this tensor in pieces
| |||
if(tensor.segmented)
| |||
AddRow("segment", "present", false);
| |||
AddRow("value", IntegerToString(tensor.values), false);
| |||
AddTensorValues(tensor);
| |||
AddRow("METRICS", "", true);
| |||
//--- Each metric reports the type it was measured in
| |||
const string metricType = m_model.TypeName(tensor.dataType);
| |||
AddRow("min", Measure(tensor.minValue), false);
| |||
AddLine("type: " + metricType);
| |||
AddRow("max", Measure(tensor.maxValue), false);
| |||
AddLine("type: " + metricType);
| |||
AddRow("mean", Measure(tensor.meanValue), false);
| |||
AddLine("type: " + metricType);
| |||
//--- The spread declares no type, so it carries no opened line
| |||
AddRow("std", Measure(tensor.stdValue), false);
| |||
AddRow("sparsity", DoubleToString(tensor.sparsity * 100.0, 1) + "%", false);
| |||
AddLine("type: percentage");
| |||
return;
| |||
}
| |||
//--- Describe a chosen connection
| |||
if(m_pickKind == OP_PICK_LINK && m_pickLink >= 0)
| |||
{
| |||
//--- Fetch the connection being described
| |||
OmvEdge edge;
| |||
m_layout.EdgeAt(m_pickLink, edge);
| |||
//--- Name the value and report what it opens to
| |||
AddRow("name", edge.label, false);
| |||
AddRow("type", "name: " + edge.label, false, edge.label);
| |||
AddValueLines(edge.label);
| |||
//--- List every box that writes the value
| |||
int writers = 0;
| |||
for(int e = 0; e < m_layout.Edges(); e++)
| |||
{
| |||
//--- Take only the connections carrying the same value
| |||
OmvEdge other;
| |||
m_layout.EdgeAt(e, other);
| |||
if(other.label != edge.label)
| |||
continue;
| |||
//--- Report each source once, heading the section first
| |||
if(writers == 0)
| |||
AddRow("INPUTS", "", true);
| |||
writers++;
| |||
AddEndpoint("from", other.from);
| |||
break;
| |||
}
| |||
//--- List every box that reads the value
| |||
int readers = 0;
| |||
for(int e = 0; e < m_layout.Edges(); e++)
| |||
{
| |||
//--- Take only the connections carrying the same value
| |||
OmvEdge other;
| |||
m_layout.EdgeAt(e, other);
| |||
if(other.label != edge.label)
| |||
continue;
| |||
//--- Report each target, heading the section before the first
| |||
if(readers == 0)
| |||
AddRow("OUTPUTS", "", true);
| |||
readers++;
| |||
AddEndpoint("to", other.to);
| |||
}
| |||
return;
| |||
}
| |||
//--- Describe the file as a whole
| |||
if(m_pickKind == OP_PICK_MODEL)
| |||
{
| |||
//--- Report the format the file announces
| |||
AddRow("format", "ONNX v" + IntegerToString(m_model.IrVersion()), false);
| |||
//--- Report the tool that wrote it, with its version when it gave one
| |||
if(StringLen(m_model.Producer()) > 0)
| |||
{
| |||
string who = m_model.Producer();
| |||
if(StringLen(m_model.ProducerVersion()) > 0)
| |||
who += " " + m_model.ProducerVersion();
| |||
AddRow("producer", who, false);
| |||
}
| |||
//--- Report the revision the model gives itself
| |||
AddRow("version", IntegerToString(m_model.ModelVersion()), false);
| |||
//--- Report whatever the file says about itself
| |||
if(StringLen(m_model.DocString()) > 0)
| |||
AddRow("description", m_model.DocString(), false);
| |||
//--- Report the domain the model belongs to
| |||
if(StringLen(m_model.Domain()) > 0)
| |||
AddRow("domain", m_model.Domain(), false);
| |||
//--- Report the operator set it imports
| |||
AddRow("imports", "ai.onnx v" + IntegerToString(m_model.Opset()), false);
| |||
//--- Report whether the file carries training state beside the model
| |||
if(m_model.HasTraining())
| |||
AddRow("training", "present", false);
| |||
//--- List every operator the file defines for itself
| |||
if(m_model.Funcs() > 0)
| |||
{
| |||
AddRow("FUNCTIONS", "", true);
| |||
for(int i = 0; i < m_model.Funcs(); i++)
| |||
{
| |||
//--- Name the definition, then say what it holds
| |||
AddRow(m_model.FuncName(i), m_model.FuncDetail(i), false);
| |||
}
| |||
}
| |||
//--- List every key and value pair the file attached to the model
| |||
if(m_model.Metas() > 0)
| |||
{
| |||
AddRow("METADATA", "", true);
| |||
for(int i = 0; i < m_model.Metas(); i++)
| |||
AddRow(m_model.MetaKey(i), m_model.MetaValue(i), false);
| |||
}
| |||
return;
| |||
}
| |||
//--- Describe the graph as a whole
| |||
if(m_pickKind == OP_PICK_GRAPH)
| |||
{
| |||
//--- Name the graph
| |||
AddRow("name", m_model.GraphName(), false);
| |||
//--- Report whatever the file says about the graph
| |||
if(StringLen(m_model.GraphDoc()) > 0)
| |||
AddRow("description", m_model.GraphDoc(), false);
| |||
//--- List every value the graph takes in
| |||
if(m_model.Inputs() > 0)
| |||
AddRow("INPUTS", "", true);
| |||
for(int p = 0; p < m_model.Inputs(); p++)
| |||
{
| |||
//--- Fetch this graph input
| |||
OmvPort port;
| |||
m_model.InputAt(p, port);
| |||
//--- Report it with its tensor on the open line
| |||
AddRow(port.name, "name: " + port.name, false, port.name);
| |||
AddValueLines(port.name);
| |||
}
| |||
//--- List every value the graph gives back
| |||
if(m_model.Outputs() > 0)
| |||
AddRow("OUTPUTS", "", true);
| |||
for(int p = 0; p < m_model.Outputs(); p++)
| |||
{
| |||
//--- Fetch this graph output
| |||
OmvPort port;
| |||
m_model.OutputAt(p, port);
| |||
//--- Report it with its tensor on the open line
| |||
AddRow(port.name, "name: " + port.name, false, port.name);
| |||
AddValueLines(port.name);
| |||
}
| |||
//--- Report what the graph weighs
| |||
AddRow("METRICS", "", true);
| |||
AddRow("parameters", IntegerToString(m_model.Parameters()), false);
| |||
//--- Report how many values the graph annotates for quantization
| |||
if(m_model.Quants() > 0)
| |||
AddRow("quantized", IntegerToString(m_model.Quants()), false);
| |||
//--- List every key and value pair the file attached to the graph
| |||
if(m_model.GraphMetas() > 0)
| |||
{
| |||
AddRow("METADATA", "", true);
| |||
for(int i = 0; i < m_model.GraphMetas(); i++)
| |||
AddRow(m_model.GraphMetaKey(i), m_model.GraphMetaValue(i), false);
| |||
}
| |||
return;
| |||
}
| |||
//--- Describe a chosen layer
| |||
if(m_pickKind == OP_PICK_NODE && m_selected >= 0 && m_selected < m_layout.Boxes())
| |||
{
| |||
//--- Fetch the box the selection points at
| |||
OmvBox box;
| |||
m_layout.BoxAt(m_selected, box);
| |||
//--- Fetch the node the box was placed for
| |||
OmvNode node;
| |||
m_model.NodeAt(box.node, node);
| |||
AddRow("type", node.op, false);
| |||
//--- An empty domain means the operator comes from the default set
| |||
const string domain = (StringLen(node.domain) > 0) ? node.domain : "ai.onnx";
| |||
AddRow("module", domain + " v" + IntegerToString(m_model.Opset()), false);
| |||
AddRow("name", node.name, false);
| |||
//--- Report the overload the operator was resolved to
| |||
if(StringLen(node.overload) > 0)
| |||
AddRow("overload", node.overload, false);
| |||
//--- Report whatever the file says about this layer
| |||
if(StringLen(node.docString) > 0)
| |||
AddRow("description", node.docString, false);
| |||
//--- Report whatever the operator was configured with
| |||
if(node.attrCount > 0)
| |||
{
| |||
AddRow("ATTRIBUTES", "", true);
| |||
for(int a = 0; a < node.attrCount; a++)
| |||
{
| |||
//--- The Cast target names a type rather than carrying a number
| |||
const bool isType = (node.op == "Cast" && node.attrName[a] == "to");
| |||
const string shown = isType
| |||
? m_model.TypeName((int)StringToInteger(node.attrValue[a]))
| |||
: node.attrValue[a];
| |||
//--- Report the value, then the type it opens to
| |||
AddRow(node.attrName[a], shown, false);
| |||
AddLine("type: " + (isType ? "DataType" : m_model.AttrType(node.attrKind[a])));
| |||
}
| |||
}
| |||
AddRow("INPUTS", "", true);
| |||
//--- Name every input the way the operator names it
| |||
for(int j = 0; j < node.feedCount; j++)
| |||
{
| |||
//--- Report the name, then whatever the value opens to
| |||
AddRow(ArgIn(node.op, j), "name: " + node.feeds[j], false, node.feeds[j]);
| |||
AddValueLines(node.feeds[j]);
| |||
}
| |||
AddRow("OUTPUTS", "", true);
| |||
//--- List every name the node writes
| |||
for(int j = 0; j < node.emitCount; j++)
| |||
{
| |||
//--- Report the name, then whatever the value opens to
| |||
AddRow(ArgOut(node.op), "name: " + node.emits[j], false, node.emits[j]);
| |||
AddValueLines(node.emits[j]);
| |||
}
| |||
return;
| |||
}
| |||
//--- Leave the listing empty when nothing is chosen
| |||
if(held > 0)
| |||
ArrayResize(m_info, 0);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the inspector for the selected node |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawInspector(void)
| |||
{
| |||
//--- Skip the column entirely while it is closed
| |||
if(!m_inspect)
| |||
return;
| |||
//--- Fill the column edge to edge so the graph can never show through it
| |||
const int colX = CanvasRight();
| |||
m_cv.FillRect(colX + 1, CanvasTop(), m_w - 2, CanvasBottom() - 1, m_th.panel);
| |||
m_cv.VRule(colX, CanvasTop(), CanvasBottom() - 1, m_th.divider);
| |||
//--- Measure the column and the band the rows scroll inside
| |||
const int left = colX + 16;
| |||
const int right = m_w - 16;
| |||
const int top = CanvasTop() + 44;
| |||
const int bottom = CanvasBottom() - 8;
| |||
//--- Offer to close the column
| |||
DrawIcon(m_rInspectX, OH_INSPECT_X, 5);
| |||
//--- Title the column after whatever it is describing
| |||
m_cv.Font(8, true);
| |||
string heading = "PROPERTIES";
| |||
if(m_pickKind == OP_PICK_NODE)
| |||
heading = "NODE PROPERTIES";
| |||
else
| |||
if(m_pickKind == OP_PICK_TENSOR)
| |||
heading = "TENSOR PROPERTIES";
| |||
else
| |||
if(m_pickKind == OP_PICK_LINK)
| |||
heading = "CONNECTION PROPERTIES";
| |||
else
| |||
if(m_pickKind == OP_PICK_GRAPH)
| |||
heading = "GRAPH PROPERTIES";
| |||
else
| |||
if(m_pickKind == OP_PICK_MODEL)
| |||
heading = "MODEL PROPERTIES";
| |||
m_cv.TxtL(left, CanvasTop() + 14, heading, m_th.muted);
| |||
//--- Lay the rows out for whatever is chosen
| |||
BuildInfo();
| |||
//--- Invite a selection when the listing came back empty
| |||
if(m_infos <= 0)
| |||
{
| |||
m_cv.Font(9);
| |||
m_cv.TxtL(left, top, "select a node", m_th.muted);
| |||
m_rInfoBar[2] = 0;
| |||
m_rInfoBar[3] = 0;
| |||
return;
| |||
}
| |||
//--- Measure every row and stack them
| |||
m_cv.Font(8);
| |||
const int lineH = m_cv.TextHigh("Ag");
| |||
//--- Assume the scrollbar is present so the width cannot change under us
| |||
const int valX = left + OP_NAME_W;
| |||
const int valW = right - valX - OP_BAR - 2 - OP_VAL_PAD * 2;
| |||
int cursor = 0;
| |||
for(int i = 0; i < m_infos; i++)
| |||
{
| |||
//--- A heading takes a line of its own with space above it
| |||
if(m_info[i].isHead)
| |||
{
| |||
m_info[i].y = cursor + 10;
| |||
m_info[i].h = lineH + 4;
| |||
m_info[i].wraps = 1;
| |||
}
| |||
else
| |||
{
| |||
//--- Take how many lines this value needs at the width it is given
| |||
string lines[];
| |||
m_info[i].wraps = MathMax(1, m_cv.WrapText(m_info[i].value, valW, lines, OP_WRAP_MAX));
| |||
//--- Grow the box by every line past the first, and again once opened
| |||
m_info[i].y = cursor;
| |||
m_info[i].h = OP_VAL_H + (m_info[i].wraps - 1) * lineH +
| |||
(m_info[i].isOpen ? m_info[i].extraCount * (lineH + 2) + OP_VAL_PAD : 0);
| |||
}
| |||
cursor = m_info[i].y + m_info[i].h + OP_VAL_GAP;
| |||
}
| |||
m_infoHigh = cursor;
| |||
//--- Hold the scroll inside what the rows actually need
| |||
const int band = bottom - top;
| |||
if(m_infoHigh <= band)
| |||
m_infoScroll = 0;
| |||
else
| |||
{
| |||
//--- Never scroll past either end of the listing
| |||
if(m_infoScroll < 0)
| |||
m_infoScroll = 0;
| |||
if(m_infoScroll > m_infoHigh - band)
| |||
m_infoScroll = m_infoHigh - band;
| |||
}
| |||
//--- Hold every row inside the band it scrolls in
| |||
const bool bar = (m_infoHigh > band);
| |||
m_cv.SetClip(colX + 1, top, right + (bar ? 0 : 8), bottom);
| |||
//--- Draw every row that falls inside the band
| |||
for(int i = 0; i < m_infos; i++)
| |||
{
| |||
//--- Place this row against the current scroll
| |||
const int ry = top + m_info[i].y - m_infoScroll;
| |||
if(ry + m_info[i].h < top || ry > bottom)
| |||
continue;
| |||
//--- Print a heading and move on
| |||
if(m_info[i].isHead)
| |||
{
| |||
//--- Text ignores the clip, so keep it inside the band by hand
| |||
if(ry >= top && ry + lineH <= bottom)
| |||
{
| |||
m_cv.Font(8, true);
| |||
m_cv.TxtL(left, ry, m_info[i].name, m_th.muted);
| |||
}
| |||
continue;
| |||
}
| |||
//--- Light a row the pointer is on, or one naming the value being lit
| |||
const bool hot = (m_infoHover == i) ||
| |||
(StringLen(m_litName) > 0 && m_info[i].linkName == m_litName);
| |||
m_cv.RoundFill(valX, ry, right - valX - (bar ? OP_BAR + 2 : 0), m_info[i].h, 2,
| |||
hot ? m_th.hoverFace : m_th.grid);
| |||
//--- Outline the box while the pointer sits on a row that leads somewhere
| |||
if(hot && (m_info[i].canOpen || StringLen(m_info[i].linkName) > 0))
| |||
m_cv.RoundStroke(valX, ry, right - valX - (bar ? OP_BAR + 2 : 0), m_info[i].h, 2, 1,
| |||
m_th.accent);
| |||
//--- Take the baseline the name and the first line share
| |||
const int firstY = ry + (OP_VAL_H - lineH) / 2;
| |||
const int valRight = right - (bar ? OP_BAR + 2 : 0) - OP_VAL_PAD;
| |||
//--- Text ignores the clip, so keep the first line inside the band by hand
| |||
if(firstY >= top && firstY + lineH <= bottom)
| |||
{
| |||
//--- Name the row beside its box
| |||
m_cv.Font(8);
| |||
string tag[];
| |||
if(m_cv.WrapText(m_info[i].name, OP_NAME_W - 10, tag, 1) > 0)
| |||
m_cv.TxtR(valX - 8, firstY, tag[0], m_th.muted);
| |||
}
| |||
//--- Print the value as the lines it wrapped onto
| |||
m_cv.Font(8);
| |||
string shown[];
| |||
const int rows = m_cv.WrapText(m_info[i].value, valW, shown, OP_WRAP_MAX);
| |||
for(int k = 0; k < rows; k++)
| |||
{
| |||
//--- Place this line and test it against the band itself
| |||
const int ly = firstY + k * lineH;
| |||
if(ly < top || ly + lineH > bottom)
| |||
continue;
| |||
m_cv.TxtL(valX + OP_VAL_PAD, ly, shown[k], m_th.text);
| |||
}
| |||
//--- Offer the expander on a row that carries a second line
| |||
if(m_info[i].canOpen && firstY >= top && firstY + lineH <= bottom)
| |||
{
| |||
//--- Light the glyph only while the pointer is on it
| |||
const bool onPlus = (hot && m_onExpander);
| |||
m_cv.Font(8, true);
| |||
m_cv.TxtR(valRight, firstY, m_info[i].isOpen ? "-" : "+",
| |||
onPlus ? m_th.accent : m_th.muted);
| |||
}
| |||
//--- Print every line the row carries once it is open
| |||
if(m_info[i].isOpen && m_info[i].extraCount > 0)
| |||
{
| |||
//--- Rule the opened lines off below whatever the value took
| |||
const int ruleY = ry + OP_VAL_H + (m_info[i].wraps - 1) * lineH;
| |||
if(ruleY >= top && ruleY <= bottom)
| |||
m_cv.HRule(valX + 1, valRight, ruleY, m_th.divider);
| |||
m_cv.Font(8);
| |||
//--- Stack the lines under the rule, skipping any outside the band
| |||
for(int e = 0; e < m_info[i].extraCount; e++)
| |||
{
| |||
//--- Place this line and test it against the band itself
| |||
const int ey = ry + OP_VAL_H + (m_info[i].wraps - 1) * lineH
| |||
+ OP_VAL_PAD / 2 + e * (lineH + 2);
| |||
if(ey < top || ey + lineH > bottom)
| |||
continue;
| |||
//--- Hold the line inside the box, marking it when it will not fit
| |||
string cut[];
| |||
if(m_cv.WrapText(m_info[i].extra[e], valW, cut, 1) > 0)
| |||
m_cv.TxtL(valX + OP_VAL_PAD, ey, cut[0], m_th.muted);
| |||
}
| |||
}
| |||
}
| |||
m_cv.NoClip();
| |||
//--- Hide the scrollbar while every row already fits
| |||
if(!bar)
| |||
{
| |||
m_rInfoBar[2] = 0;
| |||
m_rInfoBar[3] = 0;
| |||
return;
| |||
}
| |||
//--- Lay the track down the right edge of the column
| |||
const int trackX = right - OP_BAR;
| |||
m_cv.RoundFill(trackX, top, OP_BAR, band, OP_BAR / 2, m_th.grid);
| |||
//--- Size the thumb by how much of the listing is visible
| |||
int thumbH = (int)((double)band * band / m_infoHigh);
| |||
if(thumbH < 20)
| |||
thumbH = 20;
| |||
//--- Place the thumb by how far the listing has been scrolled
| |||
const double frac = (double)m_infoScroll / (m_infoHigh - band);
| |||
const int thumbY = top + (int)((band - thumbH) * frac);
| |||
//--- Store the thumb so the pointer can grab it
| |||
m_rInfoBar[0] = trackX;
| |||
m_rInfoBar[1] = thumbY;
| |||
m_rInfoBar[2] = OP_BAR;
| |||
m_rInfoBar[3] = thumbH;
| |||
//--- Draw the thumb, lit while the pointer works it
| |||
const bool lit = (m_hover == OH_INFO_BAR || m_barDrag == OH_INFO_BAR);
| |||
m_cv.RoundFill(trackX, thumbY, OP_BAR, thumbH, OP_BAR / 2, lit ? m_th.accent : m_th.muted);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Find the inspector row under a panel point |
| |||
//+------------------------------------------------------------------+
| |||
int COmvPanel::InfoAtPoint(const int mx, const int my)
| |||
{
| |||
//--- Ignore anything outside the column
| |||
if(!m_inspect || mx <= CanvasRight())
| |||
return -1;
| |||
//--- Measure the band the rows scroll inside
| |||
const int top = CanvasTop() + 44;
| |||
const int bottom = CanvasBottom() - 8;
| |||
if(my < top || my > bottom)
| |||
return -1;
| |||
//--- Test every laid out row against the current scroll
| |||
for(int i = 0; i < m_infos; i++)
| |||
{
| |||
//--- Skip a heading, it cannot be picked
| |||
if(m_info[i].isHead)
| |||
continue;
| |||
//--- Report the row that holds the point
| |||
const int ry = top + m_info[i].y - m_infoScroll;
| |||
if(my >= ry && my < ry + m_info[i].h)
| |||
return i;
| |||
}
| |||
//--- Report that no row holds the point
| |||
return -1;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the footer band that does not change between frames |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawFooterChrome(void)
| |||
{
| |||
//--- Fill the band, rounding only the two corners it owns
| |||
const int top = m_h - OP_FOOT_H;
| |||
m_cv.RoundFillSel(0, top, m_w, OP_FOOT_H, OP_RADIUS, m_th.chrome,
| |||
false, false, true, true);
| |||
m_cv.HRule(1, m_w - 2, top, m_th.divider);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the control that swaps between the two views |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawViewSwitch(void)
| |||
{
| |||
//--- Name the view the control would take us to
| |||
const string label = m_neuronView ? "Graph" : "Neurons";
| |||
//--- Take whether the control can be used at all
| |||
const bool live = m_neuronView || m_cells.Ready();
| |||
//--- Light the face while the pointer rests on a control that works
| |||
const bool hot = (m_hover == OH_VIEW && live);
| |||
m_cv.RoundFill(m_rView[0], m_rView[1], m_rView[2], m_rView[3], 4,
| |||
hot ? m_th.hoverFace : m_th.btnFace);
| |||
m_cv.RoundStroke(m_rView[0], m_rView[1], m_rView[2], m_rView[3], 4, 1,
| |||
live ? m_th.btnEdge : m_th.divider);
| |||
//--- Name it, in the muted ink when the model cannot be drawn as neurons
| |||
m_cv.Font(8);
| |||
m_cv.TxtM(m_rView[0] + m_rView[2] / 2, m_rView[1] + m_rView[3] / 2,
| |||
label, live ? m_th.text : m_th.divider);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Draw the footer controls and readouts |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::DrawFooter(void)
| |||
{
| |||
//--- Take the top of the band every control sits in
| |||
const int top = m_h - OP_FOOT_H;
| |||
//--- Draw the three view controls
| |||
DrawButton(m_rFit, OH_FIT, "Fit");
| |||
DrawIcon(m_rZoomOut, OH_ZOOM_OUT, 3);
| |||
DrawIcon(m_rZoomIn, OH_ZOOM_IN, 4);
| |||
//--- Offer the other view, dimmed when this model has no neurons to draw
| |||
DrawViewSwitch();
| |||
//--- Measure the type once so every footer readout shares a baseline
| |||
m_cv.Font(8);
| |||
const int textY = top + OP_FOOT_H / 2;
| |||
//--- Report the panel size beside the grip, which fixes the right hand edge
| |||
const string size = IntegerToString(m_w) + " x " + IntegerToString(m_h);
| |||
m_cv.TxtR(m_w - 26, textY - m_cv.TextHigh(size) / 2, size, m_th.muted);
| |||
//--- Print the zoom just after the controls it belongs to
| |||
const int afterZoom = m_rZoomIn[0] + m_rZoomIn[2] + 8;
| |||
m_cv.TxtL(afterZoom, textY - m_cv.TextHigh("100%") / 2,
| |||
IntegerToString((int)MathRound(m_zoom * 100)) + "%", m_th.muted);
| |||
//--- Name the selected node when one is chosen
| |||
if(m_selected >= 0 && m_selected < m_layout.Boxes())
| |||
{
| |||
//--- Fetch the chosen node
| |||
OmvBox box;
| |||
m_layout.BoxAt(m_selected, box);
| |||
//--- Name a port by itself and a layer by its operator
| |||
string shown = box.portName;
| |||
if(box.node >= 0)
| |||
{
| |||
OmvNode node;
| |||
m_model.NodeAt(box.node, node);
| |||
shown = node.op + " " + node.name;
| |||
}
| |||
//--- Sit it between the view switch and the size readout, never over either
| |||
const int from = m_rView[0] + m_rView[2] + 14;
| |||
const int room = m_w - 26 - m_cv.TextWide(size) - 14 - from;
| |||
string cut[];
| |||
if(room > 0 && m_cv.WrapText(shown, room, cut, 1) > 0)
| |||
m_cv.TxtL(from, textY - m_cv.TextHigh(cut[0]) / 2, cut[0], m_th.muted);
| |||
}
| |||
//--- Draw the three diagonals that mark the grip
| |||
const bool lit = (m_hover == OH_GRIP || m_resizing);
| |||
const uint ink = lit ? m_th.accent : m_th.muted;
| |||
const int gx = m_rGrip[0] + m_rGrip[2];
| |||
const int gy = m_rGrip[1] + m_rGrip[3];
| |||
m_cv.StrokeAA(gx - 3, gy - 12, gx - 12, gy - 3, lit ? 2 : 1, ink);
| |||
m_cv.StrokeAA(gx - 3, gy - 8, gx - 8, gy - 3, lit ? 2 : 1, ink);
| |||
m_cv.StrokeAA(gx - 3, gy - 4, gx - 4, gy - 3, lit ? 2 : 1, ink);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Paint every band in order and publish the bitmap |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::Render(void)
| |||
{
| |||
//--- Do nothing without a canvas or after teardown
| |||
if(m_destroyed || !m_created)
| |||
return;
| |||
//--- Rebuild the cached chrome only when something structural moved
| |||
const string sig = ChromeSignature();
| |||
if(sig != m_bgSig)
| |||
{
| |||
m_bgSig = sig;
| |||
m_bgValid = false;
| |||
}
| |||
//--- Rebuild the cached scene only when the view itself moved
| |||
const string scene = SceneSignature();
| |||
if(scene != m_sceneSig)
| |||
{
| |||
m_sceneSig = scene;
| |||
m_sceneValid = false;
| |||
}
| |||
//--- Put back the whole scene when nothing under the pointer changed it
| |||
if(m_sceneValid && m_mode == OM_FULL)
| |||
m_cv.RestoreFrom(m_scene);
| |||
else
| |||
{
| |||
//--- Draw the chrome once and keep it, or put the kept copy back
| |||
if(!m_bgValid)
| |||
{
| |||
DrawChrome();
| |||
m_cv.SaveTo(m_bg);
| |||
m_bgValid = true;
| |||
}
| |||
else
| |||
m_cv.RestoreFrom(m_bg);
| |||
//--- Draw the view the panel is showing over the cached chrome
| |||
if(m_mode == OM_FULL)
| |||
{
| |||
//--- Keep the finished scene so a hover does not redraw it
| |||
if(m_neuronView)
| |||
DrawNeurons();
| |||
else
| |||
DrawGraph();
| |||
m_cv.SaveTo(m_scene);
| |||
m_sceneValid = true;
| |||
}
| |||
}
| |||
//--- Draw everything that answers to the pointer on top of the scene
| |||
if(m_mode == OM_FULL)
| |||
{
| |||
DrawBars();
| |||
DrawInspector();
| |||
DrawFooter();
| |||
}
| |||
//--- Draw the header icons over the cached band
| |||
DrawHeader();
| |||
//--- Publish the finished bitmap
| |||
m_cv.Update();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Repaint no faster than the frame cap allows |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::PaintCapped(void)
| |||
{
| |||
//--- Read the clock once for this decision
| |||
const uint now = GetTickCount();
| |||
//--- Remember a skipped frame so the next event still lands it
| |||
if(now - m_lastFrameMs < OP_FRAME_MS)
| |||
{
| |||
m_dirty = true;
| |||
return;
| |||
}
| |||
//--- Stamp this frame and paint it
| |||
m_lastFrameMs = now;
| |||
m_dirty = false;
| |||
Render();
| |||
ChartRedraw(m_chartId);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Build the panel and load the model |
| |||
//+------------------------------------------------------------------+
| |||
bool COmvPanel::Init(const long chartId, const string path)
| |||
{
| |||
//--- Reset every member before touching the chart
| |||
InitDefaults();
| |||
m_chartId = chartId;
| |||
//--- Take the header title from the running program
| |||
m_title = MQLInfoString(MQL_PROGRAM_NAME);
| |||
if(StringLen(m_title) == 0)
| |||
m_title = "ONNX Model Viewer";
| |||
//--- Name the bitmap after the program so parts never clash
| |||
m_name = "OMV_" + m_title;
| |||
//--- Resolve the starting theme
| |||
m_th.Apply(m_dark);
| |||
//--- Read the model before anything is drawn
| |||
if(!m_model.Load(path))
| |||
{
| |||
Print(m_title, ": ", m_model.Error());
| |||
return false;
| |||
}
| |||
//--- Arrange what was read
| |||
m_layout.Build(m_model);
| |||
//--- Work out whether this model can also be shown as neurons
| |||
m_cells.Build(m_model);
| |||
//--- Place every control and build the bitmap
| |||
ComputeLayout();
| |||
if(!CreateCanvas())
| |||
return false;
| |||
//--- Scale the graph to the canvas it now has
| |||
FitGraph();
| |||
//--- Remember how the chart was set before the panel changes it
| |||
m_hadMouse = (bool)ChartGetInteger(m_chartId, CHART_EVENT_MOUSE_MOVE);
| |||
m_hadWheel = (bool)ChartGetInteger(m_chartId, CHART_EVENT_MOUSE_WHEEL);
| |||
//--- Follow the pointer so the panel can be worked
| |||
ChartSetInteger(m_chartId, CHART_EVENT_MOUSE_MOVE, true);
| |||
ChartSetInteger(m_chartId, CHART_EVENT_MOUSE_WHEEL, true);
| |||
//--- Show the first frame
| |||
Render();
| |||
ChartRedraw(m_chartId);
| |||
//--- Report what was loaded
| |||
Print(m_title, ": ", path, " -> ", m_model.Nodes(), " nodes, ",
| |||
m_model.Tensors(), " tensors, ", m_model.Parameters(), " parameters");
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Remove the canvas and restore the chart |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::Destroy(void)
| |||
{
| |||
//--- Run the teardown only once
| |||
if(m_destroyed)
| |||
return;
| |||
m_destroyed = true;
| |||
//--- Give scrolling back to the chart
| |||
LockChartScroll(false);
| |||
//--- Put the chart's own mouse settings back the way they were found
| |||
ChartSetInteger(m_chartId, CHART_EVENT_MOUSE_MOVE, m_hadMouse);
| |||
ChartSetInteger(m_chartId, CHART_EVENT_MOUSE_WHEEL, m_hadWheel);
| |||
//--- Clear any tooltip left on the object
| |||
if(m_created)
| |||
ObjectSetString(m_chartId, m_name, OBJPROP_TOOLTIP, "\n");
| |||
//--- Release the bitmap
| |||
if(m_created)
| |||
{
| |||
m_cv.Destroy();
| |||
m_created = false;
| |||
}
| |||
//--- Release the chrome cache
| |||
ArrayFree(m_bg);
| |||
ArrayFree(m_scene);
| |||
m_bgValid = false;
| |||
m_sceneValid = false;
| |||
//--- Release the parsed structures
| |||
m_layout.Clear();
| |||
m_cells.Clear();
| |||
m_model.Clear();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Route pointer events into the panel |
| |||
//+------------------------------------------------------------------+
| |||
void COmvPanel::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
| |||
{
| |||
//--- Ignore events after teardown or before the canvas exists
| |||
if(m_destroyed || !m_created)
| |||
return;
| |||
//--- Route a wheel turn before anything else looks at the pointer
| |||
if(id == CHARTEVENT_MOUSE_WHEEL)
| |||
{
| |||
//--- The position packs into the low and high halves of lparam
| |||
const int wx = (int)(short)(lparam & 0xFFFF) - m_x;
| |||
const int wy = (int)(short)((lparam >> 16) & 0xFFFF) - m_y;
| |||
//--- The terminal hands the wheel delta through dparam unpacked
| |||
OnWheel(wx, wy, (int)dparam);
| |||
return;
| |||
}
| |||
//--- Handle pointer events only
| |||
if(id != CHARTEVENT_MOUSE_MOVE)
| |||
return;
| |||
//--- Convert the pointer into panel space
| |||
const int mx = (int)lparam - m_x;
| |||
const int my = (int)dparam - m_y;
| |||
const bool down = ((int)StringToInteger(sparam) & 1) == 1;
| |||
//--- Hold the scroll lock for the whole gesture
| |||
const bool over = (mx >= 0 && mx < m_w && my >= 0 && my < m_h);
| |||
LockChartScroll(over || m_dragging || m_resizing || m_panning || m_barDrag != OH_NONE);
| |||
//--- Identify what sits under the pointer
| |||
const int hit = over ? HitTest(mx, my) : OH_NONE;
| |||
//--- Move the panel while a header drag is active
| |||
if(m_dragging)
| |||
{
| |||
//--- End the drag on release
| |||
if(!down)
| |||
{
| |||
m_dragging = false;
| |||
m_lmbPrev = false;
| |||
m_press = OH_NONE;
| |||
LockChartScroll(over);
| |||
return;
| |||
}
| |||
//--- Follow the pointer by the offset captured at press
| |||
m_x = (int)lparam - m_dragOffX;
| |||
m_y = (int)dparam - m_dragOffY;
| |||
ObjectSetInteger(m_chartId, m_name, OBJPROP_XDISTANCE, m_x);
| |||
ObjectSetInteger(m_chartId, m_name, OBJPROP_YDISTANCE, m_y);
| |||
ChartRedraw(m_chartId);
| |||
return;
| |||
}
| |||
//--- Resize the panel while a grip drag is active
| |||
if(m_resizing)
| |||
{
| |||
//--- End the resize on release
| |||
if(!down)
| |||
{
| |||
m_resizing = false;
| |||
m_lmbPrev = false;
| |||
m_press = OH_NONE;
| |||
LockChartScroll(over);
| |||
//--- Land whatever frame the cap skipped
| |||
m_lastFrameMs = 0;
| |||
PaintCapped();
| |||
return;
| |||
}
| |||
//--- Take the size the pointer implies through the captured offset
| |||
int nw = (int)lparam - m_x + m_gripOffX;
| |||
int nh = (int)dparam - m_y + m_gripOffY;
| |||
//--- Clamp both sides to the allowed bounds
| |||
if(nw < OP_W_MIN)
| |||
nw = OP_W_MIN;
| |||
if(nw > OP_W_MAX)
| |||
nw = OP_W_MAX;
| |||
if(nh < OP_H_MIN)
| |||
nh = OP_H_MIN;
| |||
if(nh > OP_H_MAX)
| |||
nh = OP_H_MAX;
| |||
//--- Skip everything when the size did not actually change
| |||
if(nw == m_w && nh == m_h)
| |||
return;
| |||
//--- Take the new size at once, the bitmap behind it never changes
| |||
m_w = nw;
| |||
m_h = nh;
| |||
m_fullH = nh;
| |||
m_bgValid = false;
| |||
ComputeLayout();
| |||
ClampPan();
| |||
//--- Paint through the cap so dragging stays responsive
| |||
PaintCapped();
| |||
return;
| |||
}
| |||
//--- Drag the vertical scrollbar while its thumb is held
| |||
if(m_barDrag == OH_BAR_V)
| |||
{
| |||
//--- End the drag on release
| |||
if(!down)
| |||
{
| |||
m_barDrag = OH_NONE;
| |||
m_lmbPrev = false;
| |||
m_press = OH_NONE;
| |||
LockChartScroll(over);
| |||
//--- Land whatever frame the cap skipped
| |||
m_lastFrameMs = 0;
| |||
PaintCapped();
| |||
return;
| |||
}
| |||
//--- Convert the pointer travel into graph travel
| |||
const int viewH = ViewH(), fullH = ScaledH();
| |||
const int span = fullH - viewH + OP_PAD * 2;
| |||
if(span > 0 && m_rBarV[3] > 0 && viewH > m_rBarV[3])
| |||
{
| |||
//--- Move the graph by the same fraction the thumb travelled
| |||
m_panY = m_barFrom - (int)((double)(my - m_fromY) * span / (viewH - m_rBarV[3]));
| |||
ClampPan();
| |||
PaintCapped();
| |||
}
| |||
return;
| |||
}
| |||
//--- Drag the horizontal scrollbar while its thumb is held
| |||
if(m_barDrag == OH_BAR_H)
| |||
{
| |||
//--- End the drag on release
| |||
if(!down)
| |||
{
| |||
m_barDrag = OH_NONE;
| |||
m_lmbPrev = false;
| |||
m_press = OH_NONE;
| |||
LockChartScroll(over);
| |||
//--- Land whatever frame the cap skipped
| |||
m_lastFrameMs = 0;
| |||
PaintCapped();
| |||
return;
| |||
}
| |||
//--- Convert the pointer travel into graph travel
| |||
const int viewW = ViewW(), fullW = ScaledW();
| |||
const int span = fullW - viewW + OP_PAD * 2;
| |||
if(span > 0 && m_rBarH[2] > 0 && viewW > m_rBarH[2])
| |||
{
| |||
//--- Move the graph by the same fraction the thumb travelled
| |||
m_panX = m_barFrom - (int)((double)(mx - m_fromX) * span / (viewW - m_rBarH[2]));
| |||
ClampPan();
| |||
PaintCapped();
| |||
}
| |||
return;
| |||
}
| |||
//--- Scroll the inspector while its thumb is held
| |||
if(m_barDrag == OH_INFO_BAR)
| |||
{
| |||
//--- End the drag on release
| |||
if(!down)
| |||
{
| |||
m_barDrag = OH_NONE;
| |||
m_lmbPrev = false;
| |||
m_press = OH_NONE;
| |||
LockChartScroll(over);
| |||
return;
| |||
}
| |||
//--- Move the listing by the same fraction the thumb travelled
| |||
const int band = (CanvasBottom() - 8) - (CanvasTop() + 44);
| |||
if(m_infoHigh > band && m_rInfoBar[3] > 0 && band > m_rInfoBar[3])
| |||
{
| |||
//--- Scale the movement by how much taller the listing is
| |||
m_infoScroll = m_barFrom +
| |||
(int)((double)(my - m_fromY) * (m_infoHigh - band) / (band - m_rInfoBar[3]));
| |||
PaintCapped();
| |||
}
| |||
return;
| |||
}
| |||
//--- Pan the graph while a canvas drag is active
| |||
if(m_panning)
| |||
{
| |||
//--- End the pan on release
| |||
if(!down)
| |||
{
| |||
m_panning = false;
| |||
m_lmbPrev = false;
| |||
m_press = OH_NONE;
| |||
LockChartScroll(over);
| |||
//--- A press that never moved is a click on the background
| |||
if(MathAbs(mx - m_pressX) < 3 && MathAbs(my - m_pressY) < 3)
| |||
{
| |||
m_pickKind = OP_PICK_MODEL;
| |||
m_selected = -1;
| |||
m_inspect = true;
| |||
ComputeLayout();
| |||
ClampPan();
| |||
}
| |||
//--- Land whatever frame the cap skipped
| |||
m_lastFrameMs = 0;
| |||
PaintCapped();
| |||
return;
| |||
}
| |||
//--- Shift the graph by however far the pointer moved
| |||
m_panX += mx - m_fromX;
| |||
m_panY += my - m_fromY;
| |||
m_fromX = mx;
| |||
m_fromY = my;
| |||
ClampPan();
| |||
PaintCapped();
| |||
return;
| |||
}
| |||
//--- Track whatever sits under the pointer so it can be highlighted
| |||
const int overNode = (hit == OH_NODE) ? NodeAtPoint(mx, my) : -1;
| |||
const int overRow = (hit == OH_ROW) ? RowAtPoint(mx, my) : -1;
| |||
//--- Take the value a hovered box row lists, so its sidebar row lights too
| |||
string overRowName = "";
| |||
if(overRow >= 0 && overRow < m_rows)
| |||
overRowName = TensorNameAt(m_rowTensor[overRow]);
| |||
const int overLink = (hit == OH_LINK) ? LinkAtPoint(mx, my) : -1;
| |||
//--- Take the value a hovered connection carries, so its siblings light too
| |||
string overEdgeName = "";
| |||
if(overLink >= 0 && overLink < m_layout.Edges())
| |||
{
| |||
//--- Fetch the connection under the pointer
| |||
OmvEdge lit;
| |||
m_layout.EdgeAt(overLink, lit);
| |||
overEdgeName = lit.label;
| |||
}
| |||
const int overInfo = (hit == OH_INFO) ? InfoAtPoint(mx, my) : -1;
| |||
//--- Take the value the hovered row points at, and where on the row we are
| |||
string overName = (overInfo >= 0) ? m_info[overInfo].linkName : "";
| |||
//--- A hovered connection lights by name just as a hovered row does
| |||
if(StringLen(overName) == 0)
| |||
overName = overEdgeName;
| |||
//--- So does a hovered weight row inside a box
| |||
if(StringLen(overName) == 0)
| |||
overName = overRowName;
| |||
const bool overPlus = (overInfo >= 0) && OnExpander(mx, overInfo);
| |||
//--- Repaint on any hover change, capped to one frame
| |||
if(hit != m_hover || overNode != m_hoverNode ||
| |||
overRow != m_hoverRow || overLink != m_hoverLink || overInfo != m_infoHover ||
| |||
overName != m_litName || overPlus != m_onExpander)
| |||
{
| |||
m_hover = hit;
| |||
m_hoverNode = overNode;
| |||
m_hoverRow = overRow;
| |||
m_hoverLink = overLink;
| |||
m_infoHover = overInfo;
| |||
m_litName = overName;
| |||
m_onExpander = overPlus;
| |||
//--- Let the terminal draw the tooltip for whatever is hovered
| |||
SetTooltip(TipFor(hit));
| |||
PaintCapped();
| |||
}
| |||
//--- React to the press edge
| |||
if(down && !m_lmbPrev)
| |||
{
| |||
//--- Remember where the press began
| |||
m_press = hit;
| |||
//--- Start a header drag
| |||
if(hit == OH_HEADER)
| |||
{
| |||
m_dragging = true;
| |||
m_dragOffX = (int)lparam - m_x;
| |||
m_dragOffY = (int)dparam - m_y;
| |||
}
| |||
//--- Start a grip drag, keeping the grip under the pointer
| |||
else
| |||
if(hit == OH_GRIP)
| |||
{
| |||
m_resizing = true;
| |||
m_gripOffX = (m_x + m_w) - (int)lparam;
| |||
m_gripOffY = (m_y + m_h) - (int)dparam;
| |||
}
| |||
//--- Start a scrollbar drag from where the thumb sits now
| |||
else
| |||
if(hit == OH_BAR_V || hit == OH_BAR_H)
| |||
{
| |||
m_barDrag = hit;
| |||
m_fromX = mx;
| |||
m_fromY = my;
| |||
m_barFrom = (hit == OH_BAR_V) ? m_panY : m_panX;
| |||
}
| |||
//--- Start an inspector scroll from where its thumb sits now
| |||
else
| |||
if(hit == OH_INFO_BAR)
| |||
{
| |||
m_barDrag = OH_INFO_BAR;
| |||
m_fromY = my;
| |||
m_barFrom = m_infoScroll;
| |||
}
| |||
//--- Start a canvas pan, remembering where it began
| |||
else
| |||
if(hit == OH_CANVAS)
| |||
{
| |||
m_panning = true;
| |||
m_fromX = mx;
| |||
m_fromY = my;
| |||
m_pressX = mx;
| |||
m_pressY = my;
| |||
}
| |||
//--- Select the box that was pressed and open the inspector for it
| |||
else
| |||
if(hit == OH_NODE)
| |||
{
| |||
m_selected = NodeAtPoint(mx, my);
| |||
//--- A graph port belongs to the graph, so it opens the graph panel
| |||
OmvBox picked;
| |||
m_layout.BoxAt(m_selected, picked);
| |||
m_pickKind = (picked.node < 0) ? OP_PICK_GRAPH : OP_PICK_NODE;
| |||
m_inspect = true;
| |||
ComputeLayout();
| |||
ClampPan();
| |||
PaintCapped();
| |||
}
| |||
//--- Select the weight row that was pressed
| |||
else
| |||
if(hit == OH_ROW)
| |||
{
| |||
//--- Take the tensor the row names
| |||
const int row = RowAtPoint(mx, my);
| |||
if(row >= 0)
| |||
{
| |||
m_pickTensor = m_rowTensor[row];
| |||
m_pickKind = OP_PICK_TENSOR;
| |||
m_selected = -1;
| |||
m_inspect = true;
| |||
ComputeLayout();
| |||
ClampPan();
| |||
}
| |||
PaintCapped();
| |||
}
| |||
//--- Select the connection that was pressed
| |||
else
| |||
if(hit == OH_LINK)
| |||
{
| |||
m_pickLink = LinkAtPoint(mx, my);
| |||
m_pickKind = OP_PICK_LINK;
| |||
m_selected = -1;
| |||
m_inspect = true;
| |||
ComputeLayout();
| |||
ClampPan();
| |||
PaintCapped();
| |||
}
| |||
//--- Light the pressed control
| |||
else
| |||
if(hit != OH_NONE)
| |||
PaintCapped();
| |||
}
| |||
//--- React to the release edge and run the action
| |||
else
| |||
if(!down && m_lmbPrev)
| |||
{
| |||
//--- Act only when the release lands where the press began
| |||
const int act = (m_press == hit) ? hit : OH_NONE;
| |||
m_press = OH_NONE;
| |||
//--- Swap the theme
| |||
if(act == OH_THEME)
| |||
{
| |||
m_dark = !m_dark;
| |||
m_th.Apply(m_dark);
| |||
m_bgValid = false;
| |||
}
| |||
//--- Swap the view, but only where the other one can be drawn
| |||
else
| |||
if(act == OH_VIEW)
| |||
{
| |||
//--- Refuse to leave the graph when the model has no neurons to show
| |||
if(m_neuronView || m_cells.Ready())
| |||
{
| |||
m_neuronView = !m_neuronView;
| |||
//--- The neuron view has nothing selectable, so drop what the graph chose
| |||
if(m_neuronView)
| |||
{
| |||
m_selected = -1;
| |||
m_pickKind = OP_PICK_NONE;
| |||
m_hoverNode = -1;
| |||
m_hoverRow = -1;
| |||
m_hoverLink = -1;
| |||
m_litName = "";
| |||
}
| |||
//--- The two arrangements differ in size, so refit rather than keep the pan
| |||
FitGraph();
| |||
m_bgValid = false;
| |||
}
| |||
}
| |||
//--- Scale the graph back into the canvas
| |||
else
| |||
if(act == OH_FIT)
| |||
FitGraph();
| |||
//--- Step the zoom down
| |||
else
| |||
if(act == OH_ZOOM_OUT)
| |||
SetZoom(m_zoom / 1.25);
| |||
//--- Step the zoom up
| |||
else
| |||
if(act == OH_ZOOM_IN)
| |||
SetZoom(m_zoom * 1.25);
| |||
//--- Act on the row that was pressed
| |||
else
| |||
if(act == OH_INFO)
| |||
{
| |||
//--- Take the row and decide which half of it was hit
| |||
const int row = InfoAtPoint(mx, my);
| |||
if(row >= 0 && m_info[row].canOpen && OnExpander(mx, row))
| |||
ToggleRow(m_info[row].name + "|" + m_info[row].value);
| |||
else
| |||
if(row >= 0 && StringLen(m_info[row].linkName) > 0)
| |||
{
| |||
//--- Open the connection the row points at
| |||
const int link = LinkNamed(m_info[row].linkName);
| |||
if(link >= 0)
| |||
{
| |||
m_pickLink = link;
| |||
m_pickKind = OP_PICK_LINK;
| |||
m_selected = -1;
| |||
m_litName = "";
| |||
}
| |||
}
| |||
else
| |||
if(row >= 0 && m_info[row].canOpen)
| |||
ToggleRow(m_info[row].name + "|" + m_info[row].value);
| |||
}
| |||
//--- Close the inspector and give its width back to the graph
| |||
else
| |||
if(act == OH_INSPECT_X)
| |||
{
| |||
m_inspect = false;
| |||
m_selected = -1;
| |||
m_pickKind = OP_PICK_NONE;
| |||
m_pickTensor = -1;
| |||
m_pickLink = -1;
| |||
ComputeLayout();
| |||
ClampPan();
| |||
}
| |||
//--- Collapse or restore the panel
| |||
else
| |||
if(act == OH_MIN || act == OH_RESTORE)
| |||
{
| |||
m_mode = (act == OH_MIN) ? OM_MIN : OM_FULL;
| |||
m_hover = OH_NONE;
| |||
ApplyMode();
| |||
}
| |||
//--- Remove the panel
| |||
else
| |||
if(act == OH_CLOSE)
| |||
{
| |||
Destroy();
| |||
ChartRedraw(m_chartId);
| |||
return;
| |||
}
| |||
//--- Repaint whenever an action ran
| |||
if(act != OH_NONE)
| |||
{
| |||
m_lastFrameMs = 0;
| |||
PaintCapped();
| |||
}
| |||
}
| |||
//--- Land a frame the cap skipped once enough time has passed
| |||
if(m_dirty && GetTickCount() - m_lastFrameMs >= OP_FRAME_MS)
| |||
PaintCapped();
| |||
//--- Remember the button state for the next event
| |||
m_lmbPrev = down;
| |||
}
| |||
| |||
#endif // OMV_PANEL_MQH
| |||
//+------------------------------------------------------------------+
|