def create_boundary_problem_matrix(self, omega):
        # full dof number
        self.map_dofs()
        num_dof = len(self.dof_mapping)

        # number of nodes
        node_no = len(self.nodes)
        # number of internal dofs
        internal_dof_no = np.sum(e.num_degrees_of_freedom()
                                 for e in self.elements)
        # number of terminals
        terminal_no = np.sum(e.num_terminals() for e in self.elements)

        # dynamic equations reflect the element's IV characteristic
        dynamic_equation_no = terminal_no + internal_dof_no
        # kinetic equations are Kirchhof's law that the sum of nodal currents is zero
        kinetic_equation_no = node_no

        num_equations = dynamic_equation_no + kinetic_equation_no

        boundary_condition_matrix = np.zeros((num_equations, num_dof),
                                             dtype=complex)

        # filling dynamic equations
        equation_id = 0
        current_offset = 0
        internal_dof_offset = 0
        for e_id, e in enumerate(self.elements):
            equations = e.boundary_condition(omega)
            for element_equation_id in range(equations.shape[0]):
                equation = equations[element_equation_id, :]
                for terminal_id, terminal_node in enumerate(
                        self.terminal_node_mapping[e_id]):
                    node_id = self.nodes.index(terminal_node)
                    boundary_condition_matrix[equation_id, node_id] = equation[
                        terminal_id]  # nodal voltages
                    boundary_condition_matrix[
                        equation_id,
                        node_no + current_offset + terminal_id] = equation[
                            terminal_id + e.num_terminals()]  # nodal current
                for internal_dof_id in range(e.num_degrees_of_freedom()):
                    boundary_condition_matrix[
                        equation_id,
                        node_no + terminal_no + internal_dof_offset +
                        internal_dof_id] = equation[2 * e.num_terminals() +
                                                    internal_dof_id]
                equation_id += 1
            internal_dof_offset += e.num_degrees_of_freedom()
            current_offset += e.num_terminals()

        full_terminal_id = 0
        # filling kinetic equations
        for e_id, e in enumerate(self.elements):
            for terminal_id, node in enumerate(
                    self.terminal_node_mapping[e_id]):
                boundary_condition_matrix[dynamic_equation_no +
                                          self.nodes.index(node),
                                          node_no + full_terminal_id] = 1
                full_terminal_id += 1
        return boundary_condition_matrix
    def get_element_dynamic_equations(self, element: TLSystemElement):
        e_id = self.elements.index(element)
        offset = np.sum(e.num_terminals() + e.num_degrees_of_freedom()
                        for e in self.elements[:e_id])

        return np.arange(
            offset, offset + element.num_terminals() +
            element.num_degrees_of_freedom())
 def map_dofs(self):
     # count nodes
     self.dof_mapping = [n for n in self.nodes]  # nodal voltages
     self.dof_mapping.extend([(e_id, p_id)
                              for e_id, e in enumerate(self.elements)
                              for p_id in range(e.num_terminals())])
     # currents incident into each terminal
     self.dof_mapping.extend([
         (e_id, int_dof_id) for e_id, e in enumerate(self.elements)
         for int_dof_id in range(e.num_degrees_of_freedom())
     ])