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 get_element_energies_from_dynamic(self, state):
        # number of nodes
        node_no = len(self.nodes)
        # number of internal dofs
        internal_dof_no = np.sum(e.num_degrees_of_freedom_dynamic()
                                 for e in self.elements)
        # number of terminals
        terminal_no = np.sum(e.num_terminals() for e in self.elements)

        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

        # todo: build energy and dissipation rate matrices
        e = np.zeros((num_equations, num_equations))
        p = np.zeros((num_equations, num_equations))

        current_offset = 0
        internal_dof_offset = 0
        energies = []
        for e_id, e in enumerate(self.elements):
            element_state_size = 2 * e.num_terminals(
            ) + e.num_degrees_of_freedom_dynamic()
            element_state = np.zeros((element_state_size, ), dtype=complex)
            for terminal_id, terminal_node in enumerate(
                    self.terminal_node_mapping[e_id]):
                node_id = self.nodes.index(terminal_node)
                element_state[terminal_id] = state[node_id]
                element_state[terminal_id +
                              e.num_terminals()] = state[node_no +
                                                         current_offset +
                                                         terminal_id]
            for internal_dof_id in range(e.num_degrees_of_freedom_dynamic()):
                element_state[2 * e.num_terminals() +
                              internal_dof_id] = state[node_no + terminal_no +
                                                       internal_dof_offset +
                                                       internal_dof_id]
            internal_dof_offset += e.num_degrees_of_freedom_dynamic()
            current_offset += e.num_terminals()
            energies.append(e.energy(element_state))

        return energies
 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())
     ])
    def get_element_dofs(self, element: TLSystemElement):
        self.map_dofs()
        e_id = self.elements.index(element)
        voltages = [
            self.dof_mapping[:len(self.nodes)].index(t)
            for t in self.terminal_node_mapping[e_id]
        ]

        terminal_no = np.sum(e.num_terminals() for e in self.elements)

        current_variables = self.dof_mapping[len(self.nodes):len(self.nodes) +
                                             terminal_no]
        internal_dof_variables = self.dof_mapping[len(self.nodes) +
                                                  terminal_no:]

        currents = [
            current_variables.index((e_id, p_id)) + len(self.nodes)
            for p_id in range(element.num_terminals())
        ]
        degrees_of_freedom = [internal_dof_variables.index((e_id, dof_id)) + len(self.nodes) + terminal_no \
                              for dof_id in range(element.num_degrees_of_freedom())]

        return voltages, currents, degrees_of_freedom
    def create_dynamic_equation_matrices(self):
        # number of nodes
        node_no = len(self.nodes)
        # number of internal dofs
        internal_dof_no = np.sum(e.num_degrees_of_freedom_dynamic()
                                 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

        dynamic_equation_matrix_a = np.zeros((num_equations, num_equations),
                                             dtype=float)
        dynamic_equation_matrix_b = np.zeros((num_equations, num_equations),
                                             dtype=float)

        # filling dynamic equations
        equation_id = 0
        current_offset = 0
        internal_dof_offset = 0
        for e_id, e in enumerate(self.elements):
            equations_a, equations_b = e.dynamic_equations()
            for element_equation_id in range(equations_b.shape[0]):
                equation_b = equations_b[element_equation_id, :]
                equation_a = equations_a[element_equation_id, :]
                for terminal_id, terminal_node in enumerate(
                        self.terminal_node_mapping[e_id]):
                    node_id = self.nodes.index(terminal_node)
                    dynamic_equation_matrix_a[
                        equation_id,
                        node_id] = equation_a[terminal_id]  # nodal voltages
                    dynamic_equation_matrix_a[
                        equation_id,
                        node_no + current_offset + terminal_id] = equation_a[
                            terminal_id + e.num_terminals()]  # nodal current
                    dynamic_equation_matrix_b[
                        equation_id,
                        node_id] = equation_b[terminal_id]  # nodal voltages
                    dynamic_equation_matrix_b[
                        equation_id,
                        node_no + current_offset + terminal_id] = equation_b[
                            terminal_id + e.num_terminals()]  # nodal current
                for internal_dof_id in range(
                        e.num_degrees_of_freedom_dynamic()):
                    dynamic_equation_matrix_a[
                        equation_id,
                        node_no + terminal_no + internal_dof_offset +
                        internal_dof_id] = equation_a[2 * e.num_terminals() +
                                                      internal_dof_id]
                    dynamic_equation_matrix_b[
                        equation_id,
                        node_no + terminal_no + internal_dof_offset +
                        internal_dof_id] = equation_b[2 * e.num_terminals() +
                                                      internal_dof_id]
                equation_id += 1
            internal_dof_offset += e.num_degrees_of_freedom_dynamic()
            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]):
                dynamic_equation_matrix_a[dynamic_equation_no +
                                          self.nodes.index(node),
                                          node_no + full_terminal_id] = 1
                full_terminal_id += 1
        return dynamic_equation_matrix_a, dynamic_equation_matrix_b