Skip to content
Snippets Groups Projects
group.rs 32.1 KiB
Newer Older
Udo Eisenbarth's avatar
Udo Eisenbarth committed
#![warn(missing_docs)]
use crate::analyzer::AnalyzerType;
use crate::dottable::Dottable;
use crate::error::OpmResult;
Udo Eisenbarth's avatar
Udo Eisenbarth committed
use crate::error::OpossumError;
Udo Eisenbarth's avatar
Udo Eisenbarth committed
use crate::light::Light;
use crate::lightdata::LightData;
use crate::optic_graph::OpticGraph;
use crate::optical::LightResult;
use crate::properties::{Properties, Property, Proptype};
use crate::{optic_ports::OpticPorts, optical::Optical};
use petgraph::prelude::NodeIndex;
use petgraph::visit::EdgeRef;
Udo Eisenbarth's avatar
Udo Eisenbarth committed
use petgraph::{algo::*, Direction};
use serde::Serialize;
use std::collections::HashMap;

/// Mappin of group internal ports to externally visble ports.
pub type PortMap = HashMap<String, (NodeIndex, String)>;
#[derive(Debug, Clone)]
/// A node that represents a group of other [`Optical`]s arranges in a subgraph.
Udo Eisenbarth's avatar
Udo Eisenbarth committed
/// All unconnected input and output ports of this subgraph could be used as ports of
/// this [`NodeGroup`]. For this, port mapping is neccessary (see below).
///
/// ## Optical Ports
///   - Inputs
///     - defined by [`map_input_port`](NodeGroup::map_input_port()) function.
///   - Outputs
///     - defined by [`map_output_port`](NodeGroup::map_output_port()) function.
Udo Eisenbarth's avatar
Udo Eisenbarth committed
///
/// ## Properties
///   - `name`
///   - `inverted`
///   - `expand view`
///   - `graph`
///   - `input port map`
///   - `output port map`
Udo Eisenbarth's avatar
Udo Eisenbarth committed
pub struct NodeGroup {
    g: OpticGraph,
fn create_default_props() -> Properties {
    let mut props = Properties::default();
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    props.set("name", "group".into());
    props.set("inverted", false.into());
    props.set("expand view", false.into());
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    props.set("graph", OpticGraph::default().into());
    props.set("input port map", PortMap::new().into());
    props.set("output port map", PortMap::new().into());
impl Default for NodeGroup {
    fn default() -> Self {
        Self {
            g: Default::default(),
            props: create_default_props(),
        }
    }
}
Udo Eisenbarth's avatar
Udo Eisenbarth committed
impl NodeGroup {
y.zobus's avatar
y.zobus committed
    /// Creates a new [`NodeGroup`].
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    pub fn new(name: &str) -> Self {
        let mut props = create_default_props();
        props.set("name", name.into());
y.zobus's avatar
y.zobus committed
            ..Default::default()
        }
    /// Add a given [`Optical`] to the (sub-)graph of this [`NodeGroup`].
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    ///
    /// This command just adds an [`Optical`] but does not connect it to existing nodes in the (sub-)graph. The given node is
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    /// consumed (owned) by the [`NodeGroup`].
    pub fn add_node<T: Optical + 'static>(&mut self, node: T) -> NodeIndex {
        let idx = self.g.add_node(node);
        self.props.set("graph", self.g.clone().into());
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    }
    /// Connect (already existing) nodes denoted by the respective `NodeIndex`.
    ///
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    /// Both node indices must exist. Otherwise an [`OpossumError::OpticScenery`] is returned. In addition, connections are
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    /// rejected and an [`OpossumError::OpticScenery`] is returned, if the graph would form a cycle (loop in the graph). **Note**:
    /// The connection of two internal nodes might affect external port mappings (see [`map_input_port`](NodeGroup::map_input_port())
    /// & [`map_output_port`](NodeGroup::map_output_port()) functions). In this case no longer valid mappings will be deleted.
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    pub fn connect_nodes(
        &mut self,
        src_node: NodeIndex,
        src_port: &str,
        target_node: NodeIndex,
        target_port: &str,
    ) -> OpmResult<()> {
        self.g
            .connect_nodes(src_node, src_port, target_node, target_port)?;
        self.props.set("graph", self.g.clone().into());

        let in_map = self.input_port_map();
        let invalid_mapping = in_map
            .iter()
            .find(|m| m.1 .0 == target_node && m.1 .1 == target_port);
        let mut in_map = self.input_port_map();
        if let Some(input) = invalid_mapping {
            in_map.remove(input.0);
            self.set_input_port_map(in_map);
        let out_map = self.output_port_map();
        let invalid_mapping = out_map
            .iter()
            .find(|m| m.1 .0 == src_node && m.1 .1 == src_port);
        let mut out_map = self.output_port_map();
        if let Some(input) = invalid_mapping {
            out_map.remove(input.0);
            self.set_output_port_map(out_map);
    fn input_port_map(&self) -> PortMap {
        let input_port_map = self.props.get("input port map").unwrap().prop.clone();
        if let Proptype::GroupPortMap(input_port_map) = input_port_map {
            input_port_map
        } else {
            panic!("wrong data type")
        }
    }
    fn set_input_port_map(&mut self, port_map: PortMap) {
        self.props.set("input port map", port_map.into());
    }
    fn output_port_map(&self) -> PortMap {
        let output_port_map = self.props.get("output port map").unwrap().prop.clone();
        if let Proptype::GroupPortMap(output_port_map) = output_port_map {
            output_port_map
        } else {
            panic!("wrong data type")
        }
    }
    fn set_output_port_map(&mut self, port_map: PortMap) {
        self.props.set("output port map", port_map.into());
    }
    fn input_nodes(&self) -> Vec<NodeIndex> {
        let mut input_nodes: Vec<NodeIndex> = Vec::default();
        for node_idx in self.g.0.node_indices() {
            let incoming_edges = self
                .g
                .0
                .edges_directed(node_idx, Direction::Incoming)
                .count();
            let input_ports = self
                .node_weight(node_idx)
Udo Eisenbarth's avatar
Udo Eisenbarth committed
                .optical_ref
                .borrow()
                .ports()
                .inputs()
                .len();
            if input_ports != incoming_edges {
                input_nodes.push(node_idx);
            }
        }
        input_nodes
    }
    fn output_nodes(&self) -> Vec<NodeIndex> {
        let mut output_nodes: Vec<NodeIndex> = Vec::default();
        for node_idx in self.g.0.node_indices() {
            let outgoing_edges = self
                .g
                .0
                .edges_directed(node_idx, Direction::Outgoing)
                .count();
            let output_ports = self
                .node_weight(node_idx)
Udo Eisenbarth's avatar
Udo Eisenbarth committed
                .optical_ref
                .borrow()
                .ports()
                .outputs()
                .len();
            if output_ports != outgoing_edges {
                output_nodes.push(node_idx);
            }
        }
        output_nodes
    }
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    /// Map an input port of an internal node to an external port of the group.
    ///
    /// In oder to use a [`NodeGroup`] from the outside, internal nodes / ports must be mapped to be visible. The
    /// corresponding [`ports`](NodeGroup::ports()) function only returns ports that have been mapped before.
    /// # Errors
    ///
    /// This function will return an error if
    ///   - an external input port name has already been assigned.
    ///   - the `input_node` / `internal_name` does not exist.
    ///   - the specified `input_node` is not an input node of the group (i.e. fully connected to other internal nodes).
    ///   - the `input_node` has an input port with the specified `internal_name` but is already internally connected.
    pub fn map_input_port(
        &mut self,
        input_node: NodeIndex,
        internal_name: &str,
        external_name: &str,
    ) -> OpmResult<()> {
        if self.input_port_map().contains_key(external_name) {
            return Err(OpossumError::OpticGroup(
                "external input port name already assigned".into(),
            ));
        }
        let node = self
            .g
            .0
            .node_weight(input_node)
            .ok_or(OpossumError::OpticGroup(
                "internal node index not found".into(),
Udo Eisenbarth's avatar
Udo Eisenbarth committed
            .optical_ref
            .borrow()
            .ports()
            .inputs()
            .contains(&(internal_name.to_string()))
        {
            return Err(OpossumError::OpticGroup(
                "internal input port name not found".into(),
        if !self.input_nodes().contains(&input_node) {
            return Err(OpossumError::OpticGroup(
                "node to be mapped is not an input node of the group".into(),
            ));
        }
        let incoming_edge_connected = self
            .edges_directed(input_node, Direction::Incoming)
            .map(|e| e.weight().target_port())
            .any(|p| p == internal_name);
        if incoming_edge_connected {
            return Err(OpossumError::OpticGroup(
                "port of input node is already internally connected".into(),
            ));
        }
        let mut input_port_map = self.input_port_map();
        input_port_map.insert(
            external_name.to_string(),
            (input_node, internal_name.to_string()),
        );
        self.set_input_port_map(input_port_map);
y.zobus's avatar
y.zobus committed

Udo Eisenbarth's avatar
Udo Eisenbarth committed
    /// Map an output port of an internal node to an external port of the group.
    ///
    /// In oder to use a [`NodeGroup`] from the outside, internal nodes / ports must be mapped to be visible. The
    /// corresponding [`ports`](NodeGroup::ports()) function only returns ports that have been mapped before.
    /// # Errors
    ///
    /// This function will return an error if
    ///   - an external output port name has already been assigned.
    ///   - the `output_node` / `internal_name` does not exist.
    ///   - the specified `output_node` is not an output node of the group (i.e. fully connected to other internal nodes).
    ///   - the `output_node` has an output port with the specified `internal_name` but is already internally connected.
    pub fn map_output_port(
        &mut self,
        output_node: NodeIndex,
        internal_name: &str,
        external_name: &str,
    ) -> OpmResult<()> {
        if self.output_port_map().contains_key(external_name) {
            return Err(OpossumError::OpticGroup(
                "external output port name already assigned".into(),
            ));
        }
        let node = self
            .g
            .0
            .node_weight(output_node)
            .ok_or(OpossumError::OpticGroup(
                "internal node index not found".into(),
Udo Eisenbarth's avatar
Udo Eisenbarth committed
            .optical_ref
            .borrow()
            .ports()
            .outputs()
            .contains(&(internal_name.to_string()))
        {
            return Err(OpossumError::OpticGroup(
                "internal output port name not found".into(),
        if !self.output_nodes().contains(&output_node) {
            return Err(OpossumError::OpticGroup(
                "node to be mapped is not an output node of the group".into(),
            ));
        }
        let outgoing_edge_connected = self
            .edges_directed(output_node, Direction::Outgoing)
            .map(|e| e.weight().src_port())
            .any(|p| p == internal_name);
        if outgoing_edge_connected {
            return Err(OpossumError::OpticGroup(
                "port of output node is already internally connected".into(),
            ));
        }
        let mut out_map = self.output_port_map();
        out_map.insert(
            external_name.to_string(),
            (output_node, internal_name.to_string()),
        );
        self.set_output_port_map(out_map);
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    fn incoming_edges(&self, idx: NodeIndex) -> LightResult {
        let edges = self.g.0.edges_directed(idx, Direction::Incoming);
        edges
            .into_iter()
            .map(|e| {
                (
                    e.weight().target_port().to_owned(),
                    e.weight().data().cloned(),
                )
            })
            .collect::<HashMap<String, Option<LightData>>>()
    }
    fn set_outgoing_edge_data(&mut self, idx: NodeIndex, port: String, data: Option<LightData>) {
        let edges = self.g.0.edges_directed(idx, Direction::Outgoing);
        let edge_ref = edges
            .into_iter()
            .filter(|idx| idx.weight().src_port() == port)
            .last();
        if let Some(edge_ref) = edge_ref {
            let edge_idx = edge_ref.id();
            let light = self.g.0.edge_weight_mut(edge_idx);
            if let Some(light) = light {
                light.set_data(data);
            }
        } // else outgoing edge not connected -> data dropped
    }
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    fn analyze_group(
        &mut self,
        incoming_data: LightResult,
        analyzer_type: &AnalyzerType,
    ) -> OpmResult<LightResult> {
        let is_inverted = self.props.get_bool("inverted").unwrap().unwrap();
        if is_inverted {
            self.invert_graph();
        }
        let g_clone = self.g.0.clone();
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut group_srcs = g_clone.externals(Direction::Incoming);
        let mut light_result = LightResult::default();
        let sorted = toposort(&self.g.0, None)
            .map_err(|_| OpossumError::Analysis("topological sort failed".into()))?;
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        for idx in sorted {
            // Check if node is group src node
            let incoming_edges = if group_srcs.any(|gs| gs == idx) {
                // get from incoming_data
                let portmap = if is_inverted {
                    self.output_port_map()
                    self.input_port_map()
                let assigned_ports = portmap.iter().filter(|p| p.1 .0 == idx);
Udo Eisenbarth's avatar
Udo Eisenbarth committed
                let mut incoming = LightResult::default();
                for port in assigned_ports {
                    incoming.insert(
                        port.1 .1.to_owned(),
                        incoming_data.get(port.0).unwrap().clone(),
                    );
                }
                incoming
            } else {
                self.incoming_edges(idx)
            };
            let node = g_clone.node_weight(idx).unwrap();
Udo Eisenbarth's avatar
Udo Eisenbarth committed
            let outgoing_edges: HashMap<String, Option<LightData>> = node
                .optical_ref
                .borrow_mut()
                .analyze(incoming_edges, analyzer_type)?;
            let mut group_sinks = g_clone.externals(Direction::Outgoing);
Udo Eisenbarth's avatar
Udo Eisenbarth committed
            // Check if node is group sink node
            if group_sinks.any(|gs| gs == idx) {
                let portmap = if is_inverted {
                    self.input_port_map()
                    self.output_port_map()
                let assigned_ports = portmap.iter().filter(|p| p.1 .0 == idx);
Udo Eisenbarth's avatar
Udo Eisenbarth committed
                for port in assigned_ports {
                    light_result.insert(
                        port.0.to_owned(),
                        outgoing_edges.get(&port.1 .1).unwrap().clone(),
                    );
                }
            } else {
                for outgoing_edge in outgoing_edges {
                    self.set_outgoing_edge_data(idx, outgoing_edge.0, outgoing_edge.1)
                }
            }
        }
            self.invert_graph();
        } // revert initial inversion (if necessary)
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        Ok(light_result)
y.zobus's avatar
y.zobus committed

    /// Sets the expansion flag of this [`NodeGroup`].  
    /// If true, the group expands and the internal nodes of this group are displayed in the dot format.
    /// If false, only the group node itself is displayed and the internal setup is not shown
    pub fn shall_expand(&self) -> bool {
        self.props.get_bool("expand view").unwrap().unwrap()
    /// Defines and returns the node/port identifier to connect the edges in the dot format
    /// parameters:
    /// - port_name:            name of the external port of the group
    /// - parent_identifier:    String that contains the hierarchical structure: parentidx_childidx_childofchildidx ...
    /// Error, if the port is not mapped as input or output
    pub fn get_mapped_port_str(
        &self,
        port_name: &str,
        parent_identifier: String,
    ) -> OpmResult<String> {
y.zobus's avatar
y.zobus committed
        if self.shall_expand() {
            if self.input_port_map().contains_key(port_name) {
                let input_port_map = self.input_port_map();
                let port = input_port_map.get(port_name).unwrap();
y.zobus's avatar
y.zobus committed

                Ok(format!(
                    "{}_i{}:{}",
                    parent_identifier,
                    port.0.index(),
                    port.1
                ))
            } else if self.output_port_map().contains_key(port_name) {
                let output_port_map = self.output_port_map();
                let port = output_port_map.get(port_name).unwrap();
y.zobus's avatar
y.zobus committed

                Ok(format!(
                    "{}_i{}:{}",
                    parent_identifier,
                    port.0.index(),
                    port.1
                ))
            } else {
                Err(OpossumError::OpticGroup(format!(
                    "port {} is not mapped",
                    port_name
                )))
y.zobus's avatar
y.zobus committed
            }
        } else {
y.zobus's avatar
y.zobus committed
            Ok(format!("{}:{}", parent_identifier, port_name))
        }
    }

    /// returns the boolean which defines whether the group expands or not.
    pub fn expand_view(&mut self, expand_view: bool) {
        self.props.set("expand view", expand_view.into());
y.zobus's avatar
y.zobus committed
    }
    /// Creates the dot-format string which describes the edge that connects two nodes
    /// parameters:
    /// - end_node_idx:         NodeIndex of the node that should be connected
    /// - light_port:           port name that should be connected
    /// - parent_identifier:    String that contains the hierarchical structure: parentidx_childidx_childofchildidx ...
    /// Returns the result of the edge strnig for the dot format
    fn create_node_edge_str(
        &self,
        end_node_idx: NodeIndex,
        light_port: &str,
        mut parent_identifier: String,
    ) -> OpmResult<String> {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let node = self
            .g
            .0
            .node_weight(end_node_idx)
            .unwrap()
            .optical_ref
            .borrow();
y.zobus's avatar
y.zobus committed

Udo Eisenbarth's avatar
Udo Eisenbarth committed
        parent_identifier = if parent_identifier.is_empty() {
            format!("i{}", end_node_idx.index())
        } else {
            format!("{}_i{}", &parent_identifier, end_node_idx.index())
        };
y.zobus's avatar
y.zobus committed

        if node.node_type() == "group" {
            let group_node: &NodeGroup = node.as_group()?;
            Ok(group_node.get_mapped_port_str(light_port, parent_identifier)?)
        } else {
y.zobus's avatar
y.zobus committed
            Ok(format!("{}:{}", parent_identifier, light_port))
y.zobus's avatar
y.zobus committed
    }
    /// creates the dot format of the group node in its expanded view
    /// parameters:
    /// - node_index:           NodeIndex of the group
    /// - name:                 name of the node
    /// - inverted:             boolean that descries wether the node is inverted or not
    /// - parent_identifier:    String that contains the hierarchical structure: parentidx_childidx_childofchildidx ...
    /// Returns the result of the dot string that describes this node
    fn to_dot_expanded_view(
        &self,
        node_index: &str,
        name: &str,
        inverted: bool,
        mut parent_identifier: String,
    ) -> OpmResult<String> {
y.zobus's avatar
y.zobus committed
        let inv_string = if inverted { "(inv)" } else { "" };
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        parent_identifier = if parent_identifier.is_empty() {
            format!("i{}", node_index)
        } else {
            format!("{}_i{}", &parent_identifier, node_index)
        };
y.zobus's avatar
y.zobus committed
        let mut dot_string = format!(
            "  subgraph {} {{\n\tlabel=\"{}{}\"\n\tfontsize=15\n\tcluster=true\n\t",
            parent_identifier, name, inv_string
        );

        for node_idx in self.g.0.node_indices() {
            let node = self.g.0.node_weight(node_idx).unwrap();
Udo Eisenbarth's avatar
Udo Eisenbarth committed
            dot_string += &node.optical_ref.borrow().to_dot(
                &format!("{}", node_idx.index()),
Udo Eisenbarth's avatar
Udo Eisenbarth committed
                node.optical_ref.borrow().name(),
                node.optical_ref.borrow().inverted(),
                &node.optical_ref.borrow().ports(),
                parent_identifier.clone(),
        for edge in self.g.0.edge_indices() {
            let light: &Light = self.g.0.edge_weight(edge).unwrap();
            let end_nodes = self.g.0.edge_endpoints(edge).unwrap();
y.zobus's avatar
y.zobus committed

            let src_edge_str = self.create_node_edge_str(
                end_nodes.0,
                light.src_port(),
                parent_identifier.clone(),
            )?;
            let target_edge_str = self.create_node_edge_str(
                end_nodes.1,
                light.target_port(),
                parent_identifier.clone(),
            )?;
y.zobus's avatar
y.zobus committed

            dot_string.push_str(&format!("  {} -> {} \n", src_edge_str, target_edge_str));
            // needed when multiple ports can be assigned
            // for src in src_edge_str.iter(){
            //     println!("{}", src);
            //     for target in target_edge_str.iter(){
            //         println!("{}", target);
            //         dot_string.push_str(&format!("  {} -> {} \n", src, target));
            //     };
            // };
        }
        dot_string += "}";
        Ok(dot_string)
    }

    /// creates the dot format of the group node in its collapsed view
    /// parameters:
    /// - node_index:           NodeIndex of the group
    /// - name:                 name of the node
    /// - inverted:             boolean that descries wether the node is inverted or not
    /// - _ports:               
    /// - parent_identifier:    String that contains the hierarchical structure: parentidx_childidx_childofchildidx ...
    /// Returns the result of the dot string that describes this node
    fn to_dot_collapsed_view(
        &self,
        node_index: &str,
        name: &str,
        inverted: bool,
        _ports: &OpticPorts,
        mut parent_identifier: String,
    ) -> OpmResult<String> {
y.zobus's avatar
y.zobus committed
        let inv_string = if inverted { " (inv)" } else { "" };
        let node_name = format!("{}{}", name, inv_string);
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        parent_identifier = if parent_identifier.is_empty() {
            format!("i{}", node_index)
        } else {
            format!("{}_i{}", &parent_identifier, node_index)
        };
y.zobus's avatar
y.zobus committed
        let mut dot_str = format!("\t{} [\n\t\tshape=plaintext\n", parent_identifier);
        let mut indent_level = 2;
        dot_str.push_str(&self.add_html_like_labels(
            &node_name,
            &mut indent_level,
            _ports,
            inverted,
            rankdir,
        ));
        Ok(dot_str)
y.zobus's avatar
y.zobus committed
    }
    fn invert_graph(&mut self) {
        for node in self.g.0.node_weights_mut() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
            node.optical_ref
                .borrow_mut()
                .set_property("inverted", true.into())
                .unwrap();
        for edge in self.g.0.edge_weights_mut() {
            edge.inverse();
        }
        self.g.0.reverse();
impl Serialize for NodeGroup {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_newtype_struct("props", self.properties())
    }
}
Udo Eisenbarth's avatar
Udo Eisenbarth committed
impl Optical for NodeGroup {
    fn name(&self) -> &str {
        if let Proptype::String(name) = &self.props.get("name").unwrap().prop {
            name
        } else {
            self.node_type()
        }
    }
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    fn node_type(&self) -> &str {
        "group"
    }
    fn ports(&self) -> OpticPorts {
        let mut ports = OpticPorts::new();
        for p in self.input_port_map().iter() {
            ports.add_input(p.0).unwrap();
        }
        for p in self.output_port_map().iter() {
            ports.add_output(p.0).unwrap();
        }
        ports
    }
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    fn analyze(
        &mut self,
        incoming_data: LightResult,
        analyzer_type: &AnalyzerType,
    ) -> OpmResult<LightResult> {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        self.analyze_group(incoming_data, analyzer_type)
    }
    fn as_group(&self) -> OpmResult<&NodeGroup> {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        Ok(self)
    fn properties(&self) -> &Properties {
        &self.props
    }
    fn set_property(&mut self, name: &str, prop: Property) -> OpmResult<()> {
        if self.props.set(name, prop).is_none() {
            Err(OpossumError::Other("property not defined".into()))
        } else {
            Ok(())
        }
    }
Udo Eisenbarth's avatar
Udo Eisenbarth committed
impl Dottable for NodeGroup {
    fn to_dot(
        &self,
        node_index: &str,
        name: &str,
        inverted: bool,
        ports: &OpticPorts,
        parent_identifier: String,
    ) -> OpmResult<String> {
        let mut cloned_self = self.clone();
        if self.props.get_bool("inverted").unwrap().unwrap() {
            cloned_self.invert_graph();
        }
        if self.shall_expand() {
            cloned_self.to_dot_expanded_view(node_index, name, inverted, parent_identifier, rankdir)
        } else {
            cloned_self.to_dot_collapsed_view(
                node_index,
                name,
                inverted,
                ports,
                parent_identifier,
                rankdir,
            )
    fn node_color(&self) -> &str {
        "yellow"
Udo Eisenbarth's avatar
Udo Eisenbarth committed
    }

#[cfg(test)]
mod test {
        nodes::{BeamSplitter, Dummy},
        optical::Optical,
    fn default() {
        let node = NodeGroup::default();
        assert_eq!(node.g.0.node_count(), 0);
        assert_eq!(node.g.0.edge_count(), 0);
        assert!(node.input_port_map().is_empty());
        assert!(node.output_port_map().is_empty());
        assert_eq!(node.name(), "group");
        assert_eq!(node.node_type(), "group");
        assert_eq!(node.is_detector(), false);
        assert_eq!(node.inverted(), false);
        assert_eq!(node.node_color(), "yellow");
        assert!(node.as_group().is_ok());
    }
    #[test]
    fn net() {
        let node = NodeGroup::new("test");
        assert_eq!(node.name(), "test");
    }
    #[test]
    fn add_node() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        og.add_node(Dummy::new("n1"));
        assert_eq!(og.g.0.node_count(), 1);
    }
    #[test]
    fn connect_nodes() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(Dummy::new("n1"));
        let sn2_i = og.add_node(Dummy::new("n2"));
        // wrong port names
        assert!(og.connect_nodes(sn1_i, "wrong", sn2_i, "front").is_err());
        assert_eq!(og.g.0.edge_count(), 0);
        assert!(og.connect_nodes(sn1_i, "rear", sn2_i, "wrong").is_err());
        assert_eq!(og.g.0.edge_count(), 0);
        // wrong node index
        assert!(og.connect_nodes(5.into(), "rear", sn2_i, "front").is_err());
        assert_eq!(og.g.0.edge_count(), 0);
        assert!(og.connect_nodes(sn1_i, "rear", 5.into(), "front").is_err());
        assert_eq!(og.g.0.edge_count(), 0);
        // correct usage
        assert!(og.connect_nodes(sn1_i, "rear", sn2_i, "front").is_ok());
        assert_eq!(og.g.0.edge_count(), 1);
    fn connect_nodes_update_port_mapping() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(Dummy::new("n1"));
        let sn2_i = og.add_node(Dummy::new("n2"));

        og.map_input_port(sn2_i, "front", "input").unwrap();
        og.map_output_port(sn1_i, "rear", "output").unwrap();
        assert_eq!(og.input_port_map().len(), 1);
        assert_eq!(og.output_port_map().len(), 1);
        og.connect_nodes(sn1_i, "rear", sn2_i, "front").unwrap();
        // delete no longer valid port mapping
        assert_eq!(og.input_port_map().len(), 0);
        assert_eq!(og.output_port_map().len(), 0);
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(Dummy::new("n1"));
        let sn2_i = og.add_node(Dummy::new("n2"));
        let sub_node3 = BeamSplitter::new("test", 0.5).unwrap();
        let sn3_i = og.add_node(sub_node3);
        og.connect_nodes(sn1_i, "rear", sn2_i, "front").unwrap();
        og.connect_nodes(sn2_i, "rear", sn3_i, "input1").unwrap();
        assert_eq!(og.input_nodes(), vec![0.into(), 2.into()])
    }
    #[test]
    fn output_nodes() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(Dummy::new("n1"));
        let sub_node1 = BeamSplitter::new("test", 0.5).unwrap();
        let sn2_i = og.add_node(sub_node1);
        let sn3_i = og.add_node(Dummy::new("n3"));
        og.connect_nodes(sn1_i, "rear", sn2_i, "input1").unwrap();
        og.connect_nodes(sn2_i, "out1_trans1_refl2", sn3_i, "front")
            .unwrap();
        assert_eq!(og.input_nodes(), vec![0.into(), 1.into()])
    fn map_input_port() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(Dummy::new("n1"));
        let sn2_i = og.add_node(Dummy::new("n2"));
        og.connect_nodes(sn1_i, "rear", sn2_i, "front").unwrap();

        // wrong port name
        assert!(og.map_input_port(sn1_i, "wrong", "input").is_err());
        assert!(og.input_port_map().is_empty());
        // wrong node index
        assert!(og.map_input_port(5.into(), "front", "input").is_err());
        assert!(og.input_port_map().is_empty());
        // map output port
        assert!(og.map_input_port(sn2_i, "rear", "input").is_err());
        assert!(og.input_port_map().is_empty());
        // map internal node
        assert!(og.map_input_port(sn2_i, "front", "input").is_err());
        assert!(og.input_port_map().is_empty());
        // correct usage
        assert!(og.map_input_port(sn1_i, "front", "input").is_ok());
        assert_eq!(og.input_port_map().len(), 1);
    fn map_input_port_half_connected_nodes() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(Dummy::new("n1"));
        let sn2_i = og.add_node(BeamSplitter::default());
        og.connect_nodes(sn1_i, "rear", sn2_i, "input1").unwrap();

        // node port already internally connected
        assert!(og.map_input_port(sn2_i, "input1", "bs_input").is_err());

        // correct usage
        assert!(og.map_input_port(sn1_i, "front", "input").is_ok());
        assert!(og.map_input_port(sn2_i, "input2", "bs_input").is_ok());
        assert_eq!(og.input_port_map().len(), 2);
    fn map_output_port() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(Dummy::new("n1"));
        let sn2_i = og.add_node(Dummy::new("n2"));
        og.connect_nodes(sn1_i, "rear", sn2_i, "front").unwrap();

        // wrong port name
        assert!(og.map_output_port(sn2_i, "wrong", "output").is_err());
        assert!(og.output_port_map().is_empty());
        // wrong node index
        assert!(og.map_output_port(5.into(), "rear", "output").is_err());
        assert!(og.output_port_map().is_empty());
        // map input port
        assert!(og.map_output_port(sn1_i, "front", "output").is_err());
        assert!(og.output_port_map().is_empty());
        // map internal node
        assert!(og.map_output_port(sn1_i, "rear", "output").is_err());
        assert!(og.output_port_map().is_empty());
        // correct usage
        assert!(og.map_output_port(sn2_i, "rear", "output").is_ok());
        assert_eq!(og.output_port_map().len(), 1);
    fn map_output_port_half_connected_nodes() {
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(BeamSplitter::default());
        let sn2_i = og.add_node(Dummy::new("n2"));
        og.connect_nodes(sn1_i, "out1_trans1_refl2", sn2_i, "front")
            .unwrap();

        // node port already internally connected
        assert!(og
            .map_output_port(sn1_i, "out1_trans1_refl2", "bs_output")
            .is_err());

        // correct usage
        assert!(og
            .map_output_port(sn1_i, "out2_trans2_refl1", "bs_output")
            .is_ok());
        assert!(og.map_output_port(sn2_i, "rear", "output").is_ok());
        assert_eq!(og.output_port_map().len(), 2);
Udo Eisenbarth's avatar
Udo Eisenbarth committed
        let mut og = NodeGroup::default();
        let sn1_i = og.add_node(Dummy::new("n1"));
        let sn2_i = og.add_node(Dummy::new("n2"));
        og.connect_nodes(sn1_i, "rear", sn2_i, "front").unwrap();
        assert!(og.ports().inputs().is_empty());
        assert!(og.ports().outputs().is_empty());
        og.map_input_port(sn1_i, "front", "input").unwrap();
        assert!(og.ports().inputs().contains(&("input".to_string())));
        og.map_output_port(sn2_i, "rear", "output").unwrap();
        assert!(og.ports().outputs().contains(&("output".to_string())));
    }
}